Merge "[Fill Dialog Improvements] Implement Fill Dialog improvements" into main
diff --git a/core/api/current.txt b/core/api/current.txt
index 48ee065..6367002 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -27268,11 +27268,11 @@
method public void addActiveProcessingPictureListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.quality.MediaQualityManager.ActiveProcessingPictureListener);
method public void createPictureProfile(@NonNull android.media.quality.PictureProfile);
method public void createSoundProfile(@NonNull android.media.quality.SoundProfile);
- method @NonNull public java.util.List<android.media.quality.PictureProfile> getAvailablePictureProfiles();
- method @NonNull public java.util.List<android.media.quality.SoundProfile> getAvailableSoundProfiles();
+ method @NonNull public java.util.List<android.media.quality.PictureProfile> getAvailablePictureProfiles(boolean);
+ method @NonNull public java.util.List<android.media.quality.SoundProfile> getAvailableSoundProfiles(boolean);
method @NonNull public java.util.List<android.media.quality.ParamCapability> getParamCapabilities(@NonNull java.util.List<java.lang.String>);
- method @Nullable public android.media.quality.PictureProfile getPictureProfile(int, @NonNull String);
- method @Nullable public android.media.quality.SoundProfile getSoundProfile(int, @NonNull String);
+ method @Nullable public android.media.quality.PictureProfile getPictureProfile(int, @NonNull String, boolean);
+ method @Nullable public android.media.quality.SoundProfile getSoundProfile(int, @NonNull String, boolean);
method public boolean isAmbientBacklightEnabled();
method public boolean isAutoPictureQualityEnabled();
method public boolean isAutoSoundQualityEnabled();
@@ -27303,7 +27303,7 @@
public abstract static class MediaQualityManager.PictureProfileCallback {
ctor public MediaQualityManager.PictureProfileCallback();
- method public void onError(int);
+ method public void onError(@Nullable String, int);
method public void onParamCapabilitiesChanged(@Nullable String, @NonNull java.util.List<android.media.quality.ParamCapability>);
method public void onPictureProfileAdded(@NonNull String, @NonNull android.media.quality.PictureProfile);
method public void onPictureProfileRemoved(@NonNull String, @NonNull android.media.quality.PictureProfile);
@@ -27312,7 +27312,7 @@
public abstract static class MediaQualityManager.SoundProfileCallback {
ctor public MediaQualityManager.SoundProfileCallback();
- method public void onError(int);
+ method public void onError(@Nullable String, int);
method public void onParamCapabilitiesChanged(@Nullable String, @NonNull java.util.List<android.media.quality.ParamCapability>);
method public void onSoundProfileAdded(@NonNull String, @NonNull android.media.quality.SoundProfile);
method public void onSoundProfileRemoved(@NonNull String, @NonNull android.media.quality.SoundProfile);
@@ -34765,7 +34765,10 @@
method public android.os.MessageQueue getMessageQueue();
method public boolean hasMessages(android.os.Handler, Object, int);
method public boolean hasMessages(android.os.Handler, Object, Runnable);
+ method @FlaggedApi("android.os.message_queue_testability") public boolean isBlockedOnSyncBarrier();
method public android.os.Message next();
+ method @FlaggedApi("android.os.message_queue_testability") @Nullable public Long peekWhen();
+ method @FlaggedApi("android.os.message_queue_testability") @Nullable public android.os.Message pop();
method public void recycle(android.os.Message);
method public void release();
}
@@ -47460,6 +47463,7 @@
field public static final long NETWORK_TYPE_BITMASK_IWLAN = 131072L; // 0x20000L
field public static final long NETWORK_TYPE_BITMASK_LTE = 4096L; // 0x1000L
field @Deprecated public static final long NETWORK_TYPE_BITMASK_LTE_CA = 262144L; // 0x40000L
+ field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final long NETWORK_TYPE_BITMASK_NB_IOT_NTN = 1048576L; // 0x100000L
field public static final long NETWORK_TYPE_BITMASK_NR = 524288L; // 0x80000L
field public static final long NETWORK_TYPE_BITMASK_TD_SCDMA = 65536L; // 0x10000L
field public static final long NETWORK_TYPE_BITMASK_UMTS = 4L; // 0x4L
@@ -47479,6 +47483,7 @@
field @Deprecated public static final int NETWORK_TYPE_IDEN = 11; // 0xb
field public static final int NETWORK_TYPE_IWLAN = 18; // 0x12
field public static final int NETWORK_TYPE_LTE = 13; // 0xd
+ field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int NETWORK_TYPE_NB_IOT_NTN = 21; // 0x15
field public static final int NETWORK_TYPE_NR = 20; // 0x14
field public static final int NETWORK_TYPE_TD_SCDMA = 17; // 0x11
field public static final int NETWORK_TYPE_UMTS = 3; // 0x3
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 831e005..4a4776d 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -7990,10 +7990,10 @@
method public void addGlobalActiveProcessingPictureListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.quality.MediaQualityManager.ActiveProcessingPictureListener);
method @NonNull public java.util.List<java.lang.String> getPictureProfileAllowList();
method @NonNull public java.util.List<java.lang.String> getPictureProfilePackageNames();
- method @NonNull public java.util.List<android.media.quality.PictureProfile> getPictureProfilesByPackage(@NonNull String);
+ method @NonNull public java.util.List<android.media.quality.PictureProfile> getPictureProfilesByPackage(@NonNull String, boolean);
method @NonNull public java.util.List<java.lang.String> getSoundProfileAllowList();
method @NonNull public java.util.List<java.lang.String> getSoundProfilePackageNames();
- method @NonNull public java.util.List<android.media.quality.SoundProfile> getSoundProfilesByPackage(@NonNull String);
+ method @NonNull public java.util.List<android.media.quality.SoundProfile> getSoundProfilesByPackage(@NonNull String, boolean);
method public void setAutoPictureQualityEnabled(boolean);
method public void setAutoSoundQualityEnabled(boolean);
method public boolean setDefaultPictureProfile(@Nullable String);
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 38aea64..03ef669 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -1279,7 +1279,24 @@
*
* <p>This method should be utilized when an activity wants to nudge the user to switch
* to the web application in cases where the web may provide the user with a better
- * experience. Note that this method does not guarantee that the education will be shown.</p>
+ * experience. Note that this method does not guarantee that the education will be shown.
+ *
+ * <p>The number of times that the "Open in browser" education can be triggered by this method
+ * is limited per application, and, when shown, the education appears above the app's content.
+ * For these reasons, developers should use this method sparingly when it is least
+ * disruptive to the user to show the education and when it is optimal to switch the user to a
+ * browser session. Before requesting to show the education, developers should assert that they
+ * have set a link that can be used by the "Open in browser" feature through either
+ * {@link AssistContent#EXTRA_AUTHENTICATING_USER_WEB_URI} or
+ * {@link AssistContent#setWebUri} so that users are navigated to a relevant page if they choose
+ * to switch to the browser. If a URI is not set using either method, "Open in browser" will
+ * utilize a generic link if available which will direct users to the homepage of the site
+ * associated with the app. The generic link is provided for a limited number of applications by
+ * the system and cannot be edited by developers. If none of these options contains a valid URI,
+ * the user will not be provided with the option to switch to the browser and the education will
+ * not be shown if requested.
+ *
+ * @see android.app.assist.AssistContent#EXTRA_SESSION_TRANSFER_WEB_URI
*/
@FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION)
public final void requestOpenInBrowserEducation() {
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 33ba058..69d3e8d 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -1189,6 +1189,18 @@
return procState == PROCESS_STATE_FOREGROUND_SERVICE;
}
+ /** @hide Should this process state be considered jank perceptible? */
+ public static final boolean isProcStateJankPerceptible(int procState) {
+ if (Flags.jankPerceptibleNarrow()) {
+ return procState == PROCESS_STATE_PERSISTENT_UI
+ || procState == PROCESS_STATE_TOP
+ || procState == PROCESS_STATE_IMPORTANT_FOREGROUND
+ || procState == PROCESS_STATE_TOP_SLEEPING;
+ } else {
+ return !isProcStateCached(procState);
+ }
+ }
+
/** @hide requestType for assist context: only basic information. */
public static final int ASSIST_CONTEXT_BASIC = 0;
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index eccb6ff..abdfb535 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -44,6 +44,8 @@
import android.os.PowerExemptionManager.TempAllowListType;
import android.os.TransactionTooLargeException;
import android.os.WorkSource;
+import android.os.instrumentation.IOffsetCallback;
+import android.os.instrumentation.MethodDescriptor;
import android.util.ArraySet;
import android.util.Pair;
@@ -1352,6 +1354,14 @@
String reason, int exitInfoReason);
/**
+ * Queries the offset data for a given method on a process.
+ * @hide
+ */
+ public abstract void getExecutableMethodFileOffsets(@NonNull String processName,
+ int pid, int uid, @NonNull MethodDescriptor methodDescriptor,
+ @NonNull IOffsetCallback callback);
+
+ /**
* Add a creator token for all embedded intents (stored as extra) of the given intent.
*
* @param intent The given intent
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 27661ce..48b74f2 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -165,6 +165,10 @@
import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
+import android.os.instrumentation.ExecutableMethodFileOffsets;
+import android.os.instrumentation.IOffsetCallback;
+import android.os.instrumentation.MethodDescriptor;
+import android.os.instrumentation.MethodDescriptorParser;
import android.permission.IPermissionManager;
import android.provider.BlockedNumberContract;
import android.provider.CalendarContract;
@@ -2236,6 +2240,29 @@
args.arg6 = uiTranslationSpec;
sendMessage(H.UPDATE_UI_TRANSLATION_STATE, args);
}
+
+ @Override
+ public void getExecutableMethodFileOffsets(
+ @NonNull MethodDescriptor methodDescriptor,
+ @NonNull IOffsetCallback resultCallback) {
+ Method method = MethodDescriptorParser.parseMethodDescriptor(
+ getClass().getClassLoader(), methodDescriptor);
+ VMDebug.ExecutableMethodFileOffsets location =
+ VMDebug.getExecutableMethodFileOffsets(method);
+ try {
+ if (location == null) {
+ resultCallback.onResult(null);
+ return;
+ }
+ ExecutableMethodFileOffsets ret = new ExecutableMethodFileOffsets();
+ ret.containerPath = location.getContainerPath();
+ ret.containerOffset = location.getContainerOffset();
+ ret.methodOffset = location.getMethodOffset();
+ resultCallback.onResult(ret);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
private @NonNull SafeCancellationTransport createSafeCancellationTransport(
@@ -3918,12 +3945,7 @@
if (mLastProcessState == processState) {
return;
}
- // Do not issue a transitional GC if we are transitioning between 2 cached states.
- // Only update if the state flips between cached and uncached or vice versa
- if (ActivityManager.isProcStateCached(mLastProcessState)
- != ActivityManager.isProcStateCached(processState)) {
- updateVmProcessState(processState);
- }
+ updateVmProcessState(mLastProcessState, processState);
mLastProcessState = processState;
if (localLOGV) {
Slog.i(TAG, "******************* PROCESS STATE CHANGED TO: " + processState
@@ -3932,18 +3954,21 @@
}
}
+ /** Converts a process state to a VM process state. */
+ private static int toVmProcessState(int processState) {
+ final int state = ActivityManager.isProcStateJankPerceptible(processState)
+ ? VM_PROCESS_STATE_JANK_PERCEPTIBLE
+ : VM_PROCESS_STATE_JANK_IMPERCEPTIBLE;
+ return state;
+ }
+
/** Update VM state based on ActivityManager.PROCESS_STATE_* constants. */
- // Currently ART VM only uses state updates for Transitional GC, and thus
- // this function initiates a Transitional GC for transitions into Cached apps states.
- private void updateVmProcessState(int processState) {
- // Only a transition into Cached state should result in a Transitional GC request
- // to the ART runtime. Update VM state to JANK_IMPERCEPTIBLE in that case.
- // Note that there are 4 possible cached states currently, all of which are
- // JANK_IMPERCEPTIBLE from GC point of view.
- final int state = ActivityManager.isProcStateCached(processState)
- ? VM_PROCESS_STATE_JANK_IMPERCEPTIBLE
- : VM_PROCESS_STATE_JANK_PERCEPTIBLE;
- VMRuntime.getRuntime().updateProcessState(state);
+ private void updateVmProcessState(int lastProcessState, int newProcessState) {
+ final int state = toVmProcessState(newProcessState);
+ if (lastProcessState == PROCESS_STATE_UNKNOWN
+ || state != toVmProcessState(lastProcessState)) {
+ VMRuntime.getRuntime().updateProcessState(state);
+ }
}
@Override
diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl
index 06d01ec..063501b 100644
--- a/core/java/android/app/IApplicationThread.aidl
+++ b/core/java/android/app/IApplicationThread.aidl
@@ -46,6 +46,8 @@
import android.os.PersistableBundle;
import android.os.RemoteCallback;
import android.os.SharedMemory;
+import android.os.instrumentation.IOffsetCallback;
+import android.os.instrumentation.MethodDescriptor;
import android.view.autofill.AutofillId;
import android.view.translation.TranslationSpec;
import android.view.translation.UiTranslationSpec;
@@ -183,4 +185,6 @@
void scheduleTimeoutService(IBinder token, int startId);
void scheduleTimeoutServiceForType(IBinder token, int startId, int fgsType);
void schedulePing(in RemoteCallback pong);
+ void getExecutableMethodFileOffsets(in MethodDescriptor methodDescriptor,
+ in IOffsetCallback resultCallback);
}
diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig
index bea3010..720e045 100644
--- a/core/java/android/app/activity_manager.aconfig
+++ b/core/java/android/app/activity_manager.aconfig
@@ -156,3 +156,10 @@
bug: "362537357"
is_exported: true
}
+
+flag {
+ name: "jank_perceptible_narrow"
+ namespace: "system_performance"
+ description: "Narrow the scope of Jank Perceptible"
+ bug: "304837972"
+}
diff --git a/core/java/android/companion/DeviceId.java b/core/java/android/companion/DeviceId.java
index f66a1ae..d9514a0 100644
--- a/core/java/android/companion/DeviceId.java
+++ b/core/java/android/companion/DeviceId.java
@@ -154,6 +154,10 @@
/**
* A builder for {@link DeviceId}
+ *
+ * <p>Calling apps must provide at least one of the following to identify
+ * the device: a custom ID using {@link #setCustomId(String)}, or a MAC address using
+ * {@link #setMacAddress(MacAddress)}.</p>
*/
public static final class Builder extends OneTimeUseBuilder<DeviceId> {
private String mCustomId;
diff --git a/core/java/android/hardware/contexthub/HubEndpointSession.java b/core/java/android/hardware/contexthub/HubEndpointSession.java
index b8af398..77f937e 100644
--- a/core/java/android/hardware/contexthub/HubEndpointSession.java
+++ b/core/java/android/hardware/contexthub/HubEndpointSession.java
@@ -69,6 +69,8 @@
* @return For messages that does not require a response, the transaction will immediately
* complete. For messages that requires a response, the transaction will complete after
* receiving the response for the message.
+ * @throws SecurityException if the application doesn't have the right permissions to send this
+ * message.
*/
@NonNull
@RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java
index 310e1a6..d9888ad 100644
--- a/core/java/android/hardware/location/ContextHubManager.java
+++ b/core/java/android/hardware/location/ContextHubManager.java
@@ -841,6 +841,7 @@
* @param endpointId The identifier of the hub endpoint.
* @param callback The callback to be invoked.
* @param executor The executor to invoke the callback on.
+ * @throws UnsupportedOperationException If the operation is not supported.
*/
@RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@FlaggedApi(Flags.FLAG_OFFLOAD_API)
@@ -881,6 +882,7 @@
* @param callback The callback to be invoked.
* @param executor The executor to invoke the callback on.
* @throws IllegalArgumentException if the serviceDescriptor is empty.
+ * @throws UnsupportedOperationException If the operation is not supported.
*/
@RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@FlaggedApi(Flags.FLAG_OFFLOAD_API)
@@ -911,6 +913,7 @@
*
* @param callback The callback previously registered.
* @throws IllegalArgumentException If the callback was not previously registered.
+ * @throws UnsupportedOperationException If the operation is not supported.
*/
@RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@FlaggedApi(Flags.FLAG_OFFLOAD_API)
@@ -1311,6 +1314,8 @@
* endpoint discovery results (e.g. from {@link ContextHubManager#findEndpoints(long)}).
* @param serviceDescriptor A string that describes the service associated with this session.
* The information will be sent to the destination as part of open request.
+ * @throws IllegalStateException if hubEndpoint was not successfully registered, or if there is
+ * insufficient capacity for creating a session.
*/
@RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@FlaggedApi(Flags.FLAG_OFFLOAD_API)
diff --git a/core/java/android/os/CombinedMessageQueue/MessageQueue.java b/core/java/android/os/CombinedMessageQueue/MessageQueue.java
index 11b80ce..ce56a4f 100644
--- a/core/java/android/os/CombinedMessageQueue/MessageQueue.java
+++ b/core/java/android/os/CombinedMessageQueue/MessageQueue.java
@@ -1353,6 +1353,69 @@
}
}
+ /**
+ * @return true if we are blocked on a sync barrier
+ */
+ boolean isBlockedOnSyncBarrier() {
+ throwIfNotTest();
+ if (mUseConcurrent) {
+ Iterator<MessageNode> queueIter = mPriorityQueue.iterator();
+ MessageNode queueNode = iterateNext(queueIter);
+
+ if (queueNode.isBarrier()) {
+ long now = SystemClock.uptimeMillis();
+
+ /* Look for a deliverable async node. If one exists we are not blocked. */
+ Iterator<MessageNode> asyncQueueIter = mAsyncPriorityQueue.iterator();
+ MessageNode asyncNode = iterateNext(asyncQueueIter);
+ if (asyncNode != null && now >= asyncNode.getWhen()) {
+ return false;
+ }
+ /*
+ * Look for a deliverable sync node. In this case, if one exists we are blocked
+ * since the barrier prevents delivery of the Message.
+ */
+ while (queueNode.isBarrier()) {
+ queueNode = iterateNext(queueIter);
+ }
+ if (queueNode != null && now >= queueNode.getWhen()) {
+ return true;
+ }
+
+ return false;
+ }
+ } else {
+ Message msg = mMessages;
+ if (msg != null && msg.target == null) {
+ Message iter = msg;
+ /* Look for a deliverable async node */
+ do {
+ iter = iter.next;
+ } while (iter != null && !iter.isAsynchronous());
+
+ long now = SystemClock.uptimeMillis();
+ if (iter != null && now >= iter.when) {
+ return false;
+ }
+ /*
+ * Look for a deliverable sync node. In this case, if one exists we are blocked
+ * since the barrier prevents delivery of the Message.
+ */
+ iter = msg;
+ do {
+ iter = iter.next;
+ } while (iter != null && (iter.target == null || iter.isAsynchronous()));
+
+ if (iter != null && now >= iter.when) {
+ return true;
+ }
+ return false;
+ }
+ }
+ /* No barrier was found. */
+ return false;
+ }
+
private static final class MatchHandlerWhatAndObject extends MessageCompare {
@Override
public boolean compareMessage(MessageNode n, Handler h, int what, Object object, Runnable r,
diff --git a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
index 47778ed..576c4cc 100644
--- a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
+++ b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
@@ -1107,6 +1107,38 @@
return nextMessage(false);
}
+ /**
+ * @return true if we are blocked on a sync barrier
+ */
+ boolean isBlockedOnSyncBarrier() {
+ throwIfNotTest();
+ Iterator<MessageNode> queueIter = mPriorityQueue.iterator();
+ MessageNode queueNode = iterateNext(queueIter);
+
+ if (queueNode.isBarrier()) {
+ long now = SystemClock.uptimeMillis();
+
+ /* Look for a deliverable async node. If one exists we are not blocked. */
+ Iterator<MessageNode> asyncQueueIter = mAsyncPriorityQueue.iterator();
+ MessageNode asyncNode = iterateNext(asyncQueueIter);
+ if (asyncNode != null && now >= asyncNode.getWhen()) {
+ return false;
+ }
+ /*
+ * Look for a deliverable sync node. In this case, if one exists we are blocked
+ * since the barrier prevents delivery of the Message.
+ */
+ while (queueNode.isBarrier()) {
+ queueNode = iterateNext(queueIter);
+ }
+ if (queueNode != null && now >= queueNode.getWhen()) {
+ return true;
+ }
+
+ return false;
+ }
+ }
+
private StateNode getStateNode(StackNode node) {
if (node.isMessageNode()) {
return ((MessageNode) node).mBottomOfStack;
diff --git a/core/java/android/os/LegacyMessageQueue/MessageQueue.java b/core/java/android/os/LegacyMessageQueue/MessageQueue.java
index f49acd1..10d0904 100644
--- a/core/java/android/os/LegacyMessageQueue/MessageQueue.java
+++ b/core/java/android/os/LegacyMessageQueue/MessageQueue.java
@@ -812,6 +812,40 @@
return legacyPeekOrPop(false);
}
+ /**
+ * @return true if we are blocked on a sync barrier
+ */
+ boolean isBlockedOnSyncBarrier() {
+ throwIfNotTest();
+ Message msg = mMessages;
+ if (msg != null && msg.target == null) {
+ Message iter = msg;
+ /* Look for a deliverable async node */
+ do {
+ iter = iter.next;
+ } while (iter != null && !iter.isAsynchronous());
+
+ long now = SystemClock.uptimeMillis();
+ if (iter != null && now >= iter.when) {
+ return false;
+ }
+ /*
+ * Look for a deliverable sync node. In this case, if one exists we are blocked
+ * since the barrier prevents delivery of the Message.
+ */
+ iter = msg;
+ do {
+ iter = iter.next;
+ } while (iter != null && (iter.target == null || iter.isAsynchronous()));
+
+ if (iter != null && now >= iter.when) {
+ return true;
+ }
+ return false;
+ }
+ return false;
+ }
+
boolean hasMessages(Handler h, int what, Object object) {
if (h == null) {
return false;
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index d7e7ff2..cf473ec 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -50,7 +50,6 @@
import dalvik.annotation.optimization.CriticalNative;
import dalvik.annotation.optimization.FastNative;
-import dalvik.annotation.optimization.NeverInline;
import libcore.util.SneakyThrow;
@@ -588,17 +587,6 @@
return parcel;
}
- @NeverInline
- private void errorUsedWhileRecycling() {
- Log.wtf(TAG, "Parcel used while recycled. "
- + Log.getStackTraceString(new Throwable())
- + " Original recycle call (if DEBUG_RECYCLE): ", mStack);
- }
-
- private void assertNotRecycled() {
- if (mRecycled) errorUsedWhileRecycling();
- }
-
/**
* Put a Parcel object back into the pool. You must not touch
* the object after this call.
@@ -647,7 +635,6 @@
* @hide
*/
public void setReadWriteHelper(@Nullable ReadWriteHelper helper) {
- assertNotRecycled();
mReadWriteHelper = helper != null ? helper : ReadWriteHelper.DEFAULT;
}
@@ -657,7 +644,6 @@
* @hide
*/
public boolean hasReadWriteHelper() {
- assertNotRecycled();
return (mReadWriteHelper != null) && (mReadWriteHelper != ReadWriteHelper.DEFAULT);
}
@@ -684,7 +670,6 @@
* @hide
*/
public final void markSensitive() {
- assertNotRecycled();
nativeMarkSensitive(mNativePtr);
}
@@ -701,7 +686,6 @@
* @hide
*/
public final boolean isForRpc() {
- assertNotRecycled();
return nativeIsForRpc(mNativePtr);
}
@@ -709,25 +693,21 @@
@ParcelFlags
@TestApi
public int getFlags() {
- assertNotRecycled();
return mFlags;
}
/** @hide */
public void setFlags(@ParcelFlags int flags) {
- assertNotRecycled();
mFlags = flags;
}
/** @hide */
public void addFlags(@ParcelFlags int flags) {
- assertNotRecycled();
mFlags |= flags;
}
/** @hide */
private boolean hasFlags(@ParcelFlags int flags) {
- assertNotRecycled();
return (mFlags & flags) == flags;
}
@@ -740,7 +720,6 @@
// We don't really need to protect it; even if 3p / non-system apps, nothing would happen.
// This would only work when used on a reply parcel by a binder object that's allowed-blocking.
public void setPropagateAllowBlocking() {
- assertNotRecycled();
addFlags(FLAG_PROPAGATE_ALLOW_BLOCKING);
}
@@ -748,7 +727,6 @@
* Returns the total amount of data contained in the parcel.
*/
public int dataSize() {
- assertNotRecycled();
return nativeDataSize(mNativePtr);
}
@@ -757,7 +735,6 @@
* parcel. That is, {@link #dataSize}-{@link #dataPosition}.
*/
public final int dataAvail() {
- assertNotRecycled();
return nativeDataAvail(mNativePtr);
}
@@ -766,7 +743,6 @@
* more than {@link #dataSize}.
*/
public final int dataPosition() {
- assertNotRecycled();
return nativeDataPosition(mNativePtr);
}
@@ -777,7 +753,6 @@
* data buffer.
*/
public final int dataCapacity() {
- assertNotRecycled();
return nativeDataCapacity(mNativePtr);
}
@@ -789,7 +764,6 @@
* @param size The new number of bytes in the Parcel.
*/
public final void setDataSize(int size) {
- assertNotRecycled();
nativeSetDataSize(mNativePtr, size);
}
@@ -799,7 +773,6 @@
* {@link #dataSize}.
*/
public final void setDataPosition(int pos) {
- assertNotRecycled();
nativeSetDataPosition(mNativePtr, pos);
}
@@ -811,13 +784,11 @@
* with this method.
*/
public final void setDataCapacity(int size) {
- assertNotRecycled();
nativeSetDataCapacity(mNativePtr, size);
}
/** @hide */
public final boolean pushAllowFds(boolean allowFds) {
- assertNotRecycled();
return nativePushAllowFds(mNativePtr, allowFds);
}
@@ -838,7 +809,6 @@
* in different versions of the platform.
*/
public final byte[] marshall() {
- assertNotRecycled();
return nativeMarshall(mNativePtr);
}
@@ -846,18 +816,15 @@
* Fills the raw bytes of this Parcel with the supplied data.
*/
public final void unmarshall(@NonNull byte[] data, int offset, int length) {
- assertNotRecycled();
nativeUnmarshall(mNativePtr, data, offset, length);
}
public final void appendFrom(Parcel parcel, int offset, int length) {
- assertNotRecycled();
nativeAppendFrom(mNativePtr, parcel.mNativePtr, offset, length);
}
/** @hide */
public int compareData(Parcel other) {
- assertNotRecycled();
return nativeCompareData(mNativePtr, other.mNativePtr);
}
@@ -868,7 +835,6 @@
/** @hide */
public final void setClassCookie(Class clz, Object cookie) {
- assertNotRecycled();
if (mClassCookies == null) {
mClassCookies = new ArrayMap<>();
}
@@ -878,13 +844,11 @@
/** @hide */
@Nullable
public final Object getClassCookie(Class clz) {
- assertNotRecycled();
return mClassCookies != null ? mClassCookies.get(clz) : null;
}
/** @hide */
public void removeClassCookie(Class clz, Object expectedCookie) {
- assertNotRecycled();
if (mClassCookies != null) {
Object removedCookie = mClassCookies.remove(clz);
if (removedCookie != expectedCookie) {
@@ -902,25 +866,21 @@
* @hide
*/
public boolean hasClassCookie(Class clz) {
- assertNotRecycled();
return mClassCookies != null && mClassCookies.containsKey(clz);
}
/** @hide */
public final void adoptClassCookies(Parcel from) {
- assertNotRecycled();
mClassCookies = from.mClassCookies;
}
/** @hide */
public Map<Class, Object> copyClassCookies() {
- assertNotRecycled();
return new ArrayMap<>(mClassCookies);
}
/** @hide */
public void putClassCookies(Map<Class, Object> cookies) {
- assertNotRecycled();
if (cookies == null) {
return;
}
@@ -940,7 +900,6 @@
* if the return value changes.
*/
public boolean hasFileDescriptors() {
- assertNotRecycled();
return nativeHasFileDescriptors(mNativePtr);
}
@@ -962,7 +921,6 @@
* @throws IllegalArgumentException if the parameters are out of the permitted ranges.
*/
public boolean hasFileDescriptors(int offset, int length) {
- assertNotRecycled();
return nativeHasFileDescriptorsInRange(mNativePtr, offset, length);
}
@@ -1060,7 +1018,6 @@
* @hide
*/
public boolean hasBinders() {
- assertNotRecycled();
return nativeHasBinders(mNativePtr);
}
@@ -1084,7 +1041,6 @@
* @hide
*/
public boolean hasBinders(int offset, int length) {
- assertNotRecycled();
return nativeHasBindersInRange(mNativePtr, offset, length);
}
@@ -1095,7 +1051,6 @@
* at the beginning of transactions as a header.
*/
public final void writeInterfaceToken(@NonNull String interfaceName) {
- assertNotRecycled();
nativeWriteInterfaceToken(mNativePtr, interfaceName);
}
@@ -1106,7 +1061,6 @@
* should propagate to the caller.
*/
public final void enforceInterface(@NonNull String interfaceName) {
- assertNotRecycled();
nativeEnforceInterface(mNativePtr, interfaceName);
}
@@ -1117,7 +1071,6 @@
* When used over binder, this exception should propagate to the caller.
*/
public void enforceNoDataAvail() {
- assertNotRecycled();
final int n = dataAvail();
if (n > 0) {
throw new BadParcelableException("Parcel data not fully consumed, unread size: " + n);
@@ -1134,7 +1087,6 @@
* @hide
*/
public boolean replaceCallingWorkSourceUid(int workSourceUid) {
- assertNotRecycled();
return nativeReplaceCallingWorkSourceUid(mNativePtr, workSourceUid);
}
@@ -1151,7 +1103,6 @@
* @hide
*/
public int readCallingWorkSourceUid() {
- assertNotRecycled();
return nativeReadCallingWorkSourceUid(mNativePtr);
}
@@ -1161,7 +1112,6 @@
* @param b Bytes to place into the parcel.
*/
public final void writeByteArray(@Nullable byte[] b) {
- assertNotRecycled();
writeByteArray(b, 0, (b != null) ? b.length : 0);
}
@@ -1173,7 +1123,6 @@
* @param len Number of bytes to write.
*/
public final void writeByteArray(@Nullable byte[] b, int offset, int len) {
- assertNotRecycled();
if (b == null) {
writeInt(-1);
return;
@@ -1195,7 +1144,6 @@
* @see #readBlob()
*/
public final void writeBlob(@Nullable byte[] b) {
- assertNotRecycled();
writeBlob(b, 0, (b != null) ? b.length : 0);
}
@@ -1214,7 +1162,6 @@
* @see #readBlob()
*/
public final void writeBlob(@Nullable byte[] b, int offset, int len) {
- assertNotRecycled();
if (b == null) {
writeInt(-1);
return;
@@ -1233,7 +1180,6 @@
* growing dataCapacity() if needed.
*/
public final void writeInt(int val) {
- assertNotRecycled();
int err = nativeWriteInt(mNativePtr, val);
if (err != OK) {
nativeSignalExceptionForError(err);
@@ -1245,7 +1191,6 @@
* growing dataCapacity() if needed.
*/
public final void writeLong(long val) {
- assertNotRecycled();
int err = nativeWriteLong(mNativePtr, val);
if (err != OK) {
nativeSignalExceptionForError(err);
@@ -1257,7 +1202,6 @@
* dataPosition(), growing dataCapacity() if needed.
*/
public final void writeFloat(float val) {
- assertNotRecycled();
int err = nativeWriteFloat(mNativePtr, val);
if (err != OK) {
nativeSignalExceptionForError(err);
@@ -1269,7 +1213,6 @@
* current dataPosition(), growing dataCapacity() if needed.
*/
public final void writeDouble(double val) {
- assertNotRecycled();
int err = nativeWriteDouble(mNativePtr, val);
if (err != OK) {
nativeSignalExceptionForError(err);
@@ -1281,19 +1224,16 @@
* growing dataCapacity() if needed.
*/
public final void writeString(@Nullable String val) {
- assertNotRecycled();
writeString16(val);
}
/** {@hide} */
public final void writeString8(@Nullable String val) {
- assertNotRecycled();
mReadWriteHelper.writeString8(this, val);
}
/** {@hide} */
public final void writeString16(@Nullable String val) {
- assertNotRecycled();
mReadWriteHelper.writeString16(this, val);
}
@@ -1305,19 +1245,16 @@
* @hide
*/
public void writeStringNoHelper(@Nullable String val) {
- assertNotRecycled();
writeString16NoHelper(val);
}
/** {@hide} */
public void writeString8NoHelper(@Nullable String val) {
- assertNotRecycled();
nativeWriteString8(mNativePtr, val);
}
/** {@hide} */
public void writeString16NoHelper(@Nullable String val) {
- assertNotRecycled();
nativeWriteString16(mNativePtr, val);
}
@@ -1329,7 +1266,6 @@
* for true or false, respectively, but may change in the future.
*/
public final void writeBoolean(boolean val) {
- assertNotRecycled();
writeInt(val ? 1 : 0);
}
@@ -1341,7 +1277,6 @@
@UnsupportedAppUsage
@RavenwoodThrow(blockedBy = android.text.Spanned.class)
public final void writeCharSequence(@Nullable CharSequence val) {
- assertNotRecycled();
TextUtils.writeToParcel(val, this, 0);
}
@@ -1350,7 +1285,6 @@
* growing dataCapacity() if needed.
*/
public final void writeStrongBinder(IBinder val) {
- assertNotRecycled();
nativeWriteStrongBinder(mNativePtr, val);
}
@@ -1359,7 +1293,6 @@
* growing dataCapacity() if needed.
*/
public final void writeStrongInterface(IInterface val) {
- assertNotRecycled();
writeStrongBinder(val == null ? null : val.asBinder());
}
@@ -1374,7 +1307,6 @@
* if {@link Parcelable#PARCELABLE_WRITE_RETURN_VALUE} is set.</p>
*/
public final void writeFileDescriptor(@NonNull FileDescriptor val) {
- assertNotRecycled();
nativeWriteFileDescriptor(mNativePtr, val);
}
@@ -1383,7 +1315,6 @@
* This will be the new name for writeFileDescriptor, for consistency.
**/
public final void writeRawFileDescriptor(@NonNull FileDescriptor val) {
- assertNotRecycled();
nativeWriteFileDescriptor(mNativePtr, val);
}
@@ -1394,7 +1325,6 @@
* @param value The array of objects to be written.
*/
public final void writeRawFileDescriptorArray(@Nullable FileDescriptor[] value) {
- assertNotRecycled();
if (value != null) {
int N = value.length;
writeInt(N);
@@ -1414,7 +1344,6 @@
* the future.
*/
public final void writeByte(byte val) {
- assertNotRecycled();
writeInt(val);
}
@@ -1430,7 +1359,6 @@
* allows you to avoid mysterious type errors at the point of marshalling.
*/
public final void writeMap(@Nullable Map val) {
- assertNotRecycled();
writeMapInternal((Map<String, Object>) val);
}
@@ -1439,7 +1367,6 @@
* growing dataCapacity() if needed. The Map keys must be String objects.
*/
/* package */ void writeMapInternal(@Nullable Map<String,Object> val) {
- assertNotRecycled();
if (val == null) {
writeInt(-1);
return;
@@ -1465,7 +1392,6 @@
* growing dataCapacity() if needed. The Map keys must be String objects.
*/
/* package */ void writeArrayMapInternal(@Nullable ArrayMap<String, Object> val) {
- assertNotRecycled();
if (val == null) {
writeInt(-1);
return;
@@ -1495,7 +1421,6 @@
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public void writeArrayMap(@Nullable ArrayMap<String, Object> val) {
- assertNotRecycled();
writeArrayMapInternal(val);
}
@@ -1514,7 +1439,6 @@
*/
public <T extends Parcelable> void writeTypedArrayMap(@Nullable ArrayMap<String, T> val,
int parcelableFlags) {
- assertNotRecycled();
if (val == null) {
writeInt(-1);
return;
@@ -1536,7 +1460,6 @@
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public void writeArraySet(@Nullable ArraySet<? extends Object> val) {
- assertNotRecycled();
final int size = (val != null) ? val.size() : -1;
writeInt(size);
for (int i = 0; i < size; i++) {
@@ -1549,7 +1472,6 @@
* growing dataCapacity() if needed.
*/
public final void writeBundle(@Nullable Bundle val) {
- assertNotRecycled();
if (val == null) {
writeInt(-1);
return;
@@ -1563,7 +1485,6 @@
* growing dataCapacity() if needed.
*/
public final void writePersistableBundle(@Nullable PersistableBundle val) {
- assertNotRecycled();
if (val == null) {
writeInt(-1);
return;
@@ -1577,7 +1498,6 @@
* growing dataCapacity() if needed.
*/
public final void writeSize(@NonNull Size val) {
- assertNotRecycled();
writeInt(val.getWidth());
writeInt(val.getHeight());
}
@@ -1587,7 +1507,6 @@
* growing dataCapacity() if needed.
*/
public final void writeSizeF(@NonNull SizeF val) {
- assertNotRecycled();
writeFloat(val.getWidth());
writeFloat(val.getHeight());
}
@@ -1598,7 +1517,6 @@
* {@link #writeValue} and must follow the specification there.
*/
public final void writeList(@Nullable List val) {
- assertNotRecycled();
if (val == null) {
writeInt(-1);
return;
@@ -1618,7 +1536,6 @@
* {@link #writeValue} and must follow the specification there.
*/
public final void writeArray(@Nullable Object[] val) {
- assertNotRecycled();
if (val == null) {
writeInt(-1);
return;
@@ -1639,7 +1556,6 @@
* specification there.
*/
public final <T> void writeSparseArray(@Nullable SparseArray<T> val) {
- assertNotRecycled();
if (val == null) {
writeInt(-1);
return;
@@ -1655,7 +1571,6 @@
}
public final void writeSparseBooleanArray(@Nullable SparseBooleanArray val) {
- assertNotRecycled();
if (val == null) {
writeInt(-1);
return;
@@ -1674,7 +1589,6 @@
* @hide
*/
public final void writeSparseIntArray(@Nullable SparseIntArray val) {
- assertNotRecycled();
if (val == null) {
writeInt(-1);
return;
@@ -1690,7 +1604,6 @@
}
public final void writeBooleanArray(@Nullable boolean[] val) {
- assertNotRecycled();
if (val != null) {
int N = val.length;
writeInt(N);
@@ -1725,7 +1638,6 @@
}
private void ensureWithinMemoryLimit(int typeSize, @NonNull int... dimensions) {
- assertNotRecycled();
// For Multidimensional arrays, Calculate total object
// which will be allocated.
int totalObjects = 1;
@@ -1743,7 +1655,6 @@
}
private void ensureWithinMemoryLimit(int typeSize, int length) {
- assertNotRecycled();
int estimatedAllocationSize = 0;
try {
estimatedAllocationSize = Math.multiplyExact(typeSize, length);
@@ -1767,7 +1678,6 @@
@Nullable
public final boolean[] createBooleanArray() {
- assertNotRecycled();
int N = readInt();
ensureWithinMemoryLimit(SIZE_BOOLEAN, N);
// >>2 as a fast divide-by-4 works in the create*Array() functions
@@ -1785,7 +1695,6 @@
}
public final void readBooleanArray(@NonNull boolean[] val) {
- assertNotRecycled();
int N = readInt();
if (N == val.length) {
for (int i=0; i<N; i++) {
@@ -1798,7 +1707,6 @@
/** @hide */
public void writeShortArray(@Nullable short[] val) {
- assertNotRecycled();
if (val != null) {
int n = val.length;
writeInt(n);
@@ -1813,7 +1721,6 @@
/** @hide */
@Nullable
public short[] createShortArray() {
- assertNotRecycled();
int n = readInt();
ensureWithinMemoryLimit(SIZE_SHORT, n);
if (n >= 0 && n <= (dataAvail() >> 2)) {
@@ -1829,7 +1736,6 @@
/** @hide */
public void readShortArray(@NonNull short[] val) {
- assertNotRecycled();
int n = readInt();
if (n == val.length) {
for (int i = 0; i < n; i++) {
@@ -1841,7 +1747,6 @@
}
public final void writeCharArray(@Nullable char[] val) {
- assertNotRecycled();
if (val != null) {
int N = val.length;
writeInt(N);
@@ -1855,7 +1760,6 @@
@Nullable
public final char[] createCharArray() {
- assertNotRecycled();
int N = readInt();
ensureWithinMemoryLimit(SIZE_CHAR, N);
if (N >= 0 && N <= (dataAvail() >> 2)) {
@@ -1870,7 +1774,6 @@
}
public final void readCharArray(@NonNull char[] val) {
- assertNotRecycled();
int N = readInt();
if (N == val.length) {
for (int i=0; i<N; i++) {
@@ -1882,7 +1785,6 @@
}
public final void writeIntArray(@Nullable int[] val) {
- assertNotRecycled();
if (val != null) {
int N = val.length;
writeInt(N);
@@ -1896,7 +1798,6 @@
@Nullable
public final int[] createIntArray() {
- assertNotRecycled();
int N = readInt();
ensureWithinMemoryLimit(SIZE_INT, N);
if (N >= 0 && N <= (dataAvail() >> 2)) {
@@ -1911,7 +1812,6 @@
}
public final void readIntArray(@NonNull int[] val) {
- assertNotRecycled();
int N = readInt();
if (N == val.length) {
for (int i=0; i<N; i++) {
@@ -1923,7 +1823,6 @@
}
public final void writeLongArray(@Nullable long[] val) {
- assertNotRecycled();
if (val != null) {
int N = val.length;
writeInt(N);
@@ -1937,7 +1836,6 @@
@Nullable
public final long[] createLongArray() {
- assertNotRecycled();
int N = readInt();
ensureWithinMemoryLimit(SIZE_LONG, N);
// >>3 because stored longs are 64 bits
@@ -1953,7 +1851,6 @@
}
public final void readLongArray(@NonNull long[] val) {
- assertNotRecycled();
int N = readInt();
if (N == val.length) {
for (int i=0; i<N; i++) {
@@ -1965,7 +1862,6 @@
}
public final void writeFloatArray(@Nullable float[] val) {
- assertNotRecycled();
if (val != null) {
int N = val.length;
writeInt(N);
@@ -1979,7 +1875,6 @@
@Nullable
public final float[] createFloatArray() {
- assertNotRecycled();
int N = readInt();
ensureWithinMemoryLimit(SIZE_FLOAT, N);
// >>2 because stored floats are 4 bytes
@@ -1995,7 +1890,6 @@
}
public final void readFloatArray(@NonNull float[] val) {
- assertNotRecycled();
int N = readInt();
if (N == val.length) {
for (int i=0; i<N; i++) {
@@ -2007,7 +1901,6 @@
}
public final void writeDoubleArray(@Nullable double[] val) {
- assertNotRecycled();
if (val != null) {
int N = val.length;
writeInt(N);
@@ -2021,7 +1914,6 @@
@Nullable
public final double[] createDoubleArray() {
- assertNotRecycled();
int N = readInt();
ensureWithinMemoryLimit(SIZE_DOUBLE, N);
// >>3 because stored doubles are 8 bytes
@@ -2037,7 +1929,6 @@
}
public final void readDoubleArray(@NonNull double[] val) {
- assertNotRecycled();
int N = readInt();
if (N == val.length) {
for (int i=0; i<N; i++) {
@@ -2049,24 +1940,20 @@
}
public final void writeStringArray(@Nullable String[] val) {
- assertNotRecycled();
writeString16Array(val);
}
@Nullable
public final String[] createStringArray() {
- assertNotRecycled();
return createString16Array();
}
public final void readStringArray(@NonNull String[] val) {
- assertNotRecycled();
readString16Array(val);
}
/** {@hide} */
public final void writeString8Array(@Nullable String[] val) {
- assertNotRecycled();
if (val != null) {
int N = val.length;
writeInt(N);
@@ -2081,7 +1968,6 @@
/** {@hide} */
@Nullable
public final String[] createString8Array() {
- assertNotRecycled();
int N = readInt();
ensureWithinMemoryLimit(SIZE_COMPLEX_TYPE, N);
if (N >= 0) {
@@ -2097,7 +1983,6 @@
/** {@hide} */
public final void readString8Array(@NonNull String[] val) {
- assertNotRecycled();
int N = readInt();
if (N == val.length) {
for (int i=0; i<N; i++) {
@@ -2110,7 +1995,6 @@
/** {@hide} */
public final void writeString16Array(@Nullable String[] val) {
- assertNotRecycled();
if (val != null) {
int N = val.length;
writeInt(N);
@@ -2125,7 +2009,6 @@
/** {@hide} */
@Nullable
public final String[] createString16Array() {
- assertNotRecycled();
int N = readInt();
ensureWithinMemoryLimit(SIZE_COMPLEX_TYPE, N);
if (N >= 0) {
@@ -2141,7 +2024,6 @@
/** {@hide} */
public final void readString16Array(@NonNull String[] val) {
- assertNotRecycled();
int N = readInt();
if (N == val.length) {
for (int i=0; i<N; i++) {
@@ -2153,7 +2035,6 @@
}
public final void writeBinderArray(@Nullable IBinder[] val) {
- assertNotRecycled();
if (val != null) {
int N = val.length;
writeInt(N);
@@ -2178,7 +2059,6 @@
*/
public final <T extends IInterface> void writeInterfaceArray(
@SuppressLint("ArrayReturn") @Nullable T[] val) {
- assertNotRecycled();
if (val != null) {
int N = val.length;
writeInt(N);
@@ -2194,7 +2074,6 @@
* @hide
*/
public final void writeCharSequenceArray(@Nullable CharSequence[] val) {
- assertNotRecycled();
if (val != null) {
int N = val.length;
writeInt(N);
@@ -2210,7 +2089,6 @@
* @hide
*/
public final void writeCharSequenceList(@Nullable ArrayList<CharSequence> val) {
- assertNotRecycled();
if (val != null) {
int N = val.size();
writeInt(N);
@@ -2224,7 +2102,6 @@
@Nullable
public final IBinder[] createBinderArray() {
- assertNotRecycled();
int N = readInt();
ensureWithinMemoryLimit(SIZE_COMPLEX_TYPE, N);
if (N >= 0) {
@@ -2239,7 +2116,6 @@
}
public final void readBinderArray(@NonNull IBinder[] val) {
- assertNotRecycled();
int N = readInt();
if (N == val.length) {
for (int i=0; i<N; i++) {
@@ -2261,7 +2137,6 @@
@Nullable
public final <T extends IInterface> T[] createInterfaceArray(
@NonNull IntFunction<T[]> newArray, @NonNull Function<IBinder, T> asInterface) {
- assertNotRecycled();
int N = readInt();
ensureWithinMemoryLimit(SIZE_COMPLEX_TYPE, N);
if (N >= 0) {
@@ -2286,7 +2161,6 @@
public final <T extends IInterface> void readInterfaceArray(
@SuppressLint("ArrayReturn") @NonNull T[] val,
@NonNull Function<IBinder, T> asInterface) {
- assertNotRecycled();
int N = readInt();
if (N == val.length) {
for (int i=0; i<N; i++) {
@@ -2312,7 +2186,6 @@
* @see Parcelable
*/
public final <T extends Parcelable> void writeTypedList(@Nullable List<T> val) {
- assertNotRecycled();
writeTypedList(val, 0);
}
@@ -2332,7 +2205,6 @@
*/
public final <T extends Parcelable> void writeTypedSparseArray(@Nullable SparseArray<T> val,
int parcelableFlags) {
- assertNotRecycled();
if (val == null) {
writeInt(-1);
return;
@@ -2362,7 +2234,6 @@
* @see Parcelable
*/
public <T extends Parcelable> void writeTypedList(@Nullable List<T> val, int parcelableFlags) {
- assertNotRecycled();
if (val == null) {
writeInt(-1);
return;
@@ -2388,7 +2259,6 @@
* @see #readStringList
*/
public final void writeStringList(@Nullable List<String> val) {
- assertNotRecycled();
if (val == null) {
writeInt(-1);
return;
@@ -2414,7 +2284,6 @@
* @see #readBinderList
*/
public final void writeBinderList(@Nullable List<IBinder> val) {
- assertNotRecycled();
if (val == null) {
writeInt(-1);
return;
@@ -2437,7 +2306,6 @@
* @see #readInterfaceList
*/
public final <T extends IInterface> void writeInterfaceList(@Nullable List<T> val) {
- assertNotRecycled();
if (val == null) {
writeInt(-1);
return;
@@ -2459,7 +2327,6 @@
* @see #readParcelableList(List, ClassLoader)
*/
public final <T extends Parcelable> void writeParcelableList(@Nullable List<T> val, int flags) {
- assertNotRecycled();
if (val == null) {
writeInt(-1);
return;
@@ -2494,7 +2361,6 @@
*/
public final <T extends Parcelable> void writeTypedArray(@Nullable T[] val,
int parcelableFlags) {
- assertNotRecycled();
if (val != null) {
int N = val.length;
writeInt(N);
@@ -2517,7 +2383,6 @@
*/
public final <T extends Parcelable> void writeTypedObject(@Nullable T val,
int parcelableFlags) {
- assertNotRecycled();
if (val != null) {
writeInt(1);
val.writeToParcel(this, parcelableFlags);
@@ -2555,7 +2420,6 @@
*/
public <T> void writeFixedArray(@Nullable T val, int parcelableFlags,
@NonNull int... dimensions) {
- assertNotRecycled();
if (val == null) {
writeInt(-1);
return;
@@ -2667,7 +2531,6 @@
* should be used).</p>
*/
public final void writeValue(@Nullable Object v) {
- assertNotRecycled();
if (v instanceof LazyValue) {
LazyValue value = (LazyValue) v;
value.writeToParcel(this);
@@ -2785,7 +2648,6 @@
* @hide
*/
public void writeValue(int type, @Nullable Object v) {
- assertNotRecycled();
switch (type) {
case VAL_NULL:
break;
@@ -2899,7 +2761,6 @@
* {@link Parcelable#writeToParcel(Parcel, int) Parcelable.writeToParcel()}.
*/
public final void writeParcelable(@Nullable Parcelable p, int parcelableFlags) {
- assertNotRecycled();
if (p == null) {
writeString(null);
return;
@@ -2915,7 +2776,6 @@
* @see #readParcelableCreator
*/
public final void writeParcelableCreator(@NonNull Parcelable p) {
- assertNotRecycled();
String name = p.getClass().getName();
writeString(name);
}
@@ -2954,7 +2814,6 @@
*/
@TestApi
public boolean allowSquashing() {
- assertNotRecycled();
boolean previous = mAllowSquashing;
mAllowSquashing = true;
return previous;
@@ -2966,7 +2825,6 @@
*/
@TestApi
public void restoreAllowSquashing(boolean previous) {
- assertNotRecycled();
mAllowSquashing = previous;
if (!mAllowSquashing) {
mWrittenSquashableParcelables = null;
@@ -3023,7 +2881,6 @@
* @hide
*/
public boolean maybeWriteSquashed(@NonNull Parcelable p) {
- assertNotRecycled();
if (!mAllowSquashing) {
// Don't squash, and don't put it in the map either.
writeInt(0);
@@ -3074,7 +2931,6 @@
@SuppressWarnings("unchecked")
@Nullable
public <T extends Parcelable> T readSquashed(SquashReadHelper<T> reader) {
- assertNotRecycled();
final int offset = readInt();
final int pos = dataPosition();
@@ -3108,7 +2964,6 @@
* using the other approaches to writing data in to a Parcel.
*/
public final void writeSerializable(@Nullable Serializable s) {
- assertNotRecycled();
if (s == null) {
writeString(null);
return;
@@ -3161,7 +3016,6 @@
*/
@RavenwoodReplace(blockedBy = AppOpsManager.class)
public final void writeException(@NonNull Exception e) {
- assertNotRecycled();
AppOpsManager.prefixParcelWithAppOpsIfNeeded(this);
int code = getExceptionCode(e);
@@ -3242,7 +3096,6 @@
/** @hide */
public void writeStackTrace(@NonNull Throwable e) {
- assertNotRecycled();
final int sizePosition = dataPosition();
writeInt(0); // Header size will be filled in later
StackTraceElement[] stackTrace = e.getStackTrace();
@@ -3268,7 +3121,6 @@
*/
@RavenwoodReplace(blockedBy = AppOpsManager.class)
public final void writeNoException() {
- assertNotRecycled();
AppOpsManager.prefixParcelWithAppOpsIfNeeded(this);
// Despite the name of this function ("write no exception"),
@@ -3312,7 +3164,6 @@
* @see #writeNoException
*/
public final void readException() {
- assertNotRecycled();
int code = readExceptionCode();
if (code != 0) {
String msg = readString();
@@ -3336,7 +3187,6 @@
@UnsupportedAppUsage
@TestApi
public final int readExceptionCode() {
- assertNotRecycled();
int code = readInt();
if (code == EX_HAS_NOTED_APPOPS_REPLY_HEADER) {
AppOpsManager.readAndLogNotedAppops(this);
@@ -3370,7 +3220,6 @@
* @param msg The exception message.
*/
public final void readException(int code, String msg) {
- assertNotRecycled();
String remoteStackTrace = null;
final int remoteStackPayloadSize = readInt();
if (remoteStackPayloadSize > 0) {
@@ -3401,7 +3250,6 @@
/** @hide */
public Exception createExceptionOrNull(int code, String msg) {
- assertNotRecycled();
switch (code) {
case EX_PARCELABLE:
if (readInt() > 0) {
@@ -3434,7 +3282,6 @@
* Read an integer value from the parcel at the current dataPosition().
*/
public final int readInt() {
- assertNotRecycled();
return nativeReadInt(mNativePtr);
}
@@ -3442,7 +3289,6 @@
* Read a long integer value from the parcel at the current dataPosition().
*/
public final long readLong() {
- assertNotRecycled();
return nativeReadLong(mNativePtr);
}
@@ -3451,7 +3297,6 @@
* dataPosition().
*/
public final float readFloat() {
- assertNotRecycled();
return nativeReadFloat(mNativePtr);
}
@@ -3460,7 +3305,6 @@
* current dataPosition().
*/
public final double readDouble() {
- assertNotRecycled();
return nativeReadDouble(mNativePtr);
}
@@ -3469,19 +3313,16 @@
*/
@Nullable
public final String readString() {
- assertNotRecycled();
return readString16();
}
/** {@hide} */
public final @Nullable String readString8() {
- assertNotRecycled();
return mReadWriteHelper.readString8(this);
}
/** {@hide} */
public final @Nullable String readString16() {
- assertNotRecycled();
return mReadWriteHelper.readString16(this);
}
@@ -3493,19 +3334,16 @@
* @hide
*/
public @Nullable String readStringNoHelper() {
- assertNotRecycled();
return readString16NoHelper();
}
/** {@hide} */
public @Nullable String readString8NoHelper() {
- assertNotRecycled();
return nativeReadString8(mNativePtr);
}
/** {@hide} */
public @Nullable String readString16NoHelper() {
- assertNotRecycled();
return nativeReadString16(mNativePtr);
}
@@ -3513,7 +3351,6 @@
* Read a boolean value from the parcel at the current dataPosition().
*/
public final boolean readBoolean() {
- assertNotRecycled();
return readInt() != 0;
}
@@ -3524,7 +3361,6 @@
@UnsupportedAppUsage
@Nullable
public final CharSequence readCharSequence() {
- assertNotRecycled();
return TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(this);
}
@@ -3532,7 +3368,6 @@
* Read an object from the parcel at the current dataPosition().
*/
public final IBinder readStrongBinder() {
- assertNotRecycled();
final IBinder result = nativeReadStrongBinder(mNativePtr);
// If it's a reply from a method with @PropagateAllowBlocking, then inherit allow-blocking
@@ -3548,7 +3383,6 @@
* Read a FileDescriptor from the parcel at the current dataPosition().
*/
public final ParcelFileDescriptor readFileDescriptor() {
- assertNotRecycled();
FileDescriptor fd = nativeReadFileDescriptor(mNativePtr);
return fd != null ? new ParcelFileDescriptor(fd) : null;
}
@@ -3556,7 +3390,6 @@
/** {@hide} */
@UnsupportedAppUsage
public final FileDescriptor readRawFileDescriptor() {
- assertNotRecycled();
return nativeReadFileDescriptor(mNativePtr);
}
@@ -3567,7 +3400,6 @@
**/
@Nullable
public final FileDescriptor[] createRawFileDescriptorArray() {
- assertNotRecycled();
int N = readInt();
if (N < 0) {
return null;
@@ -3587,7 +3419,6 @@
* @return the FileDescriptor array, or null if the array is null.
**/
public final void readRawFileDescriptorArray(FileDescriptor[] val) {
- assertNotRecycled();
int N = readInt();
if (N == val.length) {
for (int i=0; i<N; i++) {
@@ -3602,7 +3433,6 @@
* Read a byte value from the parcel at the current dataPosition().
*/
public final byte readByte() {
- assertNotRecycled();
return (byte)(readInt() & 0xff);
}
@@ -3617,7 +3447,6 @@
*/
@Deprecated
public final void readMap(@NonNull Map outVal, @Nullable ClassLoader loader) {
- assertNotRecycled();
readMapInternal(outVal, loader, /* clazzKey */ null, /* clazzValue */ null);
}
@@ -3631,7 +3460,6 @@
public <K, V> void readMap(@NonNull Map<? super K, ? super V> outVal,
@Nullable ClassLoader loader, @NonNull Class<K> clazzKey,
@NonNull Class<V> clazzValue) {
- assertNotRecycled();
Objects.requireNonNull(clazzKey);
Objects.requireNonNull(clazzValue);
readMapInternal(outVal, loader, clazzKey, clazzValue);
@@ -3650,7 +3478,6 @@
*/
@Deprecated
public final void readList(@NonNull List outVal, @Nullable ClassLoader loader) {
- assertNotRecycled();
int N = readInt();
readListInternal(outVal, N, loader, /* clazz */ null);
}
@@ -3672,7 +3499,6 @@
*/
public <T> void readList(@NonNull List<? super T> outVal,
@Nullable ClassLoader loader, @NonNull Class<T> clazz) {
- assertNotRecycled();
Objects.requireNonNull(clazz);
int n = readInt();
readListInternal(outVal, n, loader, clazz);
@@ -3692,7 +3518,6 @@
@Deprecated
@Nullable
public HashMap readHashMap(@Nullable ClassLoader loader) {
- assertNotRecycled();
return readHashMapInternal(loader, /* clazzKey */ null, /* clazzValue */ null);
}
@@ -3707,7 +3532,6 @@
@Nullable
public <K, V> HashMap<K, V> readHashMap(@Nullable ClassLoader loader,
@NonNull Class<? extends K> clazzKey, @NonNull Class<? extends V> clazzValue) {
- assertNotRecycled();
Objects.requireNonNull(clazzKey);
Objects.requireNonNull(clazzValue);
return readHashMapInternal(loader, clazzKey, clazzValue);
@@ -3720,7 +3544,6 @@
*/
@Nullable
public final Bundle readBundle() {
- assertNotRecycled();
return readBundle(null);
}
@@ -3732,7 +3555,6 @@
*/
@Nullable
public final Bundle readBundle(@Nullable ClassLoader loader) {
- assertNotRecycled();
int length = readInt();
if (length < 0) {
if (Bundle.DEBUG) Log.d(TAG, "null bundle: length=" + length);
@@ -3753,7 +3575,6 @@
*/
@Nullable
public final PersistableBundle readPersistableBundle() {
- assertNotRecycled();
return readPersistableBundle(null);
}
@@ -3765,7 +3586,6 @@
*/
@Nullable
public final PersistableBundle readPersistableBundle(@Nullable ClassLoader loader) {
- assertNotRecycled();
int length = readInt();
if (length < 0) {
if (Bundle.DEBUG) Log.d(TAG, "null bundle: length=" + length);
@@ -3784,7 +3604,6 @@
*/
@NonNull
public final Size readSize() {
- assertNotRecycled();
final int width = readInt();
final int height = readInt();
return new Size(width, height);
@@ -3795,7 +3614,6 @@
*/
@NonNull
public final SizeF readSizeF() {
- assertNotRecycled();
final float width = readFloat();
final float height = readFloat();
return new SizeF(width, height);
@@ -3806,7 +3624,6 @@
*/
@Nullable
public final byte[] createByteArray() {
- assertNotRecycled();
return nativeCreateByteArray(mNativePtr);
}
@@ -3815,7 +3632,6 @@
* given byte array.
*/
public final void readByteArray(@NonNull byte[] val) {
- assertNotRecycled();
boolean valid = nativeReadByteArray(mNativePtr, val, (val != null) ? val.length : 0);
if (!valid) {
throw new RuntimeException("bad array lengths");
@@ -3828,7 +3644,6 @@
*/
@Nullable
public final byte[] readBlob() {
- assertNotRecycled();
return nativeReadBlob(mNativePtr);
}
@@ -3839,7 +3654,6 @@
@UnsupportedAppUsage
@Nullable
public final String[] readStringArray() {
- assertNotRecycled();
return createString16Array();
}
@@ -3849,7 +3663,6 @@
*/
@Nullable
public final CharSequence[] readCharSequenceArray() {
- assertNotRecycled();
CharSequence[] array = null;
int length = readInt();
@@ -3872,7 +3685,6 @@
*/
@Nullable
public final ArrayList<CharSequence> readCharSequenceList() {
- assertNotRecycled();
ArrayList<CharSequence> array = null;
int length = readInt();
@@ -3902,7 +3714,6 @@
@Deprecated
@Nullable
public ArrayList readArrayList(@Nullable ClassLoader loader) {
- assertNotRecycled();
return readArrayListInternal(loader, /* clazz */ null);
}
@@ -3925,7 +3736,6 @@
@Nullable
public <T> ArrayList<T> readArrayList(@Nullable ClassLoader loader,
@NonNull Class<? extends T> clazz) {
- assertNotRecycled();
Objects.requireNonNull(clazz);
return readArrayListInternal(loader, clazz);
}
@@ -3945,7 +3755,6 @@
@Deprecated
@Nullable
public Object[] readArray(@Nullable ClassLoader loader) {
- assertNotRecycled();
return readArrayInternal(loader, /* clazz */ null);
}
@@ -3967,7 +3776,6 @@
@SuppressLint({"ArrayReturn", "NullableCollection"})
@Nullable
public <T> T[] readArray(@Nullable ClassLoader loader, @NonNull Class<T> clazz) {
- assertNotRecycled();
Objects.requireNonNull(clazz);
return readArrayInternal(loader, clazz);
}
@@ -3987,7 +3795,6 @@
@Deprecated
@Nullable
public <T> SparseArray<T> readSparseArray(@Nullable ClassLoader loader) {
- assertNotRecycled();
return readSparseArrayInternal(loader, /* clazz */ null);
}
@@ -4009,7 +3816,6 @@
@Nullable
public <T> SparseArray<T> readSparseArray(@Nullable ClassLoader loader,
@NonNull Class<? extends T> clazz) {
- assertNotRecycled();
Objects.requireNonNull(clazz);
return readSparseArrayInternal(loader, clazz);
}
@@ -4021,7 +3827,6 @@
*/
@Nullable
public final SparseBooleanArray readSparseBooleanArray() {
- assertNotRecycled();
int N = readInt();
if (N < 0) {
return null;
@@ -4038,7 +3843,6 @@
*/
@Nullable
public final SparseIntArray readSparseIntArray() {
- assertNotRecycled();
int N = readInt();
if (N < 0) {
return null;
@@ -4063,7 +3867,6 @@
*/
@Nullable
public final <T> ArrayList<T> createTypedArrayList(@NonNull Parcelable.Creator<T> c) {
- assertNotRecycled();
int N = readInt();
if (N < 0) {
return null;
@@ -4087,7 +3890,6 @@
* @see #writeTypedList
*/
public final <T> void readTypedList(@NonNull List<T> list, @NonNull Parcelable.Creator<T> c) {
- assertNotRecycled();
int M = list.size();
int N = readInt();
int i = 0;
@@ -4117,7 +3919,6 @@
*/
public final @Nullable <T extends Parcelable> SparseArray<T> createTypedSparseArray(
@NonNull Parcelable.Creator<T> creator) {
- assertNotRecycled();
final int count = readInt();
if (count < 0) {
return null;
@@ -4147,7 +3948,6 @@
*/
public final @Nullable <T extends Parcelable> ArrayMap<String, T> createTypedArrayMap(
@NonNull Parcelable.Creator<T> creator) {
- assertNotRecycled();
final int count = readInt();
if (count < 0) {
return null;
@@ -4175,7 +3975,6 @@
*/
@Nullable
public final ArrayList<String> createStringArrayList() {
- assertNotRecycled();
int N = readInt();
if (N < 0) {
return null;
@@ -4202,7 +4001,6 @@
*/
@Nullable
public final ArrayList<IBinder> createBinderArrayList() {
- assertNotRecycled();
int N = readInt();
if (N < 0) {
return null;
@@ -4230,7 +4028,6 @@
@Nullable
public final <T extends IInterface> ArrayList<T> createInterfaceArrayList(
@NonNull Function<IBinder, T> asInterface) {
- assertNotRecycled();
int N = readInt();
if (N < 0) {
return null;
@@ -4251,7 +4048,6 @@
* @see #writeStringList
*/
public final void readStringList(@NonNull List<String> list) {
- assertNotRecycled();
int M = list.size();
int N = readInt();
int i = 0;
@@ -4273,7 +4069,6 @@
* @see #writeBinderList
*/
public final void readBinderList(@NonNull List<IBinder> list) {
- assertNotRecycled();
int M = list.size();
int N = readInt();
int i = 0;
@@ -4296,7 +4091,6 @@
*/
public final <T extends IInterface> void readInterfaceList(@NonNull List<T> list,
@NonNull Function<IBinder, T> asInterface) {
- assertNotRecycled();
int M = list.size();
int N = readInt();
int i = 0;
@@ -4328,7 +4122,6 @@
@NonNull
public final <T extends Parcelable> List<T> readParcelableList(@NonNull List<T> list,
@Nullable ClassLoader cl) {
- assertNotRecycled();
return readParcelableListInternal(list, cl, /*clazz*/ null);
}
@@ -4350,7 +4143,6 @@
@NonNull
public <T> List<T> readParcelableList(@NonNull List<T> list,
@Nullable ClassLoader cl, @NonNull Class<? extends T> clazz) {
- assertNotRecycled();
Objects.requireNonNull(list);
Objects.requireNonNull(clazz);
return readParcelableListInternal(list, cl, clazz);
@@ -4396,7 +4188,6 @@
*/
@Nullable
public final <T> T[] createTypedArray(@NonNull Parcelable.Creator<T> c) {
- assertNotRecycled();
int N = readInt();
if (N < 0) {
return null;
@@ -4410,7 +4201,6 @@
}
public final <T> void readTypedArray(@NonNull T[] val, @NonNull Parcelable.Creator<T> c) {
- assertNotRecycled();
int N = readInt();
if (N == val.length) {
for (int i=0; i<N; i++) {
@@ -4427,7 +4217,6 @@
*/
@Deprecated
public final <T> T[] readTypedArray(Parcelable.Creator<T> c) {
- assertNotRecycled();
return createTypedArray(c);
}
@@ -4444,7 +4233,6 @@
*/
@Nullable
public final <T> T readTypedObject(@NonNull Parcelable.Creator<T> c) {
- assertNotRecycled();
if (readInt() != 0) {
return c.createFromParcel(this);
} else {
@@ -4471,7 +4259,6 @@
* @see #readTypedArray
*/
public <T> void readFixedArray(@NonNull T val) {
- assertNotRecycled();
Class<?> componentType = val.getClass().getComponentType();
if (componentType == boolean.class) {
readBooleanArray((boolean[]) val);
@@ -4512,7 +4299,6 @@
*/
public <T, S extends IInterface> void readFixedArray(@NonNull T val,
@NonNull Function<IBinder, S> asInterface) {
- assertNotRecycled();
Class<?> componentType = val.getClass().getComponentType();
if (IInterface.class.isAssignableFrom(componentType)) {
readInterfaceArray((S[]) val, asInterface);
@@ -4539,7 +4325,6 @@
*/
public <T, S extends Parcelable> void readFixedArray(@NonNull T val,
@NonNull Parcelable.Creator<S> c) {
- assertNotRecycled();
Class<?> componentType = val.getClass().getComponentType();
if (Parcelable.class.isAssignableFrom(componentType)) {
readTypedArray((S[]) val, c);
@@ -4597,7 +4382,6 @@
*/
@Nullable
public <T> T createFixedArray(@NonNull Class<T> cls, @NonNull int... dimensions) {
- assertNotRecycled();
// Check if type matches with dimensions
// If type is one-dimensional array, delegate to other creators
// Otherwise, create an multi-dimensional array at once and then fill it with readFixedArray
@@ -4671,7 +4455,6 @@
@Nullable
public <T, S extends IInterface> T createFixedArray(@NonNull Class<T> cls,
@NonNull Function<IBinder, S> asInterface, @NonNull int... dimensions) {
- assertNotRecycled();
// Check if type matches with dimensions
// If type is one-dimensional array, delegate to other creators
// Otherwise, create an multi-dimensional array at once and then fill it with readFixedArray
@@ -4732,7 +4515,6 @@
@Nullable
public <T, S extends Parcelable> T createFixedArray(@NonNull Class<T> cls,
@NonNull Parcelable.Creator<S> c, @NonNull int... dimensions) {
- assertNotRecycled();
// Check if type matches with dimensions
// If type is one-dimensional array, delegate to other creators
// Otherwise, create an multi-dimensional array at once and then fill it with readFixedArray
@@ -4796,7 +4578,6 @@
*/
public final <T extends Parcelable> void writeParcelableArray(@Nullable T[] value,
int parcelableFlags) {
- assertNotRecycled();
if (value != null) {
int N = value.length;
writeInt(N);
@@ -4815,7 +4596,6 @@
*/
@Nullable
public final Object readValue(@Nullable ClassLoader loader) {
- assertNotRecycled();
return readValue(loader, /* clazz */ null);
}
@@ -4871,7 +4651,6 @@
*/
@Nullable
public Object readLazyValue(@Nullable ClassLoader loader) {
- assertNotRecycled();
int start = dataPosition();
int type = readInt();
if (isLengthPrefixed(type)) {
@@ -5274,7 +5053,6 @@
@Deprecated
@Nullable
public final <T extends Parcelable> T readParcelable(@Nullable ClassLoader loader) {
- assertNotRecycled();
return readParcelableInternal(loader, /* clazz */ null);
}
@@ -5294,7 +5072,6 @@
*/
@Nullable
public <T> T readParcelable(@Nullable ClassLoader loader, @NonNull Class<T> clazz) {
- assertNotRecycled();
Objects.requireNonNull(clazz);
return readParcelableInternal(loader, clazz);
}
@@ -5323,7 +5100,6 @@
@Nullable
public final <T extends Parcelable> T readCreator(@NonNull Parcelable.Creator<?> creator,
@Nullable ClassLoader loader) {
- assertNotRecycled();
if (creator instanceof Parcelable.ClassLoaderCreator<?>) {
Parcelable.ClassLoaderCreator<?> classLoaderCreator =
(Parcelable.ClassLoaderCreator<?>) creator;
@@ -5351,7 +5127,6 @@
@Deprecated
@Nullable
public final Parcelable.Creator<?> readParcelableCreator(@Nullable ClassLoader loader) {
- assertNotRecycled();
return readParcelableCreatorInternal(loader, /* clazz */ null);
}
@@ -5372,7 +5147,6 @@
@Nullable
public <T> Parcelable.Creator<T> readParcelableCreator(
@Nullable ClassLoader loader, @NonNull Class<T> clazz) {
- assertNotRecycled();
Objects.requireNonNull(clazz);
return readParcelableCreatorInternal(loader, clazz);
}
@@ -5495,7 +5269,6 @@
@Deprecated
@Nullable
public Parcelable[] readParcelableArray(@Nullable ClassLoader loader) {
- assertNotRecycled();
return readParcelableArrayInternal(loader, /* clazz */ null);
}
@@ -5516,7 +5289,6 @@
@SuppressLint({"ArrayReturn", "NullableCollection"})
@Nullable
public <T> T[] readParcelableArray(@Nullable ClassLoader loader, @NonNull Class<T> clazz) {
- assertNotRecycled();
return readParcelableArrayInternal(loader, requireNonNull(clazz));
}
@@ -5550,7 +5322,6 @@
@Deprecated
@Nullable
public Serializable readSerializable() {
- assertNotRecycled();
return readSerializableInternal(/* loader */ null, /* clazz */ null);
}
@@ -5567,7 +5338,6 @@
*/
@Nullable
public <T> T readSerializable(@Nullable ClassLoader loader, @NonNull Class<T> clazz) {
- assertNotRecycled();
Objects.requireNonNull(clazz);
return readSerializableInternal(
loader == null ? getClass().getClassLoader() : loader, clazz);
@@ -5809,7 +5579,6 @@
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public void readArrayMap(@NonNull ArrayMap<? super String, Object> outVal,
@Nullable ClassLoader loader) {
- assertNotRecycled();
final int N = readInt();
if (N < 0) {
return;
@@ -5826,7 +5595,6 @@
*/
@UnsupportedAppUsage
public @Nullable ArraySet<? extends Object> readArraySet(@Nullable ClassLoader loader) {
- assertNotRecycled();
final int size = readInt();
if (size < 0) {
return null;
@@ -5966,7 +5734,6 @@
* @hide For testing
*/
public long getOpenAshmemSize() {
- assertNotRecycled();
return nativeGetOpenAshmemSize(mNativePtr);
}
diff --git a/core/java/android/os/TestLooperManager.java b/core/java/android/os/TestLooperManager.java
index 4b16c1d..6431f3c 100644
--- a/core/java/android/os/TestLooperManager.java
+++ b/core/java/android/os/TestLooperManager.java
@@ -14,6 +14,8 @@
package android.os;
+import android.annotation.FlaggedApi;
+import android.annotation.Nullable;
import android.util.ArraySet;
import java.util.concurrent.LinkedBlockingQueue;
@@ -93,9 +95,48 @@
}
/**
- * Releases the looper to continue standard looping and processing of messages,
- * no further interactions with TestLooperManager will be allowed after
- * release() has been called.
+ * Returns the next message that should be executed by this queue, and removes it from the
+ * queue. If the queue is empty or no messages are deliverable, returns null.
+ * This method never blocks.
+ *
+ * <p>Callers should always call {@link #recycle(Message)} on the message when all interactions
+ * with it have completed.
+ */
+ @FlaggedApi(Flags.FLAG_MESSAGE_QUEUE_TESTABILITY)
+ @Nullable
+ public Message pop() {
+ checkReleased();
+ return mQueue.popForTest();
+ }
+
+ /**
+ * Returns the values of {@link Message#when} of the next message that should be executed by
+ * this queue. If the queue is empty or no messages are deliverable, returns null.
+ * This method never blocks.
+ */
+ @FlaggedApi(Flags.FLAG_MESSAGE_QUEUE_TESTABILITY)
+ @SuppressWarnings("AutoBoxing") // box the primitive long, or return null to indicate no value
+ @Nullable
+ public Long peekWhen() {
+ checkReleased();
+ return mQueue.peekWhenForTest();
+ }
+
+ /**
+ * Checks whether the Looper is currently blocked on a sync barrier.
+ *
+ * A Looper is blocked on a sync barrier if there is a Message in the Looper's
+ * queue that is ready for execution but is behind a sync barrier
+ */
+ @FlaggedApi(Flags.FLAG_MESSAGE_QUEUE_TESTABILITY)
+ public boolean isBlockedOnSyncBarrier() {
+ checkReleased();
+ return mQueue.isBlockedOnSyncBarrier();
+ }
+
+ /**
+ * Releases the looper to continue standard looping and processing of messages, no further
+ * interactions with TestLooperManager will be allowed after release() has been called.
*/
public void release() {
synchronized (sHeldLoopers) {
diff --git a/core/java/android/os/instrumentation/IDynamicInstrumentationManager.aidl b/core/java/android/os/instrumentation/IDynamicInstrumentationManager.aidl
index c45c51d..af56bfe 100644
--- a/core/java/android/os/instrumentation/IDynamicInstrumentationManager.aidl
+++ b/core/java/android/os/instrumentation/IDynamicInstrumentationManager.aidl
@@ -16,7 +16,7 @@
package android.os.instrumentation;
-import android.os.instrumentation.ExecutableMethodFileOffsets;
+import android.os.instrumentation.IOffsetCallback;
import android.os.instrumentation.MethodDescriptor;
import android.os.instrumentation.TargetProcess;
@@ -28,6 +28,7 @@
interface IDynamicInstrumentationManager {
/** Provides ART metadata about the described compiled method within the target process */
@PermissionManuallyEnforced
- @nullable ExecutableMethodFileOffsets getExecutableMethodFileOffsets(
- in TargetProcess targetProcess, in MethodDescriptor methodDescriptor);
+ void getExecutableMethodFileOffsets(
+ in TargetProcess targetProcess, in MethodDescriptor methodDescriptor,
+ in IOffsetCallback callback);
}
diff --git a/core/java/android/os/instrumentation/IOffsetCallback.aidl b/core/java/android/os/instrumentation/IOffsetCallback.aidl
new file mode 100644
index 0000000..a28c93f
--- /dev/null
+++ b/core/java/android/os/instrumentation/IOffsetCallback.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os.instrumentation;
+
+import android.os.instrumentation.ExecutableMethodFileOffsets;
+
+/**
+ * System private API for providing dynamic instrumentation offset results.
+ *
+ * {@hide}
+ */
+oneway interface IOffsetCallback {
+ void onResult(in @nullable ExecutableMethodFileOffsets offsets);
+}
diff --git a/core/java/android/os/instrumentation/MethodDescriptorParser.java b/core/java/android/os/instrumentation/MethodDescriptorParser.java
new file mode 100644
index 0000000..57fc44f
--- /dev/null
+++ b/core/java/android/os/instrumentation/MethodDescriptorParser.java
@@ -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 android.os.instrumentation;
+
+import android.annotation.NonNull;
+
+import java.lang.reflect.Method;
+
+/**
+ * A utility class for dynamic instrumentation / uprobestats.
+ *
+ * @hide
+ */
+public final class MethodDescriptorParser {
+
+ /**
+ * Parses a {@link MethodDescriptor} (in string representation) into a {@link Method}.
+ */
+ public static Method parseMethodDescriptor(ClassLoader classLoader,
+ @NonNull MethodDescriptor descriptor) {
+ try {
+ Class<?> javaClass = classLoader.loadClass(descriptor.fullyQualifiedClassName);
+ Class<?>[] parameters = new Class[descriptor.fullyQualifiedParameters.length];
+ for (int i = 0; i < descriptor.fullyQualifiedParameters.length; i++) {
+ String typeName = descriptor.fullyQualifiedParameters[i];
+ boolean isArrayType = typeName.endsWith("[]");
+ if (isArrayType) {
+ typeName = typeName.substring(0, typeName.length() - 2);
+ }
+ switch (typeName) {
+ case "boolean":
+ parameters[i] = isArrayType ? boolean.class.arrayType() : boolean.class;
+ break;
+ case "byte":
+ parameters[i] = isArrayType ? byte.class.arrayType() : byte.class;
+ break;
+ case "char":
+ parameters[i] = isArrayType ? char.class.arrayType() : char.class;
+ break;
+ case "short":
+ parameters[i] = isArrayType ? short.class.arrayType() : short.class;
+ break;
+ case "int":
+ parameters[i] = isArrayType ? int.class.arrayType() : int.class;
+ break;
+ case "long":
+ parameters[i] = isArrayType ? long.class.arrayType() : long.class;
+ break;
+ case "float":
+ parameters[i] = isArrayType ? float.class.arrayType() : float.class;
+ break;
+ case "double":
+ parameters[i] = isArrayType ? double.class.arrayType() : double.class;
+ break;
+ default:
+ parameters[i] = isArrayType ? classLoader.loadClass(typeName).arrayType()
+ : classLoader.loadClass(typeName);
+ }
+ }
+
+ return javaClass.getDeclaredMethod(descriptor.methodName, parameters);
+ } catch (ClassNotFoundException | NoSuchMethodException e) {
+ throw new IllegalArgumentException(
+ "The specified method cannot be found. Is this descriptor valid? "
+ + descriptor, e);
+ }
+ }
+}
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index d7750bd..7ad8088 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -18,6 +18,7 @@
import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.graphics.Paint.NEW_FONT_VARIATION_MANAGEMENT;
import static android.view.ContentInfo.FLAG_CONVERT_TO_PLAIN_TEXT;
import static android.view.ContentInfo.SOURCE_AUTOFILL;
import static android.view.ContentInfo.SOURCE_CLIPBOARD;
@@ -5542,7 +5543,21 @@
&& fontVariationSettings.equals(existingSettings))) {
return true;
}
- boolean effective = mTextPaint.setFontVariationSettings(fontVariationSettings);
+
+ final boolean useFontVariationStore = Flags.typefaceRedesignReadonly()
+ && CompatChanges.isChangeEnabled(NEW_FONT_VARIATION_MANAGEMENT);
+ boolean effective;
+ if (useFontVariationStore) {
+ if (mFontWeightAdjustment != 0
+ && mFontWeightAdjustment != Configuration.FONT_WEIGHT_ADJUSTMENT_UNDEFINED) {
+ mTextPaint.setFontVariationSettings(fontVariationSettings, mFontWeightAdjustment);
+ } else {
+ mTextPaint.setFontVariationSettings(fontVariationSettings);
+ }
+ effective = true;
+ } else {
+ effective = mTextPaint.setFontVariationSettings(fontVariationSettings);
+ }
if (effective && mLayout != null) {
nullLayouts();
diff --git a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
index b2eeff3..f40cfd9f 100644
--- a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
+++ b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
@@ -532,7 +532,12 @@
static const size_t kPageSize = getpagesize();
// App compat is only applicable on 16kb-page-size devices.
- return kPageSize == 0x4000;
+ if (kPageSize != 0x4000) {
+ return false;
+ }
+
+ // Explicit disabled status for app compat
+ return !android::base::GetBoolProperty("pm.16kb.app_compat.disabled", false);
}
static jint
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index bb76b9f..196da29 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -496,4 +496,8 @@
not connected state. -->
<bool name="config_satellite_allow_check_message_in_not_connected">false</bool>
<java-symbol type="bool" name="config_satellite_allow_check_message_in_not_connected" />
+
+ <!-- Whether to allow TN scanning during satellite session. -->
+ <bool name="config_satellite_allow_tn_scanning_during_satellite_session">true</bool>
+ <java-symbol type="bool" name="config_satellite_allow_tn_scanning_during_satellite_session" />
</resources>
diff --git a/core/tests/coretests/src/android/os/TestLooperManagerTest.java b/core/tests/coretests/src/android/os/TestLooperManagerTest.java
deleted file mode 100644
index 4d64a3a..0000000
--- a/core/tests/coretests/src/android/os/TestLooperManagerTest.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.os;
-
-import static org.junit.Assert.assertEquals;
-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 android.platform.test.ravenwood.RavenwoodRule;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-@RunWith(AndroidJUnit4.class)
-public class TestLooperManagerTest {
- private static final String TAG = "TestLooperManagerTest";
-
- @Rule
- public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
- .setProvideMainThread(true)
- .build();
-
- @Test
- public void testMainThread() throws Exception {
- doTest(Looper.getMainLooper());
- }
-
- @Test
- public void testCustomThread() throws Exception {
- final HandlerThread thread = new HandlerThread(TAG);
- thread.start();
- doTest(thread.getLooper());
- }
-
- private void doTest(Looper looper) throws Exception {
- final TestLooperManager tlm =
- InstrumentationRegistry.getInstrumentation().acquireLooperManager(looper);
-
- final Handler handler = new Handler(looper);
- final CountDownLatch latch = new CountDownLatch(1);
-
- assertFalse(tlm.hasMessages(handler, null, 42));
-
- handler.sendEmptyMessage(42);
- handler.post(() -> {
- latch.countDown();
- });
- assertTrue(tlm.hasMessages(handler, null, 42));
- assertFalse(latch.await(100, TimeUnit.MILLISECONDS));
-
- final Message first = tlm.next();
- assertEquals(42, first.what);
- assertNull(first.callback);
- tlm.execute(first);
- assertFalse(tlm.hasMessages(handler, null, 42));
- assertFalse(latch.await(100, TimeUnit.MILLISECONDS));
- tlm.recycle(first);
-
- final Message second = tlm.next();
- assertNotNull(second.callback);
- tlm.execute(second);
- assertFalse(tlm.hasMessages(handler, null, 42));
- assertTrue(latch.await(100, TimeUnit.MILLISECONDS));
- tlm.recycle(second);
-
- tlm.release();
- }
-}
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index 9bf4d65..2e88514 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -34,6 +34,7 @@
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.compat.annotation.UnsupportedAppUsage;
+import android.graphics.fonts.FontStyle;
import android.graphics.fonts.FontVariationAxis;
import android.graphics.text.TextRunShaper;
import android.os.Build;
@@ -2141,6 +2142,14 @@
* @see FontVariationAxis
*/
public boolean setFontVariationSettings(String fontVariationSettings) {
+ return setFontVariationSettings(fontVariationSettings, 0 /* wght adjust */);
+ }
+
+ /**
+ * Set font variation settings with weight adjustment
+ * @hide
+ */
+ public boolean setFontVariationSettings(String fontVariationSettings, int wghtAdjust) {
final boolean useFontVariationStore = Flags.typefaceRedesignReadonly()
&& CompatChanges.isChangeEnabled(NEW_FONT_VARIATION_MANAGEMENT);
if (useFontVariationStore) {
@@ -2154,8 +2163,13 @@
long builderPtr = nCreateFontVariationBuilder(axes.length);
for (int i = 0; i < axes.length; ++i) {
- nAddFontVariationToBuilder(builderPtr, axes[i].getOpenTypeTagValue(),
- axes[i].getStyleValue());
+ int tag = axes[i].getOpenTypeTagValue();
+ float value = axes[i].getStyleValue();
+ if (tag == 0x77676874 /* wght */) {
+ value = Math.clamp(value + wghtAdjust,
+ FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX);
+ }
+ nAddFontVariationToBuilder(builderPtr, tag, value);
}
nSetFontVariationOverride(mNativePaint, builderPtr);
mFontVariationSettings = fontVariationSettings;
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
index b38d00da..1d0c505 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
@@ -602,8 +602,72 @@
testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = true)
}
+ @Test
+ fun getExpandedViewContainerPadding_largeScreen_fitsMaxViewWidth() {
+ val expandedViewWidth = context.resources.getDimensionPixelSize(
+ R.dimen.bubble_expanded_view_largescreen_width
+ )
+ // set the screen size so that it is wide enough to fit the maximum width size
+ val screenWidth = expandedViewWidth * 2
+ positioner.update(
+ defaultDeviceConfig.copy(
+ windowBounds = Rect(0, 0, screenWidth, 2000),
+ isLargeScreen = true,
+ isLandscape = false
+ )
+ )
+ val paddings =
+ positioner.getExpandedViewContainerPadding(/* onLeft= */ true, /* isOverflow= */ false)
+
+ val padding = context.resources.getDimensionPixelSize(
+ R.dimen.bubble_expanded_view_largescreen_landscape_padding
+ )
+ val right = screenWidth - expandedViewWidth - padding
+ assertThat(paddings).isEqualTo(intArrayOf(padding - positioner.pointerSize, 0, right, 0))
+ }
+
+ @Test
+ fun getExpandedViewContainerPadding_largeScreen_doesNotFitMaxViewWidth() {
+ positioner.update(
+ defaultDeviceConfig.copy(
+ windowBounds = Rect(0, 0, 600, 2000),
+ isLargeScreen = true,
+ isLandscape = false
+ )
+ )
+ val paddings =
+ positioner.getExpandedViewContainerPadding(/* onLeft= */ true, /* isOverflow= */ false)
+
+ val padding = context.resources.getDimensionPixelSize(
+ R.dimen.bubble_expanded_view_largescreen_landscape_padding
+ )
+ // the screen is not wide enough to fit the maximum width size, so the view fills the screen
+ // minus left and right padding
+ assertThat(paddings).isEqualTo(intArrayOf(padding - positioner.pointerSize, 0, padding, 0))
+ }
+
+ @Test
+ fun getExpandedViewContainerPadding_smallTablet() {
+ val screenWidth = 500
+ positioner.update(
+ defaultDeviceConfig.copy(
+ windowBounds = Rect(0, 0, screenWidth, 2000),
+ isLargeScreen = true,
+ isSmallTablet = true,
+ isLandscape = false
+ )
+ )
+ val paddings =
+ positioner.getExpandedViewContainerPadding(/* onLeft= */ true, /* isOverflow= */ false)
+
+ // for small tablets, the view width is set to be 0.72 * screen width
+ val viewWidth = (screenWidth * 0.72).toInt()
+ val padding = (screenWidth - viewWidth) / 2
+ assertThat(paddings).isEqualTo(intArrayOf(padding - positioner.pointerSize, 0, padding, 0))
+ }
+
private fun testGetBubbleBarExpandedViewBounds(onLeft: Boolean, isOverflow: Boolean) {
- positioner.setShowingInBubbleBar(true)
+ positioner.isShowingInBubbleBar = true
val windowBounds = Rect(0, 0, 2000, 2600)
val insets = Insets.of(10, 20, 5, 15)
val deviceConfig =
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
index 04c17e5..a5205ee 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
@@ -162,6 +162,21 @@
}
/**
+ * Return the maximum size of the window decoration surface control view host pool, or zero if
+ * there should be no pooling.
+ */
+ public static int getWindowDecorScvhPoolSize(@NonNull Context context) {
+ if (!Flags.enableDesktopWindowingScvhCacheBugFix()) return 0;
+ final int maxTaskLimit = getMaxTaskLimit(context);
+ if (maxTaskLimit > 0) {
+ return maxTaskLimit;
+ }
+ // TODO: b/368032552 - task limit equal to 0 means unlimited. Figure out what the pool
+ // size should be in that case.
+ return 0;
+ }
+
+ /**
* Return {@code true} if the current device supports desktop mode.
*/
@VisibleForTesting
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 673d8b3..60a52a8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -214,6 +214,10 @@
}
ProtoLog.i(WM_SHELL_BACK_PREVIEW, "Navigation window gone.");
setTriggerBack(false);
+ // Trigger close transition if necessary.
+ if (Flags.migratePredictiveBackTransition()) {
+ mBackTransitionHandler.onAnimationFinished();
+ }
resetTouchTracker();
// Don't wait for animation start
mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index 0fd4206..de85d9a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -163,8 +163,11 @@
mExpandedViewLargeScreenWidth = (int) (bounds.width()
* EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT);
} else {
- mExpandedViewLargeScreenWidth =
- res.getDimensionPixelSize(R.dimen.bubble_expanded_view_largescreen_width);
+ int expandedViewLargeScreenSpacing = res.getDimensionPixelSize(
+ R.dimen.bubble_expanded_view_largescreen_landscape_padding);
+ mExpandedViewLargeScreenWidth = Math.min(
+ res.getDimensionPixelSize(R.dimen.bubble_expanded_view_largescreen_width),
+ bounds.width() - expandedViewLargeScreenSpacing * 2);
}
if (mDeviceConfig.isLargeScreen()) {
if (mDeviceConfig.isSmallTablet()) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/BoostExecutor.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/BoostExecutor.kt
new file mode 100644
index 0000000..498d0e4
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/BoostExecutor.kt
@@ -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.wm.shell.common
+
+import android.os.Looper
+import java.util.concurrent.Executor
+
+/** Executor implementation which can be boosted temporarily to a different thread priority. */
+interface BoostExecutor : Executor {
+ /**
+ * Requests that the executor is boosted until {@link #resetBoost()} is called.
+ */
+ fun setBoost() {}
+
+ /**
+ * Requests that the executor is not boosted (only resets if there are no other boost requests
+ * in progress).
+ */
+ fun resetBoost() {}
+
+ /**
+ * Returns whether the executor is boosted.
+ */
+ fun isBoosted() : Boolean {
+ return false
+ }
+
+ /**
+ * Returns the looper for this executor.
+ */
+ fun getLooper() : Looper? {
+ return Looper.myLooper()
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java
index 736d954..803f16c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java
@@ -16,15 +16,50 @@
package com.android.wm.shell.common;
+import static android.os.Process.THREAD_PRIORITY_DEFAULT;
+import static android.os.Process.setThreadPriority;
+
import android.annotation.NonNull;
import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.util.function.BiConsumer;
/** Executor implementation which is backed by a Handler. */
public class HandlerExecutor implements ShellExecutor {
+ @NonNull
private final Handler mHandler;
+ // See android.os.Process#THREAD_PRIORITY_*
+ private final int mDefaultThreadPriority;
+ private final int mBoostedThreadPriority;
+ // Number of current requests to boost thread priority
+ private int mBoostCount;
+ private final Object mBoostLock = new Object();
+ // Default function for setting thread priority (tid, priority)
+ private BiConsumer<Integer, Integer> mSetThreadPriorityFn =
+ HandlerExecutor::setThreadPriorityInternal;
public HandlerExecutor(@NonNull Handler handler) {
+ this(handler, THREAD_PRIORITY_DEFAULT, THREAD_PRIORITY_DEFAULT);
+ }
+
+ /**
+ * Used only if this executor can be boosted, if so, it can be boosted to the given
+ * {@param boostPriority}.
+ */
+ public HandlerExecutor(@NonNull Handler handler, int defaultThreadPriority,
+ int boostedThreadPriority) {
mHandler = handler;
+ mDefaultThreadPriority = defaultThreadPriority;
+ mBoostedThreadPriority = boostedThreadPriority;
+ }
+
+ @VisibleForTesting
+ void replaceSetThreadPriorityFn(BiConsumer<Integer, Integer> setThreadPriorityFn) {
+ mSetThreadPriorityFn = setThreadPriorityFn;
}
@Override
@@ -56,9 +91,54 @@
}
@Override
+ public void setBoost() {
+ synchronized (mBoostLock) {
+ if (mDefaultThreadPriority == mBoostedThreadPriority) {
+ // Nothing to boost
+ return;
+ }
+ if (mBoostCount == 0) {
+ mSetThreadPriorityFn.accept(
+ ((HandlerThread) mHandler.getLooper().getThread()).getThreadId(),
+ mBoostedThreadPriority);
+ }
+ mBoostCount++;
+ }
+ }
+
+ @Override
+ public void resetBoost() {
+ synchronized (mBoostLock) {
+ mBoostCount--;
+ if (mBoostCount == 0) {
+ mSetThreadPriorityFn.accept(
+ ((HandlerThread) mHandler.getLooper().getThread()).getThreadId(),
+ mDefaultThreadPriority);
+ }
+ }
+ }
+
+ @Override
+ public boolean isBoosted() {
+ synchronized (mBoostLock) {
+ return mBoostCount > 0;
+ }
+ }
+
+ @Override
+ @NonNull
+ public Looper getLooper() {
+ return mHandler.getLooper();
+ }
+
+ @Override
public void assertCurrentThread() {
if (!mHandler.getLooper().isCurrentThread()) {
throw new IllegalStateException("must be called on " + mHandler);
}
}
+
+ private static void setThreadPriorityInternal(Integer tid, Integer priority) {
+ setThreadPriority(tid, priority);
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java
index 2c2961f..9e5071e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java
@@ -18,15 +18,15 @@
import java.lang.reflect.Array;
import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
/**
* Super basic Executor interface that adds support for delayed execution and removing callbacks.
- * Intended to wrap Handler while better-supporting testing.
+ * Intended to wrap Handler while better-supporting testing. Not every ShellExecutor implementation
+ * may support boosting.
*/
-public interface ShellExecutor extends Executor {
+public interface ShellExecutor extends BoostExecutor {
/**
* Executes the given runnable. If the caller is running on the same looper as this executor,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
index c5644a8..d7ddbde 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
@@ -18,6 +18,7 @@
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
import static android.os.Process.THREAD_PRIORITY_DISPLAY;
+import static android.os.Process.THREAD_PRIORITY_FOREGROUND;
import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST;
import android.content.Context;
@@ -205,13 +206,14 @@
}
/**
- * Provides a Shell background thread Executor for low priority background tasks.
+ * Provides a Shell background thread Executor for low priority background tasks. The thread
+ * may also be boosted to THREAD_PRIORITY_FOREGROUND if necessary.
*/
@WMSingleton
@Provides
@ShellBackgroundThread
public static ShellExecutor provideSharedBackgroundExecutor(
@ShellBackgroundThread Handler handler) {
- return new HandlerExecutor(handler);
+ return new HandlerExecutor(handler, THREAD_PRIORITY_BACKGROUND, THREAD_PRIORITY_FOREGROUND);
}
}
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 86e0d08..f9e3be9 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
@@ -152,6 +152,7 @@
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer;
import com.android.wm.shell.windowdecor.common.viewhost.DefaultWindowDecorViewHostSupplier;
+import com.android.wm.shell.windowdecor.common.viewhost.PooledWindowDecorViewHostSupplier;
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost;
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier;
import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationPromoController;
@@ -347,7 +348,12 @@
@WMSingleton
@Provides
static WindowDecorViewHostSupplier<WindowDecorViewHost> provideWindowDecorViewHostSupplier(
+ @NonNull Context context,
@ShellMainThread @NonNull CoroutineScope mainScope) {
+ final int poolSize = DesktopModeStatus.getWindowDecorScvhPoolSize(context);
+ if (DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context) && poolSize > 0) {
+ return new PooledWindowDecorViewHostSupplier(mainScope, poolSize);
+ }
return new DefaultWindowDecorViewHostSupplier(mainScope);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
index 7764688..50187d5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
@@ -77,6 +77,10 @@
override fun startMinimizedModeTransition(wct: WindowContainerTransaction?): IBinder =
freeformTaskTransitionHandler.startMinimizedModeTransition(wct)
+ /** Delegates starting PiP transition to [FreeformTaskTransitionHandler]. */
+ override fun startPipTransition(wct: WindowContainerTransaction?): IBinder =
+ freeformTaskTransitionHandler.startPipTransition(wct)
+
/** Starts close transition and handles or delegates desktop task close animation. */
override fun startRemoveTransition(wct: WindowContainerTransaction?): IBinder {
if (
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 1ec8684..0bc7ca9 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
@@ -50,6 +50,7 @@
import android.view.WindowManager.TRANSIT_CLOSE
import android.view.WindowManager.TRANSIT_NONE
import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_PIP
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.widget.Toast
import android.window.DesktopModeFlags
@@ -220,6 +221,7 @@
// Launch cookie used to identify a drag and drop transition to fullscreen after it has begun.
// Used to prevent handleRequest from moving the new fullscreen task to freeform.
private var dragAndDropFullscreenCookie: Binder? = null
+ private var pendingPipTransitionAndTask: Pair<IBinder, Int>? = null
init {
desktopMode = DesktopModeImpl()
@@ -361,8 +363,15 @@
}
val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)
- requireNotNull(tdaInfo) {
- "This method can only be called with the ID of a display having non-null DisplayArea."
+ // A non-organized display (e.g., non-trusted virtual displays used in CTS) doesn't have
+ // TDA.
+ if (tdaInfo == null) {
+ logW(
+ "forceEnterDesktop cannot find DisplayAreaInfo for displayId=%d. This could happen" +
+ " when the display is a non-trusted virtual display.",
+ displayId,
+ )
+ return false
}
val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
val isFreeformDisplay = tdaWindowingMode == WINDOWING_MODE_FREEFORM
@@ -557,6 +566,26 @@
}
fun minimizeTask(taskInfo: RunningTaskInfo) {
+ val wct = WindowContainerTransaction()
+
+ val isMinimizingToPip = taskInfo.pictureInPictureParams?.isAutoEnterEnabled() ?: false
+ // If task is going to PiP, start a PiP transition instead of a minimize transition
+ if (isMinimizingToPip) {
+ val requestInfo = TransitionRequestInfo(
+ TRANSIT_PIP, /* triggerTask= */ null, taskInfo, /* remoteTransition= */ null,
+ /* displayChange= */ null, /* flags= */ 0
+ )
+ val requestRes = transitions.dispatchRequest(Binder(), requestInfo, /* skip= */ null)
+ wct.merge(requestRes.second, true)
+ pendingPipTransitionAndTask =
+ freeformTaskTransitionStarter.startPipTransition(wct) to taskInfo.taskId
+ return
+ }
+
+ minimizeTaskInner(taskInfo)
+ }
+
+ private fun minimizeTaskInner(taskInfo: RunningTaskInfo) {
val taskId = taskInfo.taskId
val displayId = taskInfo.displayId
val wct = WindowContainerTransaction()
@@ -884,7 +913,10 @@
destinationBounds.height(),
displayController,
)
- toggleResizeDesktopTaskTransitionHandler.startTransition(wct)
+ toggleResizeDesktopTaskTransitionHandler.startTransition(
+ wct,
+ interaction.animationStartBounds,
+ )
}
private fun dragToMaximizeDesktopTask(
@@ -915,6 +947,7 @@
direction = ToggleTaskSizeInteraction.Direction.MAXIMIZE,
source = ToggleTaskSizeInteraction.Source.HEADER_DRAG_TO_TOP,
inputMethod = DesktopModeEventLogger.getInputMethodFromMotionEvent(motionEvent),
+ animationStartBounds = currentDragBounds,
),
)
}
@@ -1335,6 +1368,21 @@
return false
}
+ override fun onTransitionConsumed(
+ transition: IBinder,
+ aborted: Boolean,
+ finishT: Transaction?
+ ) {
+ pendingPipTransitionAndTask?.let { (pipTransition, taskId) ->
+ if (transition == pipTransition) {
+ if (aborted) {
+ shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { minimizeTaskInner(it) }
+ }
+ pendingPipTransitionAndTask = null
+ }
+ }
+ }
+
override fun handleRequest(
transition: IBinder,
request: TransitionRequestInfo,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/common/ToggleTaskSizeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/common/ToggleTaskSizeUtils.kt
index 7afd8d7..f6ebf72 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/common/ToggleTaskSizeUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/common/ToggleTaskSizeUtils.kt
@@ -15,6 +15,7 @@
*/
package com.android.wm.shell.desktopmode.common
+import android.graphics.Rect
import com.android.internal.jank.Cuj
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.InputMethod
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger
@@ -23,10 +24,13 @@
import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction.Source
/** Represents a user interaction to toggle a desktop task's size from to maximize or vice versa. */
-data class ToggleTaskSizeInteraction(
+data class ToggleTaskSizeInteraction
+@JvmOverloads
+constructor(
val direction: Direction,
val source: Source,
val inputMethod: InputMethod,
+ val animationStartBounds: Rect? = null,
) {
constructor(
isMaximized: Boolean,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md
index 9d01535..837a6dd3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md
@@ -36,7 +36,8 @@
thread)
- This is always another thread even if config_enableShellMainThread is not set true
- **Note**:
- - This thread runs with `THREAD_PRIORITY_BACKGROUND` priority
+ - This thread runs with `THREAD_PRIORITY_BACKGROUND` priority but can be requested to be boosted
+ to `THREAD_PRIORITY_FOREGROUND`
- `ShellAnimationThread` (currently only used for Transitions and Splitscreen, but potentially all
animations could be offloaded here)
- `ShellSplashScreenThread` (only for use with splashscreens)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
index 2ae9828..52b6c62 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
@@ -18,6 +18,7 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.WindowManager.TRANSIT_PIP;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -99,6 +100,12 @@
return token;
}
+ @Override
+ public IBinder startPipTransition(WindowContainerTransaction wct) {
+ final IBinder token = mTransitions.startTransition(TRANSIT_PIP, wct, null);
+ mPendingTransitionTokens.add(token);
+ return token;
+ }
@Override
public IBinder startRemoveTransition(WindowContainerTransaction wct) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java
index 5984d48..a874a5b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java
@@ -51,4 +51,13 @@
* @return the started transition
*/
IBinder startRemoveTransition(WindowContainerTransaction wct);
+
+ /**
+ * Starts PiP transition
+ *
+ * @param wct the {@link WindowContainerTransaction} that launches the PiP
+ *
+ * @return the started transition
+ */
+ IBinder startPipTransition(WindowContainerTransaction wct);
}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index 1efe2ff..dae3c21 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -55,6 +55,7 @@
import androidx.annotation.Nullable;
import com.android.internal.util.Preconditions;
+import com.android.window.flags.Flags;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
import com.android.wm.shell.common.pip.PipBoundsState;
@@ -729,6 +730,10 @@
&& getFixedRotationDelta(info, pipTaskChange) == ROTATION_90) {
adjustedSourceRectHint.offset(cutoutInsets.left, cutoutInsets.top);
}
+ if (Flags.enableDesktopWindowingPip()) {
+ adjustedSourceRectHint.offset(-pipActivityChange.getStartAbsBounds().left,
+ -pipActivityChange.getStartAbsBounds().top);
+ }
} else {
// For non-valid app provided src-rect-hint, calculate one to crop into during
// app icon overlay animation.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java
index 82c0aaf..361d7663 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java
@@ -186,6 +186,7 @@
*/
public void setObscuredTouchRect(Rect obscuredRect) {
mObscuredTouchRegion = obscuredRect != null ? new Region(obscuredRect) : null;
+ invalidate();
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplier.kt
new file mode 100644
index 0000000..adb0ba6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplier.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.common.viewhost
+
+import android.content.Context
+import android.os.Trace
+import android.util.Pools
+import android.view.Display
+import android.view.SurfaceControl
+import com.android.wm.shell.shared.annotations.ShellMainThread
+import kotlinx.coroutines.CoroutineScope
+
+/**
+ * A [WindowDecorViewHostSupplier] backed by a pool to allow recycling view hosts which may be
+ * expensive to recreate for each new or updated window decoration.
+ *
+ * Callers can obtain a [WindowDecorViewHost] using [acquire], which will return a pooled
+ * object if available, or create a new instance and return it if needed. When finished using a
+ * [WindowDecorViewHost], it must be released using [release] to allow it to be sent back
+ * into the pool and reused later on.
+ */
+class PooledWindowDecorViewHostSupplier(
+ @ShellMainThread private val mainScope: CoroutineScope,
+ maxPoolSize: Int,
+) : WindowDecorViewHostSupplier<WindowDecorViewHost> {
+
+ private val pool: Pools.Pool<WindowDecorViewHost> = Pools.SynchronizedPool(maxPoolSize)
+ private var nextDecorViewHostId = 0
+
+ override fun acquire(context: Context, display: Display): WindowDecorViewHost {
+ val pooledViewHost = pool.acquire()
+ if (pooledViewHost != null) {
+ return pooledViewHost
+ }
+ Trace.beginSection("PooledWindowDecorViewHostSupplier#acquire-newInstance")
+ val newDecorViewHost = newInstance(context, display)
+ Trace.endSection()
+ return newDecorViewHost
+ }
+
+ override fun release(viewHost: WindowDecorViewHost, t: SurfaceControl.Transaction) {
+ val pooled = pool.release(viewHost)
+ if (!pooled) {
+ viewHost.release(t)
+ }
+ }
+
+ private fun newInstance(context: Context, display: Display): ReusableWindowDecorViewHost {
+ // Use a reusable window decor view host, as it allows swapping the entire view hierarchy.
+ return ReusableWindowDecorViewHost(
+ context = context,
+ mainScope = mainScope,
+ display = display,
+ id = nextDecorViewHostId++
+ )
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt
new file mode 100644
index 0000000..bf0b118
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.common.viewhost
+
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Region
+import android.view.Display
+import android.view.SurfaceControl
+import android.view.View
+import android.view.WindowManager
+import android.widget.FrameLayout
+import androidx.tracing.Trace
+import com.android.internal.annotations.VisibleForTesting
+import com.android.wm.shell.shared.annotations.ShellMainThread
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+/**
+ * An implementation of [WindowDecorViewHost] that supports:
+ * 1) Replacing the root [View], meaning [WindowDecorViewHost.updateView] maybe be called with
+ * different [View] instances. This is useful when reusing [WindowDecorViewHost]s instances for
+ * vastly different view hierarchies, such as Desktop Windowing's App Handles and App Headers.
+ */
+class ReusableWindowDecorViewHost(
+ private val context: Context,
+ @ShellMainThread private val mainScope: CoroutineScope,
+ display: Display,
+ val id: Int,
+ @VisibleForTesting
+ val viewHostAdapter: SurfaceControlViewHostAdapter =
+ SurfaceControlViewHostAdapter(context, display),
+) : WindowDecorViewHost {
+ @VisibleForTesting val rootView = FrameLayout(context)
+
+ private var currentUpdateJob: Job? = null
+
+ override val surfaceControl: SurfaceControl
+ get() = viewHostAdapter.rootSurface
+
+ override fun updateView(
+ view: View,
+ attrs: WindowManager.LayoutParams,
+ configuration: Configuration,
+ touchableRegion: Region?,
+ onDrawTransaction: SurfaceControl.Transaction?,
+ ) {
+ Trace.beginSection("ReusableWindowDecorViewHost#updateView")
+ clearCurrentUpdateJob()
+ updateViewHost(view, attrs, configuration, touchableRegion, onDrawTransaction)
+ Trace.endSection()
+ }
+
+ override fun updateViewAsync(
+ view: View,
+ attrs: WindowManager.LayoutParams,
+ configuration: Configuration,
+ touchableRegion: Region?,
+ ) {
+ Trace.beginSection("ReusableWindowDecorViewHost#updateViewAsync")
+ clearCurrentUpdateJob()
+ currentUpdateJob =
+ mainScope.launch {
+ updateViewHost(
+ view,
+ attrs,
+ configuration,
+ touchableRegion,
+ onDrawTransaction = null,
+ )
+ }
+ Trace.endSection()
+ }
+
+ override fun release(t: SurfaceControl.Transaction) {
+ clearCurrentUpdateJob()
+ viewHostAdapter.release(t)
+ }
+
+ private fun updateViewHost(
+ view: View,
+ attrs: WindowManager.LayoutParams,
+ configuration: Configuration,
+ touchableRegion: Region?,
+ onDrawTransaction: SurfaceControl.Transaction?,
+ ) {
+ Trace.beginSection("ReusableWindowDecorViewHost#updateViewHost")
+ viewHostAdapter.prepareViewHost(configuration, touchableRegion)
+ onDrawTransaction?.let { viewHostAdapter.applyTransactionOnDraw(it) }
+ rootView.removeAllViews()
+ rootView.addView(view)
+ viewHostAdapter.updateView(rootView, attrs)
+ Trace.endSection()
+ }
+
+ private fun clearCurrentUpdateJob() {
+ currentUpdateJob?.cancel()
+ currentUpdateJob = null
+ }
+
+ companion object {
+ private const val TAG = "ReusableWindowDecorViewHost"
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/HandlerExecutorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/HandlerExecutorTest.kt
new file mode 100644
index 0000000..799b48c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/HandlerExecutorTest.kt
@@ -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 com.android.wm.shell.common
+
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.wm.shell.ShellTestCase
+import com.google.common.truth.Truth.assertThat
+import java.util.function.BiConsumer
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.MockitoSession
+import org.mockito.kotlin.whenever
+
+/**
+ * Tests for HandlerExecutor.
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:HandlerExecutorTest
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class HandlerExecutorTest : ShellTestCase() {
+
+ class TestSetThreadPriorityFn : BiConsumer<Int, Int> {
+ var lastSetPriority = UNSET_THREAD_PRIORITY
+ private set
+ var callCount = 0
+ private set
+
+ override fun accept(tid: Int, priority: Int) {
+ lastSetPriority = priority
+ callCount++
+ }
+
+ fun reset() {
+ lastSetPriority = UNSET_THREAD_PRIORITY
+ callCount = 0
+ }
+ }
+
+ val testSetPriorityFn = TestSetThreadPriorityFn()
+
+ @Test
+ fun defaultExecutorDisallowBoost() {
+ val executor = createTestHandlerExecutor()
+
+ executor.setBoost()
+
+ assertThat(executor.isBoosted()).isFalse()
+ }
+
+ @Test
+ fun boostExecutor_resetWhenNotSet_expectNoOp() {
+ val executor = createTestHandlerExecutor(DEFAULT_THREAD_PRIORITY, BOOSTED_THREAD_PRIORITY)
+ val mockSession: MockitoSession = ExtendedMockito.mockitoSession()
+ .mockStatic(android.os.Process::class.java)
+ .startMocking()
+
+ try {
+ // Try to reset and ensure we never try to set the thread priority
+ executor.resetBoost()
+
+ assertThat(testSetPriorityFn.callCount).isEqualTo(0)
+ assertThat(executor.isBoosted()).isFalse()
+ } finally {
+ mockSession.finishMocking()
+ }
+ }
+
+ @Test
+ fun boostExecutor_setResetBoost_expectThreadPriorityUpdated() {
+ val executor = createTestHandlerExecutor(DEFAULT_THREAD_PRIORITY, BOOSTED_THREAD_PRIORITY)
+ val mockSession: MockitoSession = ExtendedMockito.mockitoSession()
+ .mockStatic(android.os.Process::class.java)
+ .startMocking()
+
+ try {
+ // Boost and ensure the boosted thread priority is requested
+ executor.setBoost()
+
+ assertThat(testSetPriorityFn.lastSetPriority).isEqualTo(BOOSTED_THREAD_PRIORITY)
+ assertThat(testSetPriorityFn.callCount).isEqualTo(1)
+ assertThat(executor.isBoosted()).isTrue()
+
+ // Reset and ensure the default thread priority is requested
+ executor.resetBoost()
+
+ assertThat(testSetPriorityFn.lastSetPriority).isEqualTo(DEFAULT_THREAD_PRIORITY)
+ assertThat(testSetPriorityFn.callCount).isEqualTo(2)
+ assertThat(executor.isBoosted()).isFalse()
+ } finally {
+ mockSession.finishMocking()
+ }
+ }
+
+ @Test
+ fun boostExecutor_overlappingBoost_expectResetOnlyWhenNotOverlapping() {
+ val executor = createTestHandlerExecutor(DEFAULT_THREAD_PRIORITY, BOOSTED_THREAD_PRIORITY)
+ val mockSession: MockitoSession = ExtendedMockito.mockitoSession()
+ .mockStatic(android.os.Process::class.java)
+ .startMocking()
+
+ try {
+ // Set and ensure we only update the thread priority once
+ executor.setBoost()
+ executor.setBoost()
+
+ assertThat(testSetPriorityFn.lastSetPriority).isEqualTo(BOOSTED_THREAD_PRIORITY)
+ assertThat(testSetPriorityFn.callCount).isEqualTo(1)
+ assertThat(executor.isBoosted()).isTrue()
+
+ // Reset and ensure we are still boosted and the thread priority doesn't change
+ executor.resetBoost()
+
+ assertThat(testSetPriorityFn.lastSetPriority).isEqualTo(BOOSTED_THREAD_PRIORITY)
+ assertThat(testSetPriorityFn.callCount).isEqualTo(1)
+ assertThat(executor.isBoosted()).isTrue()
+
+ // Reset again and ensure we update the thread priority accordingly
+ executor.resetBoost()
+
+ assertThat(testSetPriorityFn.lastSetPriority).isEqualTo(DEFAULT_THREAD_PRIORITY)
+ assertThat(testSetPriorityFn.callCount).isEqualTo(2)
+ assertThat(executor.isBoosted()).isFalse()
+ } finally {
+ mockSession.finishMocking()
+ }
+ }
+
+ /**
+ * Creates a test handler executor backed by a mocked handler thread.
+ */
+ private fun createTestHandlerExecutor(
+ defaultThreadPriority: Int = DEFAULT_THREAD_PRIORITY,
+ boostedThreadPriority: Int = DEFAULT_THREAD_PRIORITY
+ ) : HandlerExecutor {
+ val handler = mock(Handler::class.java)
+ val looper = mock(Looper::class.java)
+ val thread = mock(HandlerThread::class.java)
+ whenever(handler.looper).thenReturn(looper)
+ whenever(looper.thread).thenReturn(thread)
+ whenever(thread.threadId).thenReturn(1234)
+ val executor = HandlerExecutor(handler, defaultThreadPriority, boostedThreadPriority)
+ executor.replaceSetThreadPriorityFn(testSetPriorityFn)
+ return executor
+ }
+
+ companion object {
+ private const val UNSET_THREAD_PRIORITY = 0
+ private const val DEFAULT_THREAD_PRIORITY = 1
+ private const val BOOSTED_THREAD_PRIORITY = 1000
+ }
+}
\ No newline at end of file
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 3bee588..7c9494c 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
@@ -21,6 +21,7 @@
import android.app.ActivityOptions
import android.app.KeyguardManager
import android.app.PendingIntent
+import android.app.PictureInPictureParams
import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
@@ -1724,6 +1725,34 @@
}
@Test
+ fun onDesktopWindowMinimize_pipTask_autoEnterEnabled_startPipTransition() {
+ val task = setUpPipTask(autoEnterEnabled = true)
+ val handler = mock(TransitionHandler::class.java)
+ whenever(freeformTaskTransitionStarter.startPipTransition(any()))
+ .thenReturn(Binder())
+ whenever(transitions.dispatchRequest(any(), any(), anyOrNull()))
+ .thenReturn(android.util.Pair(handler, WindowContainerTransaction())
+ )
+
+ controller.minimizeTask(task)
+
+ verify(freeformTaskTransitionStarter).startPipTransition(any())
+ verify(freeformTaskTransitionStarter, never()).startMinimizedModeTransition(any())
+ }
+
+ @Test
+ fun onDesktopWindowMinimize_pipTask_autoEnterDisabled_startMinimizeTransition() {
+ val task = setUpPipTask(autoEnterEnabled = false)
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(Binder())
+
+ controller.minimizeTask(task)
+
+ verify(freeformTaskTransitionStarter).startMinimizedModeTransition(any())
+ verify(freeformTaskTransitionStarter, never()).startPipTransition(any())
+ }
+
+ @Test
fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntRemoveWallpaper() {
val task = setUpFreeformTask(active = true)
val transition = Binder()
@@ -3033,20 +3062,21 @@
.thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR)
// Drag move the task to the top edge
+ val currentDragBounds = Rect(100, 50, 500, 1000)
spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000))
spyController.onDragPositioningEnd(
task,
mockSurface,
Point(100, 50), /* position */
PointF(200f, 300f), /* inputCoordinate */
- Rect(100, 50, 500, 1000), /* currentDragBounds */
+ currentDragBounds,
Rect(0, 50, 2000, 2000) /* validDragArea */,
Rect() /* dragStartBounds */,
motionEvent,
desktopWindowDecoration)
// Assert bounds set to stable bounds
- val wct = getLatestToggleResizeDesktopTaskWct()
+ val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds)
assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS)
// Assert event is properly logged
verify(desktopModeEventLogger, times(1)).logTaskResizingStarted(
@@ -4228,6 +4258,14 @@
return task
}
+ private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo {
+ return setUpFreeformTask().apply {
+ pictureInPictureParams = PictureInPictureParams.Builder()
+ .setAutoEnterEnabled(autoEnterEnabled)
+ .build()
+ }
+ }
+
private fun setUpHomeTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
val task = createHomeTask(displayId)
whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplierTest.kt
new file mode 100644
index 0000000..40583f8
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplierTest.kt
@@ -0,0 +1,129 @@
+/*
+ * 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.common.viewhost
+
+import android.content.res.Configuration
+import android.graphics.Region
+import android.testing.AndroidTestingRunner
+import android.view.SurfaceControl
+import android.view.View
+import android.view.WindowManager
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.util.StubTransaction
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.mock
+
+/**
+ * Tests for [PooledWindowDecorViewHostSupplier].
+ *
+ * Build/Install/Run: atest WMShellUnitTests:PooledWindowDecorViewHostSupplierTest
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class PooledWindowDecorViewHostSupplierTest : ShellTestCase() {
+
+ private lateinit var supplier: PooledWindowDecorViewHostSupplier
+
+ @Test
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ }
+
+ @Test
+ fun acquire_poolBelowLimit_caches() = runTest {
+ supplier = createSupplier(maxPoolSize = 5)
+
+ val viewHost = FakeWindowDecorViewHost()
+ supplier.release(viewHost, StubTransaction())
+
+ assertThat(supplier.acquire(context, context.display)).isEqualTo(viewHost)
+ }
+
+ @Test
+ fun release_poolBelowLimit_doesNotReleaseViewHost() = runTest {
+ supplier = createSupplier(maxPoolSize = 5)
+
+ val viewHost = FakeWindowDecorViewHost()
+ val mockT = mock<SurfaceControl.Transaction>()
+ supplier.release(viewHost, mockT)
+
+ assertThat(viewHost.released).isFalse()
+ }
+
+ @Test
+ fun release_poolAtLimit_doesNotCache() = runTest {
+ supplier = createSupplier(maxPoolSize = 1)
+ val viewHost = FakeWindowDecorViewHost()
+ supplier.release(viewHost, StubTransaction()) // Maxes pool.
+
+ val viewHost2 = FakeWindowDecorViewHost()
+ supplier.release(viewHost2, StubTransaction()) // Beyond limit.
+
+ assertThat(supplier.acquire(context, context.display)).isEqualTo(viewHost)
+ // Second one wasn't cached, so the acquired one should've been a new instance.
+ assertThat(supplier.acquire(context, context.display)).isNotEqualTo(viewHost2)
+ }
+
+ @Test
+ fun release_poolAtLimit_releasesViewHost() = runTest {
+ supplier = createSupplier(maxPoolSize = 1)
+ val viewHost = FakeWindowDecorViewHost()
+ supplier.release(viewHost, StubTransaction()) // Maxes pool.
+
+ val viewHost2 = FakeWindowDecorViewHost()
+ val mockT = mock<SurfaceControl.Transaction>()
+ supplier.release(viewHost2, mockT) // Beyond limit.
+
+ // Second one doesn't fit, so it needs to be released.
+ assertThat(viewHost2.released).isTrue()
+ }
+
+ private fun CoroutineScope.createSupplier(maxPoolSize: Int) =
+ PooledWindowDecorViewHostSupplier(this, maxPoolSize)
+
+ private class FakeWindowDecorViewHost : WindowDecorViewHost {
+ var released = false
+ private set
+
+ override val surfaceControl: SurfaceControl
+ get() = SurfaceControl()
+
+ override fun updateView(
+ view: View,
+ attrs: WindowManager.LayoutParams,
+ configuration: Configuration,
+ touchableRegion: Region?,
+ onDrawTransaction: SurfaceControl.Transaction?,
+ ) {}
+
+ override fun updateViewAsync(
+ view: View,
+ attrs: WindowManager.LayoutParams,
+ configuration: Configuration,
+ touchableRegion: Region?,
+ ) {}
+
+ override fun release(t: SurfaceControl.Transaction) {
+ released = true
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt
new file mode 100644
index 0000000..245393a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt
@@ -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 com.android.wm.shell.windowdecor.common.viewhost
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.SurfaceControl
+import android.view.View
+import android.view.WindowManager
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+
+/**
+ * Tests for [ReusableWindowDecorViewHost].
+ *
+ * Build/Install/Run: atest WMShellUnitTests:ReusableWindowDecorViewHostTest
+ */
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class ReusableWindowDecorViewHostTest : ShellTestCase() {
+
+ @Test
+ fun update_differentView_replacesView() = runTest {
+ val view = View(context)
+ val lp = WindowManager.LayoutParams()
+ val reusableVH = createReusableViewHost()
+ reusableVH.updateView(view, lp, context.resources.configuration, null)
+
+ assertThat(reusableVH.rootView.childCount).isEqualTo(1)
+ assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(view)
+
+ val newView = View(context)
+ val newLp = WindowManager.LayoutParams()
+ reusableVH.updateView(newView, newLp, context.resources.configuration, null)
+
+ assertThat(reusableVH.rootView.childCount).isEqualTo(1)
+ assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(newView)
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun updateView_clearsPendingAsyncJob() = runTest {
+ val reusableVH = createReusableViewHost()
+ val asyncView = View(context)
+ val syncView = View(context)
+ val asyncAttrs = WindowManager.LayoutParams(100, 100)
+ val syncAttrs = WindowManager.LayoutParams(200, 200)
+
+ reusableVH.updateViewAsync(
+ view = asyncView,
+ attrs = asyncAttrs,
+ configuration = context.resources.configuration,
+ )
+
+ // No view host yet, since the coroutine hasn't run.
+ assertThat(reusableVH.viewHostAdapter.isInitialized()).isFalse()
+
+ reusableVH.updateView(
+ view = syncView,
+ attrs = syncAttrs,
+ configuration = context.resources.configuration,
+ onDrawTransaction = null,
+ )
+
+ // Would run coroutine if it hadn't been cancelled.
+ advanceUntilIdle()
+
+ assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
+ // View host view/attrs should match the ones from the sync call.
+ assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(syncView)
+ assertThat(reusableVH.view()!!.layoutParams.width).isEqualTo(syncAttrs.width)
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun updateViewAsync() = runTest {
+ val reusableVH = createReusableViewHost()
+ val view = View(context)
+ val attrs = WindowManager.LayoutParams(100, 100)
+
+ reusableVH.updateViewAsync(
+ view = view,
+ attrs = attrs,
+ configuration = context.resources.configuration,
+ )
+
+ assertThat(reusableVH.viewHostAdapter.isInitialized()).isFalse()
+
+ advanceUntilIdle()
+
+ assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun updateViewAsync_clearsPendingAsyncJob() = runTest {
+ val reusableVH = createReusableViewHost()
+
+ val view = View(context)
+ reusableVH.updateViewAsync(
+ view = view,
+ attrs = WindowManager.LayoutParams(100, 100),
+ configuration = context.resources.configuration,
+ )
+ val otherView = View(context)
+ reusableVH.updateViewAsync(
+ view = otherView,
+ attrs = WindowManager.LayoutParams(100, 100),
+ configuration = context.resources.configuration,
+ )
+
+ advanceUntilIdle()
+
+ assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
+ assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(otherView)
+ }
+
+ @Test
+ fun release() = runTest {
+ val reusableVH = createReusableViewHost()
+
+ val view = View(context)
+ reusableVH.updateView(
+ view = view,
+ attrs = WindowManager.LayoutParams(100, 100),
+ configuration = context.resources.configuration,
+ onDrawTransaction = null,
+ )
+
+ val t = mock(SurfaceControl.Transaction::class.java)
+ reusableVH.release(t)
+
+ verify(reusableVH.viewHostAdapter).release(t)
+ }
+
+ private fun CoroutineScope.createReusableViewHost() =
+ ReusableWindowDecorViewHost(
+ context = context,
+ mainScope = this,
+ display = context.display,
+ id = 1,
+ viewHostAdapter = spy(SurfaceControlViewHostAdapter(context, context.display)),
+ )
+
+ private fun ReusableWindowDecorViewHost.view(): View? = viewHostAdapter.viewHost?.view
+}
diff --git a/libs/hwui/jni/text/TextShaper.cpp b/libs/hwui/jni/text/TextShaper.cpp
index c735989..d1782b2 100644
--- a/libs/hwui/jni/text/TextShaper.cpp
+++ b/libs/hwui/jni/text/TextShaper.cpp
@@ -225,8 +225,8 @@
constexpr float NO_OVERRIDE = -1;
-float findValueFromVariationSettings(const minikin::FontFakery& fakery, minikin::AxisTag tag) {
- for (const minikin::FontVariation& fv : fakery.variationSettings()) {
+float findValueFromVariationSettings(const minikin::VariationSettings& axes, minikin::AxisTag tag) {
+ for (const minikin::FontVariation& fv : axes) {
if (fv.axisTag == tag) {
return fv.value;
}
@@ -238,8 +238,8 @@
static jfloat TextShaper_Result_getWeightOverride(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) {
const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
if (text_feature::typeface_redesign_readonly()) {
- float value =
- findValueFromVariationSettings(layout->layout.getFakery(i), minikin::TAG_wght);
+ float value = findValueFromVariationSettings(layout->layout.typeface(i)->GetAxes(),
+ minikin::TAG_wght);
return std::isnan(value) ? NO_OVERRIDE : value;
} else {
return layout->layout.getFakery(i).wghtAdjustment();
@@ -250,8 +250,8 @@
static jfloat TextShaper_Result_getItalicOverride(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) {
const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
if (text_feature::typeface_redesign_readonly()) {
- float value =
- findValueFromVariationSettings(layout->layout.getFakery(i), minikin::TAG_ital);
+ float value = findValueFromVariationSettings(layout->layout.typeface(i)->GetAxes(),
+ minikin::TAG_ital);
return std::isnan(value) ? NO_OVERRIDE : value;
} else {
return layout->layout.getFakery(i).italAdjustment();
diff --git a/media/java/android/media/quality/IMediaQualityManager.aidl b/media/java/android/media/quality/IMediaQualityManager.aidl
index 9daebca..253c2d8 100644
--- a/media/java/android/media/quality/IMediaQualityManager.aidl
+++ b/media/java/android/media/quality/IMediaQualityManager.aidl
@@ -25,51 +25,56 @@
import android.media.quality.PictureProfile;
import android.media.quality.SoundProfileHandle;
import android.media.quality.SoundProfile;
+import android.os.UserHandle;
/**
* Interface for Media Quality Manager
* @hide
*/
interface IMediaQualityManager {
- PictureProfile createPictureProfile(in PictureProfile pp, int userId);
- void updatePictureProfile(in String id, in PictureProfile pp, int userId);
- void removePictureProfile(in String id, int userId);
- PictureProfile getPictureProfile(in int type, in String name, int userId);
- List<PictureProfile> getPictureProfilesByPackage(in String packageName, int userId);
- List<PictureProfile> getAvailablePictureProfiles(int userId);
- boolean setDefaultPictureProfile(in String id, int userId);
- List<String> getPictureProfilePackageNames(int userId);
- List<String> getPictureProfileAllowList(int userId);
- void setPictureProfileAllowList(in List<String> packages, int userId);
- List<PictureProfileHandle> getPictureProfileHandle(in String[] id, int userId);
+ PictureProfile createPictureProfile(in PictureProfile pp, in UserHandle user);
+ void updatePictureProfile(in String id, in PictureProfile pp, in UserHandle user);
+ void removePictureProfile(in String id, in UserHandle user);
+ boolean setDefaultPictureProfile(in String id, in UserHandle user);
+ PictureProfile getPictureProfile(
+ in int type, in String name, in boolean includeParams, in UserHandle user);
+ List<PictureProfile> getPictureProfilesByPackage(
+ in String packageName, in boolean includeParams, in UserHandle user);
+ List<PictureProfile> getAvailablePictureProfiles(in boolean includeParams, in UserHandle user);
+ List<String> getPictureProfilePackageNames(in UserHandle user);
+ List<String> getPictureProfileAllowList(in UserHandle user);
+ void setPictureProfileAllowList(in List<String> packages, in UserHandle user);
+ List<PictureProfileHandle> getPictureProfileHandle(in String[] id, in UserHandle user);
- SoundProfile createSoundProfile(in SoundProfile pp, int userId);
- void updateSoundProfile(in String id, in SoundProfile pp, int userId);
- void removeSoundProfile(in String id, int userId);
- SoundProfile getSoundProfile(in int type, in String name, int userId);
- List<SoundProfile> getSoundProfilesByPackage(in String packageName, int userId);
- List<SoundProfile> getAvailableSoundProfiles(int userId);
- boolean setDefaultSoundProfile(in String id, int userId);
- List<String> getSoundProfilePackageNames(int userId);
- List<String> getSoundProfileAllowList(int userId);
- void setSoundProfileAllowList(in List<String> packages, int userId);
- List<SoundProfileHandle> getSoundProfileHandle(in String[] id, int userId);
+ SoundProfile createSoundProfile(in SoundProfile pp, in UserHandle user);
+ void updateSoundProfile(in String id, in SoundProfile pp, in UserHandle user);
+ void removeSoundProfile(in String id, in UserHandle user);
+ boolean setDefaultSoundProfile(in String id, in UserHandle user);
+ SoundProfile getSoundProfile(
+ in int type, in String name, in boolean includeParams, in UserHandle user);
+ List<SoundProfile> getSoundProfilesByPackage(
+ in String packageName, in boolean includeParams, in UserHandle user);
+ List<SoundProfile> getAvailableSoundProfiles(in boolean includeParams, in UserHandle user);
+ List<String> getSoundProfilePackageNames(in UserHandle user);
+ List<String> getSoundProfileAllowList(in UserHandle user);
+ void setSoundProfileAllowList(in List<String> packages, in UserHandle user);
+ List<SoundProfileHandle> getSoundProfileHandle(in String[] id, in UserHandle user);
void registerPictureProfileCallback(in IPictureProfileCallback cb);
void registerSoundProfileCallback(in ISoundProfileCallback cb);
void registerAmbientBacklightCallback(in IAmbientBacklightCallback cb);
- List<ParamCapability> getParamCapabilities(in List<String> names, int userId);
+ List<ParamCapability> getParamCapabilities(in List<String> names, in UserHandle user);
- boolean isSupported(int userId);
- void setAutoPictureQualityEnabled(in boolean enabled, int userId);
- boolean isAutoPictureQualityEnabled(int userId);
- void setSuperResolutionEnabled(in boolean enabled, int userId);
- boolean isSuperResolutionEnabled(int userId);
- void setAutoSoundQualityEnabled(in boolean enabled, int userId);
- boolean isAutoSoundQualityEnabled(int userId);
+ boolean isSupported(in UserHandle user);
+ void setAutoPictureQualityEnabled(in boolean enabled, in UserHandle user);
+ boolean isAutoPictureQualityEnabled(in UserHandle user);
+ void setSuperResolutionEnabled(in boolean enabled, in UserHandle user);
+ boolean isSuperResolutionEnabled(in UserHandle user);
+ void setAutoSoundQualityEnabled(in boolean enabled, in UserHandle user);
+ boolean isAutoSoundQualityEnabled(in UserHandle user);
- void setAmbientBacklightSettings(in AmbientBacklightSettings settings, int userId);
- void setAmbientBacklightEnabled(in boolean enabled, int userId);
- boolean isAmbientBacklightEnabled(int userId);
+ void setAmbientBacklightSettings(in AmbientBacklightSettings settings, in UserHandle user);
+ void setAmbientBacklightEnabled(in boolean enabled, in UserHandle user);
+ boolean isAmbientBacklightEnabled(in UserHandle user);
}
diff --git a/media/java/android/media/quality/IPictureProfileCallback.aidl b/media/java/android/media/quality/IPictureProfileCallback.aidl
index 34aa2b0..7071a16 100644
--- a/media/java/android/media/quality/IPictureProfileCallback.aidl
+++ b/media/java/android/media/quality/IPictureProfileCallback.aidl
@@ -29,5 +29,5 @@
void onPictureProfileUpdated(in String id, in PictureProfile p);
void onPictureProfileRemoved(in String id, in PictureProfile p);
void onParamCapabilitiesChanged(in String id, in List<ParamCapability> caps);
- void onError(in int err);
+ void onError(in String id, in int err);
}
diff --git a/media/java/android/media/quality/ISoundProfileCallback.aidl b/media/java/android/media/quality/ISoundProfileCallback.aidl
index 9043757..30bb106 100644
--- a/media/java/android/media/quality/ISoundProfileCallback.aidl
+++ b/media/java/android/media/quality/ISoundProfileCallback.aidl
@@ -29,5 +29,5 @@
void onSoundProfileUpdated(in String id, in SoundProfile p);
void onSoundProfileRemoved(in String id, in SoundProfile p);
void onParamCapabilitiesChanged(in String id, in List<ParamCapability> caps);
- void onError(in int err);
+ void onError(in String id, in int err);
}
diff --git a/media/java/android/media/quality/MediaQualityManager.java b/media/java/android/media/quality/MediaQualityManager.java
index 024b470c..7e87462 100644
--- a/media/java/android/media/quality/MediaQualityManager.java
+++ b/media/java/android/media/quality/MediaQualityManager.java
@@ -26,6 +26,7 @@
import android.content.Context;
import android.media.tv.flags.Flags;
import android.os.RemoteException;
+import android.os.UserHandle;
import androidx.annotation.RequiresPermission;
@@ -48,7 +49,7 @@
private final IMediaQualityManager mService;
private final Context mContext;
- private final int mUserId;
+ private final UserHandle mUserHandle;
private final Object mLock = new Object();
// @GuardedBy("mLock")
private final List<PictureProfileCallbackRecord> mPpCallbackRecords = new ArrayList<>();
@@ -66,7 +67,7 @@
*/
public MediaQualityManager(Context context, IMediaQualityManager service) {
mContext = context;
- mUserId = context.getUserId();
+ mUserHandle = context.getUser();
mService = service;
IPictureProfileCallback ppCallback = new IPictureProfileCallback.Stub() {
@Override
@@ -106,11 +107,11 @@
}
}
@Override
- public void onError(int err) {
+ public void onError(String profileId, int err) {
synchronized (mLock) {
for (PictureProfileCallbackRecord record : mPpCallbackRecords) {
// TODO: filter callback record
- record.postError(err);
+ record.postError(profileId, err);
}
}
}
@@ -153,11 +154,11 @@
}
}
@Override
- public void onError(int err) {
+ public void onError(String profileId, int err) {
synchronized (mLock) {
for (SoundProfileCallbackRecord record : mSpCallbackRecords) {
// TODO: filter callback record
- record.postError(err);
+ record.postError(profileId, err);
}
}
}
@@ -214,18 +215,21 @@
}
}
-
/**
* Gets picture profile by given profile type and name.
*
+ * @param type the type of the profile.
+ * @param name the name of the profile.
+ * @param includeParams {@code true} to include parameters in the profile; {@code false}
+ * otherwise.
* @return the corresponding picture profile if available; {@code null} if the name doesn't
- * exist.
+ * exist.
*/
@Nullable
public PictureProfile getPictureProfile(
- @PictureProfile.ProfileType int type, @NonNull String name) {
+ @PictureProfile.ProfileType int type, @NonNull String name, boolean includeParams) {
try {
- return mService.getPictureProfile(type, name, mUserId);
+ return mService.getPictureProfile(type, name, includeParams, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -235,14 +239,18 @@
/**
* Gets profiles that available to the given package.
*
+ * @param packageName the package name of the profiles.
+ * @param includeParams {@code true} to include parameters in the profile; {@code false}
+ * otherwise.
* @hide
*/
@SystemApi
@NonNull
@RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE)
- public List<PictureProfile> getPictureProfilesByPackage(@NonNull String packageName) {
+ public List<PictureProfile> getPictureProfilesByPackage(
+ @NonNull String packageName, boolean includeParams) {
try {
- return mService.getPictureProfilesByPackage(packageName, mUserId);
+ return mService.getPictureProfilesByPackage(packageName, includeParams, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -250,11 +258,16 @@
/**
* Gets profiles that available to the caller.
+ *
+ * @param includeParams {@code true} to include parameters in the profile; {@code false}
+ * otherwise.
+ * @return the corresponding picture profile if available; {@code null} if the name doesn't
+ * exist.
*/
@NonNull
- public List<PictureProfile> getAvailablePictureProfiles() {
+ public List<PictureProfile> getAvailablePictureProfiles(boolean includeParams) {
try {
- return mService.getAvailablePictureProfiles(mUserId);
+ return mService.getAvailablePictureProfiles(includeParams, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -272,7 +285,7 @@
@RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE)
public boolean setDefaultPictureProfile(@Nullable String id) {
try {
- return mService.setDefaultPictureProfile(id, mUserId);
+ return mService.setDefaultPictureProfile(id, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -281,7 +294,7 @@
/**
* Gets all package names whose picture profiles are available.
*
- * @see #getPictureProfilesByPackage(String)
+ * @see #getPictureProfilesByPackage(String, boolean)
* @hide
*/
@SystemApi
@@ -289,7 +302,7 @@
@RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE)
public List<String> getPictureProfilePackageNames() {
try {
- return mService.getPictureProfilePackageNames(mUserId);
+ return mService.getPictureProfilePackageNames(mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -301,7 +314,7 @@
*/
public List<PictureProfileHandle> getPictureProfileHandle(String[] id) {
try {
- return mService.getPictureProfileHandle(id, mUserId);
+ return mService.getPictureProfileHandle(id, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -313,7 +326,7 @@
*/
public List<SoundProfileHandle> getSoundProfileHandle(String[] id) {
try {
- return mService.getSoundProfileHandle(id, mUserId);
+ return mService.getSoundProfileHandle(id, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -324,10 +337,12 @@
*
* <p>If the profile is created successfully,
* {@link PictureProfileCallback#onPictureProfileAdded(String, PictureProfile)} is invoked.
+ *
+ * @param pp the {@link PictureProfile} object to be created.
*/
public void createPictureProfile(@NonNull PictureProfile pp) {
try {
- mService.createPictureProfile(pp, mUserId);
+ mService.createPictureProfile(pp, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -336,10 +351,13 @@
/**
* Updates an existing picture profile and store it in the system.
+ *
+ * @param profileId the id of the object to be updated.
+ * @param pp the {@link PictureProfile} object to be updated.
*/
public void updatePictureProfile(@NonNull String profileId, @NonNull PictureProfile pp) {
try {
- mService.updatePictureProfile(profileId, pp, mUserId);
+ mService.updatePictureProfile(profileId, pp, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -348,10 +366,12 @@
/**
* Removes a picture profile from the system.
+ *
+ * @param profileId the id of the object to be removed.
*/
public void removePictureProfile(@NonNull String profileId) {
try {
- mService.removePictureProfile(profileId, mUserId);
+ mService.removePictureProfile(profileId, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -387,18 +407,20 @@
}
}
-
/**
* Gets sound profile by given profile type and name.
*
- * @return the corresponding sound profile if available; {@code null} if the name doesn't
- * exist.
+ * @param type the type of the profile.
+ * @param name the name of the profile.
+ * @param includeParams {@code true} to include parameters in the profile; {@code false}
+ * otherwise.
+ * @return the corresponding sound profile if available; {@code null} if the name doesn't exist.
*/
@Nullable
public SoundProfile getSoundProfile(
- @SoundProfile.ProfileType int type, @NonNull String name) {
+ @SoundProfile.ProfileType int type, @NonNull String name, boolean includeParams) {
try {
- return mService.getSoundProfile(type, name, mUserId);
+ return mService.getSoundProfile(type, name, includeParams, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -408,14 +430,18 @@
/**
* Gets profiles that available to the given package.
*
+ * @param packageName the package name of the profiles.
+ * @param includeParams {@code true} to include parameters in the profile; {@code false}
+ * otherwise.
* @hide
*/
@SystemApi
@NonNull
@RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE)
- public List<SoundProfile> getSoundProfilesByPackage(@NonNull String packageName) {
+ public List<SoundProfile> getSoundProfilesByPackage(
+ @NonNull String packageName, boolean includeParams) {
try {
- return mService.getSoundProfilesByPackage(packageName, mUserId);
+ return mService.getSoundProfilesByPackage(packageName, includeParams, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -423,11 +449,16 @@
/**
* Gets profiles that available to the caller package.
+ *
+ * @param includeParams {@code true} to include parameters in the profile; {@code false}
+ * otherwise.
+ *
+ * @return the corresponding sound profile if available; {@code null} if the none available.
*/
@NonNull
- public List<SoundProfile> getAvailableSoundProfiles() {
+ public List<SoundProfile> getAvailableSoundProfiles(boolean includeParams) {
try {
- return mService.getAvailableSoundProfiles(mUserId);
+ return mService.getAvailableSoundProfiles(includeParams, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -445,7 +476,7 @@
@RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE)
public boolean setDefaultSoundProfile(@Nullable String id) {
try {
- return mService.setDefaultSoundProfile(id, mUserId);
+ return mService.setDefaultSoundProfile(id, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -454,7 +485,7 @@
/**
* Gets all package names whose sound profiles are available.
*
- * @see #getSoundProfilesByPackage(String)
+ * @see #getSoundProfilesByPackage(String, boolean)
*
* @hide
*/
@@ -463,7 +494,7 @@
@RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE)
public List<String> getSoundProfilePackageNames() {
try {
- return mService.getSoundProfilePackageNames(mUserId);
+ return mService.getSoundProfilePackageNames(mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -475,10 +506,12 @@
*
* <p>If the profile is created successfully,
* {@link SoundProfileCallback#onSoundProfileAdded(String, SoundProfile)} is invoked.
+ *
+ * @param sp the {@link SoundProfile} object to be created.
*/
public void createSoundProfile(@NonNull SoundProfile sp) {
try {
- mService.createSoundProfile(sp, mUserId);
+ mService.createSoundProfile(sp, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -487,10 +520,13 @@
/**
* Updates an existing sound profile and store it in the system.
+ *
+ * @param profileId the id of the object to be updated.
+ * @param sp the {@link SoundProfile} object to be updated.
*/
public void updateSoundProfile(@NonNull String profileId, @NonNull SoundProfile sp) {
try {
- mService.updateSoundProfile(profileId, sp, mUserId);
+ mService.updateSoundProfile(profileId, sp, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -499,10 +535,12 @@
/**
* Removes a sound profile from the system.
+ *
+ * @param profileId the id of the object to be removed.
*/
public void removeSoundProfile(@NonNull String profileId) {
try {
- mService.removeSoundProfile(profileId, mUserId);
+ mService.removeSoundProfile(profileId, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -514,7 +552,7 @@
@NonNull
public List<ParamCapability> getParamCapabilities(@NonNull List<String> names) {
try {
- return mService.getParamCapabilities(names, mUserId);
+ return mService.getParamCapabilities(names, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -532,7 +570,7 @@
@NonNull
public List<String> getPictureProfileAllowList() {
try {
- return mService.getPictureProfileAllowList(mUserId);
+ return mService.getPictureProfileAllowList(mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -546,7 +584,7 @@
@RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE)
public void setPictureProfileAllowList(@NonNull List<String> packageNames) {
try {
- mService.setPictureProfileAllowList(packageNames, mUserId);
+ mService.setPictureProfileAllowList(packageNames, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -564,7 +602,7 @@
@NonNull
public List<String> getSoundProfileAllowList() {
try {
- return mService.getSoundProfileAllowList(mUserId);
+ return mService.getSoundProfileAllowList(mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -578,7 +616,7 @@
@RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE)
public void setSoundProfileAllowList(@NonNull List<String> packageNames) {
try {
- mService.setSoundProfileAllowList(packageNames, mUserId);
+ mService.setSoundProfileAllowList(packageNames, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -590,7 +628,7 @@
*/
public boolean isSupported() {
try {
- return mService.isSupported(mUserId);
+ return mService.isSupported(mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -608,7 +646,7 @@
@RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE)
public void setAutoPictureQualityEnabled(boolean enabled) {
try {
- mService.setAutoPictureQualityEnabled(enabled, mUserId);
+ mService.setAutoPictureQualityEnabled(enabled, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -619,7 +657,7 @@
*/
public boolean isAutoPictureQualityEnabled() {
try {
- return mService.isAutoPictureQualityEnabled(mUserId);
+ return mService.isAutoPictureQualityEnabled(mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -636,7 +674,7 @@
@RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE)
public void setSuperResolutionEnabled(boolean enabled) {
try {
- mService.setSuperResolutionEnabled(enabled, mUserId);
+ mService.setSuperResolutionEnabled(enabled, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -647,7 +685,7 @@
*/
public boolean isSuperResolutionEnabled() {
try {
- return mService.isSuperResolutionEnabled(mUserId);
+ return mService.isSuperResolutionEnabled(mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -665,7 +703,7 @@
@RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE)
public void setAutoSoundQualityEnabled(boolean enabled) {
try {
- mService.setAutoSoundQualityEnabled(enabled, mUserId);
+ mService.setAutoSoundQualityEnabled(enabled, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -676,7 +714,7 @@
*/
public boolean isAutoSoundQualityEnabled() {
try {
- return mService.isAutoSoundQualityEnabled(mUserId);
+ return mService.isAutoSoundQualityEnabled(mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -725,7 +763,7 @@
@NonNull AmbientBacklightSettings settings) {
Preconditions.checkNotNull(settings);
try {
- mService.setAmbientBacklightSettings(settings, mUserId);
+ mService.setAmbientBacklightSettings(settings, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -736,7 +774,7 @@
*/
public boolean isAmbientBacklightEnabled() {
try {
- return mService.isAmbientBacklightEnabled(mUserId);
+ return mService.isAmbientBacklightEnabled(mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -750,7 +788,7 @@
@RequiresPermission(android.Manifest.permission.READ_COLOR_ZONES)
public void setAmbientBacklightEnabled(boolean enabled) {
try {
- mService.setAmbientBacklightEnabled(enabled, mUserId);
+ mService.setAmbientBacklightEnabled(enabled, mUserHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -807,11 +845,11 @@
});
}
- public void postError(int error) {
+ public void postError(String profileId, int error) {
mExecutor.execute(new Runnable() {
@Override
public void run() {
- mCallback.onError(error);
+ mCallback.onError(profileId, error);
}
});
}
@@ -867,11 +905,11 @@
});
}
- public void postError(int error) {
+ public void postError(String profileId, int error) {
mExecutor.execute(new Runnable() {
@Override
public void run() {
- mCallback.onError(error);
+ mCallback.onError(profileId, error);
}
});
}
@@ -937,9 +975,11 @@
/**
* This is invoked when an issue has occurred.
*
+ * @param profileId the profile ID related to the error. {@code null} if there is no
+ * associated profile.
* @param errorCode the error code
*/
- public void onError(@PictureProfile.ErrorCode int errorCode) {
+ public void onError(@Nullable String profileId, @PictureProfile.ErrorCode int errorCode) {
}
/**
@@ -992,9 +1032,11 @@
/**
* This is invoked when an issue has occurred.
*
+ * @param profileId the profile ID related to the error. {@code null} if there is no
+ * associated profile.
* @param errorCode the error code
*/
- public void onError(@SoundProfile.ErrorCode int errorCode) {
+ public void onError(@Nullable String profileId, @SoundProfile.ErrorCode int errorCode) {
}
/**
diff --git a/native/android/dynamic_instrumentation_manager.cpp b/native/android/dynamic_instrumentation_manager.cpp
index 5322136..0749731 100644
--- a/native/android/dynamic_instrumentation_manager.cpp
+++ b/native/android/dynamic_instrumentation_manager.cpp
@@ -15,7 +15,9 @@
*/
#define LOG_TAG "ADynamicInstrumentationManager"
+#include <android-base/properties.h>
#include <android/dynamic_instrumentation_manager.h>
+#include <android/os/instrumentation/BnOffsetCallback.h>
#include <android/os/instrumentation/ExecutableMethodFileOffsets.h>
#include <android/os/instrumentation/IDynamicInstrumentationManager.h>
#include <android/os/instrumentation/MethodDescriptor.h>
@@ -23,7 +25,9 @@
#include <binder/Binder.h>
#include <binder/IServiceManager.h>
#include <utils/Log.h>
+#include <utils/StrongPointer.h>
+#include <future>
#include <mutex>
#include <optional>
#include <string>
@@ -31,6 +35,9 @@
namespace android::dynamicinstrumentationmanager {
+using android::os::instrumentation::BnOffsetCallback;
+using android::os::instrumentation::ExecutableMethodFileOffsets;
+
// Global instance of IDynamicInstrumentationManager, service is obtained only on first use.
static std::mutex mLock;
static sp<os::instrumentation::IDynamicInstrumentationManager> mService;
@@ -131,6 +138,30 @@
delete instance;
}
+class ResultCallback : public BnOffsetCallback {
+public:
+ ::android::binder::Status onResult(
+ const ::std::optional<ExecutableMethodFileOffsets>& offsets) override {
+ promise_.set_value(offsets);
+ return android::binder::Status::ok();
+ }
+
+ std::optional<ExecutableMethodFileOffsets> waitForResult() {
+ std::future<std::optional<ExecutableMethodFileOffsets>> futureResult =
+ promise_.get_future();
+ auto futureStatus = futureResult.wait_for(
+ std::chrono::seconds(1 * android::base::HwTimeoutMultiplier()));
+ if (futureStatus == std::future_status::ready) {
+ return futureResult.get();
+ } else {
+ return std::nullopt;
+ }
+ }
+
+private:
+ std::promise<std::optional<ExecutableMethodFileOffsets>> promise_;
+};
+
int32_t ADynamicInstrumentationManager_getExecutableMethodFileOffsets(
const ADynamicInstrumentationManager_TargetProcess* targetProcess,
const ADynamicInstrumentationManager_MethodDescriptor* methodDescriptor,
@@ -150,15 +181,15 @@
return INVALID_OPERATION;
}
- std::optional<android::os::instrumentation::ExecutableMethodFileOffsets> offsets;
+ android::sp<ResultCallback> resultCallback = android::sp<ResultCallback>::make();
binder_status_t result =
service->getExecutableMethodFileOffsets(targetProcessParcel, methodDescriptorParcel,
- &offsets)
+ resultCallback)
.exceptionCode();
if (result != OK) {
return result;
}
-
+ std::optional<ExecutableMethodFileOffsets> offsets = resultCallback->waitForResult();
if (offsets != std::nullopt) {
auto* value = new ADynamicInstrumentationManager_ExecutableMethodFileOffsets();
value->containerPath = offsets->containerPath;
@@ -170,4 +201,4 @@
}
return result;
-}
\ No newline at end of file
+}
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
index e12c7a2..326bff4 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
@@ -201,6 +201,16 @@
"could_not_read_from_cursor";
private static final String ERROR_FAILED_TO_WRITE_ENTITY =
"failed_to_write_entity";
+ private static final String ERROR_COULD_NOT_READ_ENTITY =
+ "could_not_read_entity";
+ private static final String ERROR_SKIPPED_BY_SYSTEM = "skipped_by_system";
+ private static final String ERROR_SKIPPED_BY_BLOCKLIST =
+ "skipped_by_dynamic_blocklist";
+ private static final String ERROR_SKIPPED_PRESERVED = "skipped_preserved";
+ private static final String ERROR_SKIPPED_DUE_TO_LARGE_SCREEN =
+ "skipped_due_to_large_screen";
+ private static final String ERROR_DID_NOT_PASS_VALIDATION = "did_not_pass_validation";
+
// Name of the temporary file we use during full backup/restore. This is
// stored in the full-backup tarfile as well, so should not be changed.
@@ -373,7 +383,7 @@
restoreSettings(data, Settings.System.CONTENT_URI, movedToGlobal,
movedToSecure, /* movedToSystem= */ null,
R.array.restore_blocked_system_settings, dynamicBlockList,
- preservedSystemSettings);
+ preservedSystemSettings, KEY_SYSTEM);
mSettingsHelper.applyAudioSettings();
break;
@@ -381,13 +391,13 @@
restoreSettings(data, Settings.Secure.CONTENT_URI, movedToGlobal,
/* movedToSecure= */ null, movedToSystem,
R.array.restore_blocked_secure_settings, dynamicBlockList,
- preservedSecureSettings);
+ preservedSecureSettings, KEY_SECURE);
break;
case KEY_GLOBAL :
restoreSettings(data, Settings.Global.CONTENT_URI, /* movedToGlobal= */ null,
movedToSecure, movedToSystem, R.array.restore_blocked_global_settings,
- dynamicBlockList, preservedGlobalSettings);
+ dynamicBlockList, preservedGlobalSettings, KEY_GLOBAL);
break;
case KEY_WIFI_SUPPLICANT :
@@ -506,7 +516,7 @@
restoreSettings(buffer, nBytes, Settings.System.CONTENT_URI, movedToGlobal,
movedToSecure, /* movedToSystem= */ null,
R.array.restore_blocked_system_settings, Collections.emptySet(),
- Collections.emptySet());
+ Collections.emptySet(), KEY_SYSTEM);
// secure settings
nBytes = in.readInt();
@@ -516,7 +526,7 @@
restoreSettings(buffer, nBytes, Settings.Secure.CONTENT_URI, movedToGlobal,
/* movedToSecure= */ null, movedToSystem,
R.array.restore_blocked_secure_settings, Collections.emptySet(),
- Collections.emptySet());
+ Collections.emptySet(), KEY_SECURE);
// Global only if sufficiently new
if (version >= FULL_BACKUP_ADDED_GLOBAL) {
@@ -527,7 +537,7 @@
restoreSettings(buffer, nBytes, Settings.Global.CONTENT_URI,
/* movedToGlobal= */ null, movedToSecure, movedToSystem,
R.array.restore_blocked_global_settings, Collections.emptySet(),
- Collections.emptySet());
+ Collections.emptySet(), KEY_GLOBAL);
}
// locale
@@ -808,7 +818,8 @@
return baos.toByteArray();
}
- private void restoreSettings(
+ @VisibleForTesting
+ void restoreSettings(
BackupDataInput data,
Uri contentUri,
Set<String> movedToGlobal,
@@ -816,12 +827,17 @@
Set<String> movedToSystem,
int blockedSettingsArrayId,
Set<String> dynamicBlockList,
- Set<String> settingsToPreserve) {
+ Set<String> settingsToPreserve,
+ String settingsKey) {
byte[] settings = new byte[data.getDataSize()];
try {
data.readEntityData(settings, 0, settings.length);
} catch (IOException ioe) {
Log.e(TAG, "Couldn't read entity data");
+ if (areAgentMetricsEnabled) {
+ mBackupRestoreEventLogger.logItemsRestoreFailed(
+ settingsKey, /* count= */ 1, ERROR_COULD_NOT_READ_ENTITY);
+ }
return;
}
restoreSettings(
@@ -833,7 +849,8 @@
movedToSystem,
blockedSettingsArrayId,
dynamicBlockList,
- settingsToPreserve);
+ settingsToPreserve,
+ settingsKey);
}
private void restoreSettings(
@@ -845,7 +862,8 @@
Set<String> movedToSystem,
int blockedSettingsArrayId,
Set<String> dynamicBlockList,
- Set<String> settingsToPreserve) {
+ Set<String> settingsToPreserve,
+ String settingsKey) {
restoreSettings(
settings,
0,
@@ -856,7 +874,8 @@
movedToSystem,
blockedSettingsArrayId,
dynamicBlockList,
- settingsToPreserve);
+ settingsToPreserve,
+ settingsKey);
}
@VisibleForTesting
@@ -870,12 +889,13 @@
Set<String> movedToSystem,
int blockedSettingsArrayId,
Set<String> dynamicBlockList,
- Set<String> settingsToPreserve) {
+ Set<String> settingsToPreserve,
+ String settingsKey) {
if (DEBUG) {
Log.i(TAG, "restoreSettings: " + contentUri);
}
- SettingsBackupWhitelist whitelist = getBackupWhitelist(contentUri);
+ SettingsBackupAllowlist allowlist = getBackupAllowlist(contentUri);
// Restore only the white list data.
final ArrayMap<String, String> cachedEntries = new ArrayMap<>();
@@ -885,7 +905,8 @@
Set<String> blockedSettings = getBlockedSettings(blockedSettingsArrayId);
- for (String key : whitelist.mSettingsWhitelist) {
+ int restoredSettingsCount = 0;
+ for (String key : allowlist.mSettingsAllowlist) {
boolean isBlockedBySystem = blockedSettings != null && blockedSettings.contains(key);
if (isBlockedBySystem || isBlockedByDynamicList(dynamicBlockList, contentUri, key)) {
Log.i(
@@ -895,6 +916,12 @@
+ " removed from restore by "
+ (isBlockedBySystem ? "system" : "dynamic")
+ " block list");
+ if (areAgentMetricsEnabled) {
+ mBackupRestoreEventLogger.logItemsRestoreFailed(
+ settingsKey,
+ /* count= */ 1,
+ isBlockedBySystem ? ERROR_SKIPPED_BY_SYSTEM : ERROR_SKIPPED_BY_BLOCKLIST);
+ }
continue;
}
@@ -905,12 +932,20 @@
if (isSettingPreserved && !Settings.Secure.NAVIGATION_MODE.equals(key)) {
Log.i(TAG, "Skipping restore for setting " + key + " as it is marked as "
+ "preserved");
+ if (areAgentMetricsEnabled) {
+ mBackupRestoreEventLogger.logItemsRestoreFailed(
+ settingsKey, /* count= */ 1, ERROR_SKIPPED_PRESERVED);
+ }
continue;
}
if (LargeScreenSettings.doNotRestoreIfLargeScreenSetting(key, getBaseContext())) {
Log.i(TAG, "Skipping restore for setting " + key + " as the target device "
+ "is a large screen (i.e tablet or foldable in unfolded state)");
+ if (areAgentMetricsEnabled) {
+ mBackupRestoreEventLogger.logItemsRestoreFailed(
+ settingsKey, /* count= */ 1, ERROR_SKIPPED_DUE_TO_LARGE_SCREEN);
+ }
continue;
}
@@ -947,19 +982,34 @@
}
// only restore the settings that have valid values
- if (!isValidSettingValue(key, value, whitelist.mSettingsValidators)) {
+ if (!isValidSettingValue(key, value, allowlist.mSettingsValidators)) {
Log.w(TAG, "Attempted restore of " + key + " setting, but its value didn't pass"
+ " validation, value: " + value);
+ if (areAgentMetricsEnabled) {
+ mBackupRestoreEventLogger.logItemsRestoreFailed(
+ settingsKey, /* count= */ 1, ERROR_DID_NOT_PASS_VALIDATION);
+ }
continue;
}
final Uri destination;
+ // If the destination changes, we need to update the key used as datatype for metrics.
+ String finalSettingsKey = settingsKey;
if (movedToGlobal != null && movedToGlobal.contains(key)) {
destination = Settings.Global.CONTENT_URI;
+ if (areAgentMetricsEnabled) {
+ finalSettingsKey = KEY_GLOBAL;
+ }
} else if (movedToSecure != null && movedToSecure.contains(key)) {
destination = Settings.Secure.CONTENT_URI;
+ if (areAgentMetricsEnabled) {
+ finalSettingsKey = KEY_SECURE;
+ }
} else if (movedToSystem != null && movedToSystem.contains(key)) {
destination = Settings.System.CONTENT_URI;
+ if (areAgentMetricsEnabled) {
+ finalSettingsKey = KEY_SYSTEM;
+ }
} else {
destination = contentUri;
}
@@ -977,6 +1027,10 @@
if (isSettingPreserved) {
Log.i(TAG, "Skipping restore for setting navigation_mode "
+ "as it is marked as preserved");
+ if (areAgentMetricsEnabled) {
+ mBackupRestoreEventLogger.logItemsRestoreFailed(
+ finalSettingsKey, /* count= */ 1, ERROR_SKIPPED_PRESERVED);
+ }
continue;
}
}
@@ -996,12 +1050,16 @@
Log.d(TAG, "Restored font scale from: " + toRestore + " to " + value);
}
-
+ // TODO(b/379861078): Log metrics inside this method.
settingsHelper.restoreValue(this, cr, contentValues, destination, key, value,
mRestoredFromSdkInt);
Log.d(TAG, "Restored setting: " + destination + " : " + key + "=" + value);
+ if (areAgentMetricsEnabled) {
+ mBackupRestoreEventLogger.logItemsRestored(finalSettingsKey, /* count= */ 1);
+ }
}
+
}
@@ -1031,29 +1089,29 @@
}
@VisibleForTesting
- SettingsBackupWhitelist getBackupWhitelist(Uri contentUri) {
+ SettingsBackupAllowlist getBackupAllowlist(Uri contentUri) {
// Figure out the white list and redirects to the global table. We restore anything
// in either the backup allowlist or the legacy-restore allowlist for this table.
- String[] whitelist;
+ String[] allowlist;
Map<String, Validator> validators = null;
if (contentUri.equals(Settings.Secure.CONTENT_URI)) {
- whitelist = ArrayUtils.concat(String.class, SecureSettings.SETTINGS_TO_BACKUP,
+ allowlist = ArrayUtils.concat(String.class, SecureSettings.SETTINGS_TO_BACKUP,
Settings.Secure.LEGACY_RESTORE_SETTINGS,
DeviceSpecificSettings.DEVICE_SPECIFIC_SETTINGS_TO_BACKUP);
validators = SecureSettingsValidators.VALIDATORS;
} else if (contentUri.equals(Settings.System.CONTENT_URI)) {
- whitelist = ArrayUtils.concat(String.class, SystemSettings.SETTINGS_TO_BACKUP,
+ allowlist = ArrayUtils.concat(String.class, SystemSettings.SETTINGS_TO_BACKUP,
Settings.System.LEGACY_RESTORE_SETTINGS);
validators = SystemSettingsValidators.VALIDATORS;
} else if (contentUri.equals(Settings.Global.CONTENT_URI)) {
- whitelist = ArrayUtils.concat(String.class, getGlobalSettingsToBackup(),
+ allowlist = ArrayUtils.concat(String.class, getGlobalSettingsToBackup(),
Settings.Global.LEGACY_RESTORE_SETTINGS);
validators = GlobalSettingsValidators.VALIDATORS;
} else {
throw new IllegalArgumentException("Unknown URI: " + contentUri);
}
- return new SettingsBackupWhitelist(whitelist, validators);
+ return new SettingsBackupAllowlist(allowlist, validators);
}
private String[] getGlobalSettingsToBackup() {
@@ -1449,7 +1507,8 @@
null,
blockedSettingsArrayId,
dynamicBlocklist,
- preservedSettings);
+ preservedSettings,
+ KEY_DEVICE_SPECIFIC_CONFIG);
updateWindowManagerIfNeeded(originalDensity);
@@ -1647,14 +1706,14 @@
* Store the allowlist of settings to be backed up and validators for them.
*/
@VisibleForTesting
- static class SettingsBackupWhitelist {
- final String[] mSettingsWhitelist;
+ static class SettingsBackupAllowlist {
+ final String[] mSettingsAllowlist;
final Map<String, Validator> mSettingsValidators;
- SettingsBackupWhitelist(String[] settingsWhitelist,
+ SettingsBackupAllowlist(String[] settingsAllowlist,
Map<String, Validator> settingsValidators) {
- mSettingsWhitelist = settingsWhitelist;
+ mSettingsAllowlist = settingsAllowlist;
mSettingsValidators = settingsValidators;
}
}
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java
index 4642864..350c149 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java
@@ -30,6 +30,7 @@
import android.app.backup.BackupAnnotations.BackupDestination;
import android.app.backup.BackupAnnotations.OperationType;
+import android.app.backup.BackupDataInput;
import android.app.backup.BackupDataOutput;
import android.app.backup.BackupRestoreEventLogger;
import android.app.backup.BackupRestoreEventLogger.DataTypeResult;
@@ -57,6 +58,7 @@
import com.android.window.flags.Flags;
+import java.util.List;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -95,6 +97,15 @@
private static final Map<String, Validator> TEST_VALUES_VALIDATORS = new HashMap<>();
private static final String TEST_KEY = "test_key";
private static final String TEST_VALUE = "test_value";
+ private static final String ERROR_COULD_NOT_READ_ENTITY = "could_not_read_entity";
+ private static final String ERROR_SKIPPED_BY_SYSTEM = "skipped_by_system";
+ private static final String ERROR_SKIPPED_BY_BLOCKLIST =
+ "skipped_by_dynamic_blocklist";
+ private static final String ERROR_SKIPPED_PRESERVED = "skipped_preserved";
+ private static final String ERROR_DID_NOT_PASS_VALIDATION = "did_not_pass_validation";
+ private static final String KEY_SYSTEM = "system";
+ private static final String KEY_SECURE = "secure";
+ private static final String KEY_GLOBAL = "global";
static {
DEVICE_SPECIFIC_TEST_VALUES.put(Settings.Secure.DISPLAY_DENSITY_FORCED,
@@ -113,6 +124,7 @@
@Rule
public final MockitoRule mockito = MockitoJUnit.rule();
+ @Mock private BackupDataInput mBackupDataInput;
@Mock private BackupDataOutput mBackupDataOutput;
private TestFriendlySettingsBackupAgent mAgentUnderTest;
@@ -232,19 +244,32 @@
@Test
public void testOnRestore_preservedSettingsAreNotRestored() {
- SettingsBackupAgent.SettingsBackupWhitelist whitelist =
- new SettingsBackupAgent.SettingsBackupWhitelist(
+ SettingsBackupAgent.SettingsBackupAllowlist allowlist =
+ new SettingsBackupAgent.SettingsBackupAllowlist(
new String[] { OVERRIDDEN_TEST_SETTING, PRESERVED_TEST_SETTING },
TEST_VALUES_VALIDATORS);
- mAgentUnderTest.setSettingsWhitelist(whitelist);
+ mAgentUnderTest.setSettingsAllowlist(allowlist);
mAgentUnderTest.setBlockedSettings();
TestSettingsHelper settingsHelper = new TestSettingsHelper(mContext);
mAgentUnderTest.mSettingsHelper = settingsHelper;
byte[] backupData = generateBackupData(TEST_VALUES);
- mAgentUnderTest.restoreSettings(backupData, /* pos */ 0, backupData.length, TEST_URI,
- null, null, null, /* blockedSettingsArrayId */ 0, Collections.emptySet(),
- new HashSet<>(Collections.singletonList(SettingsBackupAgent.getQualifiedKeyForSetting(PRESERVED_TEST_SETTING, TEST_URI))));
+ mAgentUnderTest.restoreSettings(
+ backupData,
+ /* pos */ 0,
+ backupData.length,
+ TEST_URI,
+ null,
+ null,
+ null,
+ /* blockedSettingsArrayId */ 0,
+ Collections.emptySet(),
+ new HashSet<>(Collections
+ .singletonList(
+ SettingsBackupAgent
+ .getQualifiedKeyForSetting(
+ PRESERVED_TEST_SETTING, TEST_URI))),
+ TEST_KEY);
assertTrue(settingsHelper.mWrittenValues.containsKey(OVERRIDDEN_TEST_SETTING));
assertFalse(settingsHelper.mWrittenValues.containsKey(PRESERVED_TEST_SETTING));
@@ -395,6 +420,382 @@
assertNull(getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest));
}
+ @Test
+ @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+ public void restoreSettings_agentMetricsAreEnabled_agentMetricsAreLogged() {
+ mAgentUnderTest.onCreate(
+ UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+ SettingsBackupAgent.SettingsBackupAllowlist allowlist =
+ new SettingsBackupAgent.SettingsBackupAllowlist(
+ new String[] {OVERRIDDEN_TEST_SETTING},
+ TEST_VALUES_VALIDATORS);
+ mAgentUnderTest.setSettingsAllowlist(allowlist);
+ mAgentUnderTest.setBlockedSettings();
+ TestSettingsHelper settingsHelper = new TestSettingsHelper(mContext);
+ mAgentUnderTest.mSettingsHelper = settingsHelper;
+
+ byte[] backupData = generateBackupData(TEST_VALUES);
+ mAgentUnderTest
+ .restoreSettings(
+ backupData,
+ /* pos= */ 0,
+ backupData.length,
+ TEST_URI,
+ /* movedToGlobal= */ null,
+ /* movedToSecure= */ null,
+ /* movedToSystem= */ null,
+ /* blockedSettingsArrayId= */ 0,
+ /* dynamicBlockList= */ Collections.emptySet(),
+ /* settingsToPreserve= */ Collections.emptySet(),
+ TEST_KEY);
+
+ DataTypeResult loggingResult =
+ getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest);
+ assertNotNull(loggingResult);
+ assertEquals(loggingResult.getSuccessCount(), 1);
+ }
+
+ @Test
+ @DisableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+ public void restoreSettings_agentMetricsAreDisabled_agentMetricsAreNotLogged() {
+ mAgentUnderTest.onCreate(
+ UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+ SettingsBackupAgent.SettingsBackupAllowlist allowlist =
+ new SettingsBackupAgent.SettingsBackupAllowlist(
+ new String[] {OVERRIDDEN_TEST_SETTING},
+ TEST_VALUES_VALIDATORS);
+ mAgentUnderTest.setSettingsAllowlist(allowlist);
+ mAgentUnderTest.setBlockedSettings();
+ TestSettingsHelper settingsHelper = new TestSettingsHelper(mContext);
+ mAgentUnderTest.mSettingsHelper = settingsHelper;
+
+ byte[] backupData = generateBackupData(TEST_VALUES);
+ mAgentUnderTest
+ .restoreSettings(
+ backupData,
+ /* pos= */ 0,
+ backupData.length,
+ TEST_URI,
+ /* movedToGlobal= */ null,
+ /* movedToSecure= */ null,
+ /* movedToSystem= */ null,
+ /* blockedSettingsArrayId= */ 0,
+ /* dynamicBlockList= */ Collections.emptySet(),
+ /* settingsToPreserve= */ Collections.emptySet(),
+ TEST_KEY);
+
+ DataTypeResult loggingResult =
+ getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest);
+ assertNull(loggingResult);
+ }
+
+ @Test
+ @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+ public void restoreSettings_agentMetricsAreEnabled_readEntityDataFails_failureIsLogged()
+ throws IOException {
+ when(mBackupDataInput.readEntityData(any(byte[].class), anyInt(), anyInt()))
+ .thenThrow(new IOException());
+ mAgentUnderTest.onCreate(
+ UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+
+ mAgentUnderTest.restoreSettings(
+ mBackupDataInput,
+ TEST_URI,
+ /* movedToGlobal= */ null,
+ /* movedToSecure= */ null,
+ /* movedToSystem= */ null,
+ /* blockedSettingsArrayId= */ 0,
+ /* dynamicBlockList= */ Collections.emptySet(),
+ /* settingsToPreserve= */ Collections.emptySet(),
+ TEST_KEY);
+
+ DataTypeResult loggingResult =
+ getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest);
+ assertNotNull(loggingResult);
+ assertEquals(loggingResult.getFailCount(), 1);
+ assertTrue(loggingResult.getErrors().containsKey(ERROR_COULD_NOT_READ_ENTITY));
+ }
+
+ @Test
+ @DisableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+ public void restoreSettings_agentMetricsAreDisabled_readEntityDataFails_failureIsNotLogged()
+ throws IOException {
+ when(mBackupDataInput.readEntityData(any(byte[].class), anyInt(), anyInt()))
+ .thenThrow(new IOException());
+ mAgentUnderTest.onCreate(
+ UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+
+ mAgentUnderTest.restoreSettings(
+ mBackupDataInput,
+ TEST_URI,
+ /* movedToGlobal= */ null,
+ /* movedToSecure= */ null,
+ /* movedToSystem= */ null,
+ /* blockedSettingsArrayId= */ 0,
+ /* dynamicBlockList= */ Collections.emptySet(),
+ /* settingsToPreserve= */ Collections.emptySet(),
+ TEST_KEY);
+
+ assertNull(getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest));
+ }
+
+ @Test
+ @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+ public void restoreSettings_agentMetricsAreEnabled_settingIsSkippedBySystem_failureIsLogged() {
+ mAgentUnderTest.onCreate(
+ UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+ String[] settingBlockedBySystem = new String[] {OVERRIDDEN_TEST_SETTING};
+ SettingsBackupAgent.SettingsBackupAllowlist allowlist =
+ new SettingsBackupAgent.SettingsBackupAllowlist(
+ settingBlockedBySystem,
+ TEST_VALUES_VALIDATORS);
+ mAgentUnderTest.setSettingsAllowlist(allowlist);
+ mAgentUnderTest.setBlockedSettings(settingBlockedBySystem);
+ TestSettingsHelper settingsHelper = new TestSettingsHelper(mContext);
+ mAgentUnderTest.mSettingsHelper = settingsHelper;
+
+ byte[] backupData = generateBackupData(TEST_VALUES);
+ mAgentUnderTest
+ .restoreSettings(
+ backupData,
+ /* pos= */ 0,
+ backupData.length,
+ TEST_URI,
+ /* movedToGlobal= */ null,
+ /* movedToSecure= */ null,
+ /* movedToSystem= */ null,
+ /* blockedSettingsArrayId= */ 0,
+ /* dynamicBlockList= */ Collections.emptySet(),
+ /* settingsToPreserve= */ Collections.emptySet(),
+ TEST_KEY);
+
+ DataTypeResult loggingResult =
+ getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest);
+ assertNotNull(loggingResult);
+ assertEquals(loggingResult.getFailCount(), 1);
+ assertTrue(loggingResult.getErrors().containsKey(ERROR_SKIPPED_BY_SYSTEM));
+ }
+
+ @Test
+ @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+ public void restoreSettings_agentMetricsAreEnabled_settingIsSkippedByBlockList_failureIsLogged() {
+ mAgentUnderTest.onCreate(
+ UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+ SettingsBackupAgent.SettingsBackupAllowlist allowlist =
+ new SettingsBackupAgent.SettingsBackupAllowlist(
+ new String[] {OVERRIDDEN_TEST_SETTING},
+ TEST_VALUES_VALIDATORS);
+ mAgentUnderTest.setSettingsAllowlist(allowlist);
+ mAgentUnderTest.setBlockedSettings();
+ TestSettingsHelper settingsHelper = new TestSettingsHelper(mContext);
+ mAgentUnderTest.mSettingsHelper = settingsHelper;
+ Set<String> dynamicBlockList =
+ Set.of(Uri.withAppendedPath(TEST_URI, OVERRIDDEN_TEST_SETTING).toString());
+
+ byte[] backupData = generateBackupData(TEST_VALUES);
+ mAgentUnderTest
+ .restoreSettings(
+ backupData,
+ /* pos= */ 0,
+ backupData.length,
+ TEST_URI,
+ /* movedToGlobal= */ null,
+ /* movedToSecure= */ null,
+ /* movedToSystem= */ null,
+ /* blockedSettingsArrayId= */ 0,
+ dynamicBlockList,
+ /* settingsToPreserve= */ Collections.emptySet(),
+ TEST_KEY);
+
+ DataTypeResult loggingResult =
+ getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest);
+ assertNotNull(loggingResult);
+ assertEquals(loggingResult.getFailCount(), 1);
+ assertTrue(loggingResult.getErrors().containsKey(ERROR_SKIPPED_BY_BLOCKLIST));
+ }
+
+ @Test
+ @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+ public void restoreSettings_agentMetricsAreEnabled_settingIsPreserved_failureIsLogged() {
+ mAgentUnderTest.onCreate(
+ UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+ SettingsBackupAgent.SettingsBackupAllowlist allowlist =
+ new SettingsBackupAgent.SettingsBackupAllowlist(
+ new String[] {OVERRIDDEN_TEST_SETTING},
+ TEST_VALUES_VALIDATORS);
+ mAgentUnderTest.setSettingsAllowlist(allowlist);
+ mAgentUnderTest.setBlockedSettings();
+ TestSettingsHelper settingsHelper = new TestSettingsHelper(mContext);
+ mAgentUnderTest.mSettingsHelper = settingsHelper;
+ Set<String> preservedSettings =
+ Set.of(Uri.withAppendedPath(TEST_URI, OVERRIDDEN_TEST_SETTING).toString());
+
+ byte[] backupData = generateBackupData(TEST_VALUES);
+ mAgentUnderTest
+ .restoreSettings(
+ backupData,
+ /* pos= */ 0,
+ backupData.length,
+ TEST_URI,
+ /* movedToGlobal= */ null,
+ /* movedToSecure= */ null,
+ /* movedToSystem= */ null,
+ /* blockedSettingsArrayId= */ 0,
+ /* dynamicBlockList = */ Collections.emptySet(),
+ preservedSettings,
+ TEST_KEY);
+
+ DataTypeResult loggingResult =
+ getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest);
+ assertNotNull(loggingResult);
+ assertEquals(loggingResult.getFailCount(), 1);
+ assertTrue(loggingResult.getErrors().containsKey(ERROR_SKIPPED_PRESERVED));
+ }
+
+ @Test
+ @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+ public void restoreSettings_agentMetricsAreEnabled_settingIsNotValid_failureIsLogged() {
+ mAgentUnderTest.onCreate(
+ UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+ SettingsBackupAgent.SettingsBackupAllowlist allowlist =
+ new SettingsBackupAgent.SettingsBackupAllowlist(
+ new String[] {OVERRIDDEN_TEST_SETTING},
+ /* settingsValidators= */ null);
+ mAgentUnderTest.setSettingsAllowlist(allowlist);
+ mAgentUnderTest.setBlockedSettings();
+ TestSettingsHelper settingsHelper = new TestSettingsHelper(mContext);
+ mAgentUnderTest.mSettingsHelper = settingsHelper;
+
+ byte[] backupData = generateBackupData(TEST_VALUES);
+ mAgentUnderTest
+ .restoreSettings(
+ backupData,
+ /* pos= */ 0,
+ backupData.length,
+ TEST_URI,
+ /* movedToGlobal= */ null,
+ /* movedToSecure= */ null,
+ /* movedToSystem= */ null,
+ /* blockedSettingsArrayId= */ 0,
+ /* dynamicBlockList = */ Collections.emptySet(),
+ /* settingsToPreserve= */ Collections.emptySet(),
+ TEST_KEY);
+
+ DataTypeResult loggingResult =
+ getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest);
+ assertNotNull(loggingResult);
+ assertEquals(loggingResult.getFailCount(), 1);
+ assertTrue(loggingResult.getErrors().containsKey(ERROR_DID_NOT_PASS_VALIDATION));
+ }
+
+ @Test
+ @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+ public void restoreSettings_agentMetricsAreEnabled_settingIsMarkedAsMovedToGlobal_agentMetricsAreLoggedWithGlobalKey() {
+ mAgentUnderTest.onCreate(
+ UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+ SettingsBackupAgent.SettingsBackupAllowlist allowlist =
+ new SettingsBackupAgent.SettingsBackupAllowlist(
+ new String[] {OVERRIDDEN_TEST_SETTING},
+ TEST_VALUES_VALIDATORS);
+ mAgentUnderTest.setSettingsAllowlist(allowlist);
+ mAgentUnderTest.setBlockedSettings();
+ TestSettingsHelper settingsHelper = new TestSettingsHelper(mContext);
+ mAgentUnderTest.mSettingsHelper = settingsHelper;
+
+ byte[] backupData = generateBackupData(TEST_VALUES);
+ mAgentUnderTest
+ .restoreSettings(
+ backupData,
+ /* pos= */ 0,
+ backupData.length,
+ TEST_URI,
+ /* movedToGlobal= */ Set.of(OVERRIDDEN_TEST_SETTING),
+ /* movedToSecure= */ null,
+ /* movedToSystem= */ null,
+ /* blockedSettingsArrayId= */ 0,
+ /* dynamicBlockList= */ Collections.emptySet(),
+ /* settingsToPreserve= */ Collections.emptySet(),
+ TEST_KEY);
+
+ DataTypeResult loggingResult =
+ getLoggingResultForDatatype(KEY_GLOBAL, mAgentUnderTest);
+ assertNotNull(loggingResult);
+ assertEquals(loggingResult.getSuccessCount(), 1);
+ assertNull(getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest));
+ }
+
+ @Test
+ @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+ public void restoreSettings_agentMetricsAreEnabled_settingIsMarkedAsMovedToSecure_agentMetricsAreLoggedWithSecureKey() {
+ mAgentUnderTest.onCreate(
+ UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+ SettingsBackupAgent.SettingsBackupAllowlist allowlist =
+ new SettingsBackupAgent.SettingsBackupAllowlist(
+ new String[] {OVERRIDDEN_TEST_SETTING},
+ TEST_VALUES_VALIDATORS);
+ mAgentUnderTest.setSettingsAllowlist(allowlist);
+ mAgentUnderTest.setBlockedSettings();
+ TestSettingsHelper settingsHelper = new TestSettingsHelper(mContext);
+ mAgentUnderTest.mSettingsHelper = settingsHelper;
+
+ byte[] backupData = generateBackupData(TEST_VALUES);
+ mAgentUnderTest
+ .restoreSettings(
+ backupData,
+ /* pos= */ 0,
+ backupData.length,
+ TEST_URI,
+ /* movedToGlobal= */ null,
+ /* movedToSecure= */ Set.of(OVERRIDDEN_TEST_SETTING),
+ /* movedToSystem= */ null,
+ /* blockedSettingsArrayId= */ 0,
+ /* dynamicBlockList= */ Collections.emptySet(),
+ /* settingsToPreserve= */ Collections.emptySet(),
+ TEST_KEY);
+
+ DataTypeResult loggingResult =
+ getLoggingResultForDatatype(KEY_SECURE, mAgentUnderTest);
+ assertNotNull(loggingResult);
+ assertEquals(loggingResult.getSuccessCount(), 1);
+ assertNull(getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest));
+ }
+
+ @Test
+ @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+ public void restoreSettings_agentMetricsAreEnabled_settingIsMarkedAsMovedToSystem_agentMetricsAreLoggedWithSystemKey() {
+ mAgentUnderTest.onCreate(
+ UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+ SettingsBackupAgent.SettingsBackupAllowlist allowlist =
+ new SettingsBackupAgent.SettingsBackupAllowlist(
+ new String[] {OVERRIDDEN_TEST_SETTING},
+ TEST_VALUES_VALIDATORS);
+ mAgentUnderTest.setSettingsAllowlist(allowlist);
+ mAgentUnderTest.setBlockedSettings();
+ TestSettingsHelper settingsHelper = new TestSettingsHelper(mContext);
+ mAgentUnderTest.mSettingsHelper = settingsHelper;
+
+ byte[] backupData = generateBackupData(TEST_VALUES);
+ mAgentUnderTest
+ .restoreSettings(
+ backupData,
+ /* pos= */ 0,
+ backupData.length,
+ TEST_URI,
+ /* movedToGlobal= */ null,
+ /* movedToSecure= */ null,
+ /* movedToSystem= */ Set.of(OVERRIDDEN_TEST_SETTING),
+ /* blockedSettingsArrayId= */ 0,
+ /* dynamicBlockList= */ Collections.emptySet(),
+ /* settingsToPreserve= */ Collections.emptySet(),
+ TEST_KEY);
+
+ DataTypeResult loggingResult =
+ getLoggingResultForDatatype(KEY_SYSTEM, mAgentUnderTest);
+ assertNotNull(loggingResult);
+ assertEquals(loggingResult.getSuccessCount(), 1);
+ assertNull(getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest));
+ }
+
private byte[] generateBackupData(Map<String, String> keyValueData) {
int totalBytes = 0;
for (String key : keyValueData.keySet()) {
@@ -426,7 +827,8 @@
null,
R.array.restore_blocked_global_settings,
/* dynamicBlockList= */ Collections.emptySet(),
- /* settingsToPreserve= */ Collections.emptySet());
+ /* settingsToPreserve= */ Collections.emptySet(),
+ TEST_KEY);
}
private byte[] generateUncorruptedHeader() throws IOException {
@@ -488,7 +890,7 @@
private static class TestFriendlySettingsBackupAgent extends SettingsBackupAgent {
private Boolean mForcedDeviceInfoRestoreAcceptability = null;
private String[] mBlockedSettings = null;
- private SettingsBackupWhitelist mSettingsWhitelist = null;
+ private SettingsBackupAllowlist mSettingsAllowlist = null;
void setForcedDeviceInfoRestoreAcceptability(boolean value) {
mForcedDeviceInfoRestoreAcceptability = value;
@@ -498,8 +900,8 @@
mBlockedSettings = blockedSettings;
}
- void setSettingsWhitelist(SettingsBackupWhitelist settingsWhitelist) {
- mSettingsWhitelist = settingsWhitelist;
+ void setSettingsAllowlist(SettingsBackupAllowlist settingsAllowlist) {
+ mSettingsAllowlist = settingsAllowlist;
}
@Override
@@ -517,12 +919,12 @@
}
@Override
- SettingsBackupWhitelist getBackupWhitelist(Uri contentUri) {
- if (mSettingsWhitelist == null) {
- return super.getBackupWhitelist(contentUri);
+ SettingsBackupAllowlist getBackupAllowlist(Uri contentUri) {
+ if (mSettingsAllowlist == null) {
+ return super.getBackupAllowlist(contentUri);
}
- return mSettingsWhitelist;
+ return mSettingsAllowlist;
}
void setNumberOfSettingsPerKey(String key, int numberOfSettings) {
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt
index 58b8836..9fe85b7 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt
@@ -111,22 +111,24 @@
draggable: NestedDraggable,
orientation: Orientation,
overscrollEffect: OverscrollEffect? = null,
+ enabled: Boolean = true,
): Modifier {
return this.thenIf(overscrollEffect != null) { Modifier.overscroll(overscrollEffect) }
- .then(NestedDraggableElement(draggable, orientation, overscrollEffect))
+ .then(NestedDraggableElement(draggable, orientation, overscrollEffect, enabled))
}
private data class NestedDraggableElement(
private val draggable: NestedDraggable,
private val orientation: Orientation,
private val overscrollEffect: OverscrollEffect?,
+ private val enabled: Boolean,
) : ModifierNodeElement<NestedDraggableNode>() {
override fun create(): NestedDraggableNode {
- return NestedDraggableNode(draggable, orientation, overscrollEffect)
+ return NestedDraggableNode(draggable, orientation, overscrollEffect, enabled)
}
override fun update(node: NestedDraggableNode) {
- node.update(draggable, orientation, overscrollEffect)
+ node.update(draggable, orientation, overscrollEffect, enabled)
}
}
@@ -134,6 +136,7 @@
private var draggable: NestedDraggable,
override var orientation: Orientation,
private var overscrollEffect: OverscrollEffect?,
+ private var enabled: Boolean,
) :
DelegatingNode(),
PointerInputModifierNode,
@@ -179,14 +182,22 @@
draggable: NestedDraggable,
orientation: Orientation,
overscrollEffect: OverscrollEffect?,
+ enabled: Boolean,
) {
this.draggable = draggable
this.orientation = orientation
this.overscrollEffect = overscrollEffect
+ this.enabled = enabled
trackDownPositionDelegate?.resetPointerInputHandler()
detectDragsDelegate?.resetPointerInputHandler()
nestedScrollController?.ensureOnDragStoppedIsCalled()
+
+ if (!enabled && trackDownPositionDelegate != null) {
+ check(detectDragsDelegate != null)
+ trackDownPositionDelegate = null
+ detectDragsDelegate = null
+ }
}
override fun onPointerEvent(
@@ -194,6 +205,8 @@
pass: PointerEventPass,
bounds: IntSize,
) {
+ if (!enabled) return
+
if (trackDownPositionDelegate == null) {
check(detectDragsDelegate == null)
trackDownPositionDelegate = SuspendingPointerInputModifierNode { trackDownPosition() }
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt
index f8561b8..fd3902f 100644
--- a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt
@@ -344,6 +344,45 @@
assertThat(draggable.onDragStoppedCalled).isTrue()
}
+ @Test
+ fun enabled() {
+ val draggable = TestDraggable()
+ var enabled by mutableStateOf(false)
+ val touchSlop =
+ rule.setContentWithTouchSlop {
+ Box(
+ Modifier.fillMaxSize()
+ .nestedDraggable(draggable, orientation, enabled = enabled)
+ )
+ }
+
+ assertThat(draggable.onDragStartedCalled).isFalse()
+
+ rule.onRoot().performTouchInput {
+ down(center)
+ moveBy(touchSlop.toOffset())
+ }
+
+ assertThat(draggable.onDragStartedCalled).isFalse()
+ assertThat(draggable.onDragStoppedCalled).isFalse()
+
+ enabled = true
+ rule.onRoot().performTouchInput {
+ // Release previously up finger.
+ up()
+
+ down(center)
+ moveBy(touchSlop.toOffset())
+ }
+
+ assertThat(draggable.onDragStartedCalled).isTrue()
+ assertThat(draggable.onDragStoppedCalled).isFalse()
+
+ enabled = false
+ rule.waitForIdle()
+ assertThat(draggable.onDragStoppedCalled).isTrue()
+ }
+
private fun ComposeContentTestRule.setContentWithTouchSlop(
content: @Composable () -> Unit
): Float {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
index 52c476e..e4a9888 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
@@ -33,6 +33,7 @@
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.nullable
import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -136,4 +137,13 @@
verify(wifiStateWorker, times(1)).isWifiEnabled = eq(true)
}
+
+ @Test
+ fun detailsViewModel() =
+ kosmos.testScope.runTest {
+ assertThat(underTest.detailsViewModel.getTitle())
+ .isEqualTo("Internet")
+ assertThat(underTest.detailsViewModel.getSubTitle())
+ .isEqualTo("Tab a network to connect")
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt
index 954215ee..2edb9c6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt
@@ -173,6 +173,21 @@
.isEqualTo(FakeQSTileDataInteractor.AvailabilityRequest(USER))
}
+ @Test
+ fun tileDetails() =
+ testScope.runTest {
+ assertThat(tileUserActionInteractor.detailsViewModel).isNotNull()
+ assertThat(tileUserActionInteractor.detailsViewModel?.getTitle())
+ .isEqualTo("FakeQSTileUserActionInteractor")
+ assertThat(underTest.detailsViewModel).isNotNull()
+ assertThat(underTest.detailsViewModel?.getTitle())
+ .isEqualTo("FakeQSTileUserActionInteractor")
+
+ tileUserActionInteractor.detailsViewModel = null
+ assertThat(tileUserActionInteractor.detailsViewModel).isNull()
+ assertThat(underTest.detailsViewModel).isNull()
+ }
+
private fun createViewModel(
scope: TestScope,
config: QSTileConfig = tileConfig,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplOldTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplOldTest.kt
deleted file mode 100644
index 8b4f53a..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplOldTest.kt
+++ /dev/null
@@ -1,698 +0,0 @@
-/*
- * Copyright (C) 2019 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.headsup
-
-import android.app.Notification
-import android.app.PendingIntent
-import android.app.Person
-import android.os.Handler
-import android.platform.test.annotations.DisableFlags
-import android.platform.test.annotations.EnableFlags
-import android.platform.test.flag.junit.FlagsParameterization
-import android.testing.TestableLooper.RunWithLooper
-import androidx.test.filters.SmallTest
-import com.android.internal.logging.testing.UiEventLoggerFake
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.kosmos.KosmosJavaAdapter
-import com.android.systemui.log.logcatLogBuffer
-import com.android.systemui.res.R
-import com.android.systemui.shade.domain.interactor.ShadeInteractor
-import com.android.systemui.statusbar.notification.collection.NotificationEntry
-import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
-import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManagerImpl
-import com.android.systemui.statusbar.notification.headsup.HeadsUpManagerImpl.HeadsUpEntry
-import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
-import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
-import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
-import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.concurrency.mockExecutorHandler
-import com.android.systemui.util.kotlin.JavaAdapter
-import com.android.systemui.util.settings.FakeGlobalSettings
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.flow.MutableStateFlow
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers
-import org.mockito.Mock
-import org.mockito.Mockito
-import org.mockito.invocation.InvocationOnMock
-import org.mockito.junit.MockitoJUnit
-import org.mockito.junit.MockitoRule
-import org.mockito.kotlin.eq
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4
-import platform.test.runner.parameterized.Parameters
-
-@SmallTest
-@RunWithLooper
-@RunWith(ParameterizedAndroidJunit4::class)
-// TODO(b/378142453): Merge this with HeadsUpManagerImplTest.
-open class HeadsUpManagerImplOldTest(flags: FlagsParameterization?) : SysuiTestCase() {
- protected var mKosmos: KosmosJavaAdapter = KosmosJavaAdapter(this)
-
- @JvmField @Rule var rule: MockitoRule = MockitoJUnit.rule()
-
- private val mUiEventLoggerFake = UiEventLoggerFake()
-
- private val mLogger: HeadsUpManagerLogger = Mockito.spy(HeadsUpManagerLogger(logcatLogBuffer()))
-
- @Mock private val mBgHandler: Handler? = null
-
- @Mock private val dumpManager: DumpManager? = null
-
- @Mock private val mShadeInteractor: ShadeInteractor? = null
- private var mAvalancheController: AvalancheController? = null
-
- @Mock private val mAccessibilityMgr: AccessibilityManagerWrapper? = null
-
- protected val globalSettings: FakeGlobalSettings = FakeGlobalSettings()
- protected val systemClock: FakeSystemClock = FakeSystemClock()
- protected val executor: FakeExecutor = FakeExecutor(systemClock)
-
- @Mock protected var mRow: ExpandableNotificationRow? = null
-
- private fun createHeadsUpManager(): HeadsUpManagerImpl {
- return HeadsUpManagerImpl(
- mContext,
- mLogger,
- mKosmos.statusBarStateController,
- mKosmos.keyguardBypassController,
- GroupMembershipManagerImpl(),
- mKosmos.visualStabilityProvider,
- mKosmos.configurationController,
- mockExecutorHandler(executor),
- globalSettings,
- systemClock,
- executor,
- mAccessibilityMgr,
- mUiEventLoggerFake,
- JavaAdapter(mKosmos.testScope),
- mShadeInteractor,
- mAvalancheController,
- )
- }
-
- private fun createStickyEntry(id: Int): NotificationEntry {
- val notif =
- Notification.Builder(mContext, "")
- .setSmallIcon(R.drawable.ic_person)
- .setFullScreenIntent(
- Mockito.mock(PendingIntent::class.java), /* highPriority */
- true,
- )
- .build()
- return HeadsUpManagerTestUtil.createEntry(id, notif)
- }
-
- private fun createStickyForSomeTimeEntry(id: Int): NotificationEntry {
- val notif =
- Notification.Builder(mContext, "")
- .setSmallIcon(R.drawable.ic_person)
- .setFlag(Notification.FLAG_FSI_REQUESTED_BUT_DENIED, true)
- .build()
- return HeadsUpManagerTestUtil.createEntry(id, notif)
- }
-
- private fun useAccessibilityTimeout(use: Boolean) {
- if (use) {
- Mockito.doReturn(TEST_A11Y_AUTO_DISMISS_TIME)
- .`when`(mAccessibilityMgr!!)
- .getRecommendedTimeoutMillis(ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())
- } else {
- Mockito.`when`(
- mAccessibilityMgr!!.getRecommendedTimeoutMillis(
- ArgumentMatchers.anyInt(),
- ArgumentMatchers.anyInt(),
- )
- )
- .then { i: InvocationOnMock -> i.getArgument(0) }
- }
- }
-
- init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
- }
-
- @Throws(Exception::class)
- override fun SysuiSetup() {
- super.SysuiSetup()
- mContext.getOrCreateTestableResources().apply {
- this.addOverride(R.integer.ambient_notification_extension_time, TEST_EXTENSION_TIME)
- this.addOverride(R.integer.touch_acceptance_delay, TEST_TOUCH_ACCEPTANCE_TIME)
- this.addOverride(
- R.integer.heads_up_notification_minimum_time,
- TEST_MINIMUM_DISPLAY_TIME,
- )
- this.addOverride(
- R.integer.heads_up_notification_minimum_time_with_throttling,
- TEST_MINIMUM_DISPLAY_TIME,
- )
- this.addOverride(R.integer.heads_up_notification_decay, TEST_AUTO_DISMISS_TIME)
- this.addOverride(
- R.integer.sticky_heads_up_notification_time,
- TEST_STICKY_AUTO_DISMISS_TIME,
- )
- }
-
- mAvalancheController =
- AvalancheController(dumpManager!!, mUiEventLoggerFake, mLogger, mBgHandler!!)
- Mockito.`when`(mShadeInteractor!!.isAnyExpanded).thenReturn(MutableStateFlow(true))
- Mockito.`when`(mKosmos.keyguardBypassController.bypassEnabled).thenReturn(false)
- }
-
- @Test
- fun testHasNotifications_headsUpManagerMapNotEmpty_true() {
- val bhum = createHeadsUpManager()
- val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
- bhum.showNotification(entry)
-
- Truth.assertThat(bhum.mHeadsUpEntryMap).isNotEmpty()
- Truth.assertThat(bhum.hasNotifications()).isTrue()
- }
-
- @Test
- @EnableFlags(NotificationThrottleHun.FLAG_NAME)
- fun testHasNotifications_avalancheMapNotEmpty_true() {
- val bhum = createHeadsUpManager()
- val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
- val headsUpEntry = bhum.createHeadsUpEntry(notifEntry)
- mAvalancheController!!.addToNext(headsUpEntry) {}
-
- Truth.assertThat(mAvalancheController!!.getWaitingEntryList()).isNotEmpty()
- Truth.assertThat(bhum.hasNotifications()).isTrue()
- }
-
- @Test
- @EnableFlags(NotificationThrottleHun.FLAG_NAME)
- fun testHasNotifications_false() {
- val bhum = createHeadsUpManager()
- Truth.assertThat(bhum.mHeadsUpEntryMap).isEmpty()
- Truth.assertThat(mAvalancheController!!.getWaitingEntryList()).isEmpty()
- Truth.assertThat(bhum.hasNotifications()).isFalse()
- }
-
- @Test
- @EnableFlags(NotificationThrottleHun.FLAG_NAME)
- fun testGetHeadsUpEntryList_includesAvalancheEntryList() {
- val bhum = createHeadsUpManager()
- val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
- val headsUpEntry = bhum.createHeadsUpEntry(notifEntry)
- mAvalancheController!!.addToNext(headsUpEntry) {}
-
- Truth.assertThat(bhum.headsUpEntryList).contains(headsUpEntry)
- }
-
- @Test
- @EnableFlags(NotificationThrottleHun.FLAG_NAME)
- fun testGetHeadsUpEntry_returnsAvalancheEntry() {
- val bhum = createHeadsUpManager()
- val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
- val headsUpEntry = bhum.createHeadsUpEntry(notifEntry)
- mAvalancheController!!.addToNext(headsUpEntry) {}
-
- Truth.assertThat(bhum.getHeadsUpEntry(notifEntry.key)).isEqualTo(headsUpEntry)
- }
-
- @Test
- fun testShowNotification_addsEntry() {
- val alm = createHeadsUpManager()
- val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-
- alm.showNotification(entry)
-
- assertThat(alm.isHeadsUpEntry(entry.key)).isTrue()
- assertThat(alm.hasNotifications()).isTrue()
- assertThat(alm.getEntry(entry.key)).isEqualTo(entry)
- }
-
- @Test
- fun testShowNotification_autoDismisses() {
- val alm = createHeadsUpManager()
- val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-
- alm.showNotification(entry)
- systemClock.advanceTime((TEST_AUTO_DISMISS_TIME * 3 / 2).toLong())
-
- assertThat(alm.isHeadsUpEntry(entry.key)).isFalse()
- }
-
- @Test
- fun testRemoveNotification_removeDeferred() {
- val alm = createHeadsUpManager()
- val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-
- alm.showNotification(entry)
-
- val removedImmediately =
- alm.removeNotification(entry.key, /* releaseImmediately= */ false, "removeDeferred")
- assertThat(removedImmediately).isFalse()
- assertThat(alm.isHeadsUpEntry(entry.key)).isTrue()
- }
-
- @Test
- fun testRemoveNotification_forceRemove() {
- val alm = createHeadsUpManager()
- val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-
- alm.showNotification(entry)
-
- val removedImmediately =
- alm.removeNotification(entry.key, /* releaseImmediately= */ true, "forceRemove")
- assertThat(removedImmediately).isTrue()
- assertThat(alm.isHeadsUpEntry(entry.key)).isFalse()
- }
-
- @Test
- fun testReleaseAllImmediately() {
- val alm = createHeadsUpManager()
- for (i in 0 until TEST_NUM_NOTIFICATIONS) {
- val entry = HeadsUpManagerTestUtil.createEntry(i, mContext)
- entry.row = mRow
- alm.showNotification(entry)
- }
-
- alm.releaseAllImmediately()
-
- assertThat(alm.allEntries.count()).isEqualTo(0)
- }
-
- @Test
- fun testCanRemoveImmediately_notShownLongEnough() {
- val alm = createHeadsUpManager()
- val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-
- alm.showNotification(entry)
-
- // The entry has just been added so we should not remove immediately.
- assertThat(alm.canRemoveImmediately(entry.key)).isFalse()
- }
-
- @Test
- fun testHunRemovedLogging() {
- val hum = createHeadsUpManager()
- val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
- val headsUpEntry = Mockito.mock(HeadsUpEntry::class.java)
- Mockito.`when`(headsUpEntry.pinnedStatus)
- .thenReturn(MutableStateFlow(PinnedStatus.NotPinned))
- headsUpEntry.mEntry = notifEntry
-
- hum.onEntryRemoved(headsUpEntry, "test")
-
- Mockito.verify(mLogger, Mockito.times(1)).logNotificationActuallyRemoved(eq(notifEntry))
- }
-
- @Test
- fun testShowNotification_autoDismissesIncludingTouchAcceptanceDelay() {
- val hum = createHeadsUpManager()
- val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
- useAccessibilityTimeout(false)
-
- hum.showNotification(entry)
- systemClock.advanceTime((TEST_TOUCH_ACCEPTANCE_TIME / 2 + TEST_AUTO_DISMISS_TIME).toLong())
-
- assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
- }
-
- @Test
- fun testShowNotification_autoDismissesWithDefaultTimeout() {
- val hum = createHeadsUpManager()
- val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
- useAccessibilityTimeout(false)
-
- hum.showNotification(entry)
- systemClock.advanceTime(
- (TEST_TOUCH_ACCEPTANCE_TIME +
- (TEST_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2)
- .toLong()
- )
-
- assertThat(hum.isHeadsUpEntry(entry.key)).isFalse()
- }
-
- @Test
- fun testShowNotification_stickyForSomeTime_autoDismissesWithStickyTimeout() {
- val hum = createHeadsUpManager()
- val entry = createStickyForSomeTimeEntry(/* id= */ 0)
- useAccessibilityTimeout(false)
-
- hum.showNotification(entry)
- systemClock.advanceTime(
- (TEST_TOUCH_ACCEPTANCE_TIME +
- (TEST_AUTO_DISMISS_TIME + TEST_STICKY_AUTO_DISMISS_TIME) / 2)
- .toLong()
- )
-
- assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
- }
-
- @Test
- fun testShowNotification_sticky_neverAutoDismisses() {
- val hum = createHeadsUpManager()
- val entry = createStickyEntry(/* id= */ 0)
- useAccessibilityTimeout(false)
-
- hum.showNotification(entry)
- systemClock.advanceTime(
- (TEST_TOUCH_ACCEPTANCE_TIME + 2 * TEST_A11Y_AUTO_DISMISS_TIME).toLong()
- )
-
- assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
- }
-
- @Test
- fun testShowNotification_autoDismissesWithAccessibilityTimeout() {
- val hum = createHeadsUpManager()
- val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
- useAccessibilityTimeout(true)
-
- hum.showNotification(entry)
- systemClock.advanceTime(
- (TEST_TOUCH_ACCEPTANCE_TIME +
- (TEST_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2)
- .toLong()
- )
-
- assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
- }
-
- @Test
- fun testShowNotification_stickyForSomeTime_autoDismissesWithAccessibilityTimeout() {
- val hum = createHeadsUpManager()
- val entry = createStickyForSomeTimeEntry(/* id= */ 0)
- useAccessibilityTimeout(true)
-
- hum.showNotification(entry)
- systemClock.advanceTime(
- (TEST_TOUCH_ACCEPTANCE_TIME +
- (TEST_STICKY_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2)
- .toLong()
- )
-
- assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
- }
-
- @Test
- fun testRemoveNotification_beforeMinimumDisplayTime() {
- val hum = createHeadsUpManager()
- val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
- useAccessibilityTimeout(false)
-
- hum.showNotification(entry)
-
- val removedImmediately =
- hum.removeNotification(
- entry.key,
- /* releaseImmediately = */ false,
- "beforeMinimumDisplayTime",
- )
- assertThat(removedImmediately).isFalse()
- assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
-
- systemClock.advanceTime(((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2).toLong())
-
- assertThat(hum.isHeadsUpEntry(entry.key)).isFalse()
- }
-
- @Test
- fun testRemoveNotification_afterMinimumDisplayTime() {
- val hum = createHeadsUpManager()
- val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
- useAccessibilityTimeout(false)
-
- hum.showNotification(entry)
- systemClock.advanceTime(((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2).toLong())
-
- assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
-
- val removedImmediately =
- hum.removeNotification(
- entry.key,
- /* releaseImmediately = */ false,
- "afterMinimumDisplayTime",
- )
- assertThat(removedImmediately).isTrue()
- assertThat(hum.isHeadsUpEntry(entry.key)).isFalse()
- }
-
- @Test
- fun testRemoveNotification_releaseImmediately() {
- val hum = createHeadsUpManager()
- val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-
- hum.showNotification(entry)
-
- val removedImmediately =
- hum.removeNotification(
- entry.key,
- /* releaseImmediately = */ true,
- "afterMinimumDisplayTime",
- )
- assertThat(removedImmediately).isTrue()
- assertThat(hum.isHeadsUpEntry(entry.key)).isFalse()
- }
-
- @Test
- fun testIsSticky_rowPinnedAndExpanded_true() {
- val hum = createHeadsUpManager()
- val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
- Mockito.`when`(mRow!!.isPinned).thenReturn(true)
- notifEntry.row = mRow
-
- hum.showNotification(notifEntry)
-
- val headsUpEntry = hum.getHeadsUpEntry(notifEntry.key)
- headsUpEntry!!.setExpanded(true)
-
- assertThat(hum.isSticky(notifEntry.key)).isTrue()
- }
-
- @Test
- fun testIsSticky_remoteInputActive_true() {
- val hum = createHeadsUpManager()
- val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-
- hum.showNotification(notifEntry)
-
- val headsUpEntry = hum.getHeadsUpEntry(notifEntry.key)
- headsUpEntry!!.mRemoteInputActive = true
-
- assertThat(hum.isSticky(notifEntry.key)).isTrue()
- }
-
- @Test
- fun testIsSticky_hasFullScreenIntent_true() {
- val hum = createHeadsUpManager()
- val notifEntry = HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id= */ 0, mContext)
-
- hum.showNotification(notifEntry)
-
- assertThat(hum.isSticky(notifEntry.key)).isTrue()
- }
-
- @Test
- fun testIsSticky_stickyForSomeTime_false() {
- val hum = createHeadsUpManager()
- val entry = createStickyForSomeTimeEntry(/* id= */ 0)
-
- hum.showNotification(entry)
-
- assertThat(hum.isSticky(entry.key)).isFalse()
- }
-
- @Test
- fun testIsSticky_false() {
- val hum = createHeadsUpManager()
- val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-
- hum.showNotification(notifEntry)
-
- val headsUpEntry = hum.getHeadsUpEntry(notifEntry.key)
- headsUpEntry!!.setExpanded(false)
- headsUpEntry.mRemoteInputActive = false
-
- assertThat(hum.isSticky(notifEntry.key)).isFalse()
- }
-
- @Test
- fun testCompareTo_withNullEntries() {
- val hum = createHeadsUpManager()
- val alertEntry = NotificationEntryBuilder().setTag("alert").build()
-
- hum.showNotification(alertEntry)
-
- assertThat(hum.compare(alertEntry, null)).isLessThan(0)
- assertThat(hum.compare(null, alertEntry)).isGreaterThan(0)
- assertThat(hum.compare(null, null)).isEqualTo(0)
- }
-
- @Test
- fun testCompareTo_withNonAlertEntries() {
- val hum = createHeadsUpManager()
-
- val nonAlertEntry1 = NotificationEntryBuilder().setTag("nae1").build()
- val nonAlertEntry2 = NotificationEntryBuilder().setTag("nae2").build()
- val alertEntry = NotificationEntryBuilder().setTag("alert").build()
- hum.showNotification(alertEntry)
-
- assertThat(hum.compare(alertEntry, nonAlertEntry1)).isLessThan(0)
- assertThat(hum.compare(nonAlertEntry1, alertEntry)).isGreaterThan(0)
- assertThat(hum.compare(nonAlertEntry1, nonAlertEntry2)).isEqualTo(0)
- }
-
- @Test
- fun testAlertEntryCompareTo_ongoingCallLessThanActiveRemoteInput() {
- val hum = createHeadsUpManager()
-
- val ongoingCall =
- hum.HeadsUpEntry(
- NotificationEntryBuilder()
- .setSbn(
- HeadsUpManagerTestUtil.createSbn(
- /* id = */ 0,
- Notification.Builder(mContext, "")
- .setCategory(Notification.CATEGORY_CALL)
- .setOngoing(true),
- )
- )
- .build()
- )
-
- val activeRemoteInput =
- hum.HeadsUpEntry(HeadsUpManagerTestUtil.createEntry(/* id= */ 1, mContext))
- activeRemoteInput.mRemoteInputActive = true
-
- assertThat(ongoingCall.compareTo(activeRemoteInput)).isLessThan(0)
- assertThat(activeRemoteInput.compareTo(ongoingCall)).isGreaterThan(0)
- }
-
- @Test
- fun testAlertEntryCompareTo_incomingCallLessThanActiveRemoteInput() {
- val hum = createHeadsUpManager()
-
- val person = Person.Builder().setName("person").build()
- val intent = Mockito.mock(PendingIntent::class.java)
- val incomingCall =
- hum.HeadsUpEntry(
- NotificationEntryBuilder()
- .setSbn(
- HeadsUpManagerTestUtil.createSbn(
- /* id = */ 0,
- Notification.Builder(mContext, "")
- .setStyle(
- Notification.CallStyle.forIncomingCall(person, intent, intent)
- ),
- )
- )
- .build()
- )
-
- val activeRemoteInput =
- hum.HeadsUpEntry(HeadsUpManagerTestUtil.createEntry(/* id= */ 1, mContext))
- activeRemoteInput.mRemoteInputActive = true
-
- assertThat(incomingCall.compareTo(activeRemoteInput)).isLessThan(0)
- assertThat(activeRemoteInput.compareTo(incomingCall)).isGreaterThan(0)
- }
-
- @Test
- @EnableFlags(NotificationThrottleHun.FLAG_NAME)
- fun testPinEntry_logsPeek_throttleEnabled() {
- val hum = createHeadsUpManager()
-
- // Needs full screen intent in order to be pinned
- val entryToPin =
- hum.HeadsUpEntry(
- HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id= */ 0, mContext)
- )
-
- // Note: the standard way to show a notification would be calling showNotification rather
- // than onAlertEntryAdded. However, in practice showNotification in effect adds
- // the notification and then updates it; in order to not log twice, the entry needs
- // to have a functional ExpandableNotificationRow that can keep track of whether it's
- // pinned or not (via isRowPinned()). That feels like a lot to pull in to test this one bit.
- hum.onEntryAdded(entryToPin)
-
- assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(2)
- assertThat(AvalancheController.ThrottleEvent.AVALANCHE_THROTTLING_HUN_SHOWN.getId())
- .isEqualTo(mUiEventLoggerFake.eventId(0))
- assertThat(HeadsUpManagerImpl.NotificationPeekEvent.NOTIFICATION_PEEK.id)
- .isEqualTo(mUiEventLoggerFake.eventId(1))
- }
-
- @Test
- @DisableFlags(NotificationThrottleHun.FLAG_NAME)
- fun testPinEntry_logsPeek_throttleDisabled() {
- val hum = createHeadsUpManager()
-
- // Needs full screen intent in order to be pinned
- val entryToPin =
- hum.HeadsUpEntry(
- HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id= */ 0, mContext)
- )
-
- // Note: the standard way to show a notification would be calling showNotification rather
- // than onAlertEntryAdded. However, in practice showNotification in effect adds
- // the notification and then updates it; in order to not log twice, the entry needs
- // to have a functional ExpandableNotificationRow that can keep track of whether it's
- // pinned or not (via isRowPinned()). That feels like a lot to pull in to test this one bit.
- hum.onEntryAdded(entryToPin)
-
- assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(1)
- assertThat(HeadsUpManagerImpl.NotificationPeekEvent.NOTIFICATION_PEEK.id)
- .isEqualTo(mUiEventLoggerFake.eventId(0))
- }
-
- @Test
- fun testSetUserActionMayIndirectlyRemove() {
- val hum = createHeadsUpManager()
- val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-
- hum.showNotification(notifEntry)
-
- assertThat(hum.canRemoveImmediately(notifEntry.key)).isFalse()
-
- hum.setUserActionMayIndirectlyRemove(notifEntry)
-
- assertThat(hum.canRemoveImmediately(notifEntry.key)).isTrue()
- }
-
- companion object {
- const val TEST_TOUCH_ACCEPTANCE_TIME: Int = 200
- const val TEST_A11Y_AUTO_DISMISS_TIME: Int = 1000
- const val TEST_EXTENSION_TIME = 500
-
- const val TEST_MINIMUM_DISPLAY_TIME: Int = 400
- const val TEST_AUTO_DISMISS_TIME: Int = 600
- const val TEST_STICKY_AUTO_DISMISS_TIME: Int = 800
-
- // Number of notifications to use in tests requiring multiple notifications
- private const val TEST_NUM_NOTIFICATIONS = 4
-
- init {
- Truth.assertThat(TEST_MINIMUM_DISPLAY_TIME).isLessThan(TEST_AUTO_DISMISS_TIME)
- Truth.assertThat(TEST_AUTO_DISMISS_TIME).isLessThan(TEST_STICKY_AUTO_DISMISS_TIME)
- Truth.assertThat(TEST_STICKY_AUTO_DISMISS_TIME).isLessThan(TEST_A11Y_AUTO_DISMISS_TIME)
- }
-
- @get:Parameters(name = "{0}")
- @JvmStatic
- val flags: List<FlagsParameterization>
- get() = FlagsParameterization.allCombinationsOf(NotificationThrottleHun.FLAG_NAME)
- }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
index a5fecb8..8420c49 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
@@ -15,25 +15,38 @@
*/
package com.android.systemui.statusbar.notification.headsup
+import android.app.Notification
+import android.app.PendingIntent
+import android.app.Person
import android.os.Handler
+import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
+import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper
+import android.view.accessibility.accessibilityManager
import android.view.accessibility.accessibilityManagerWrapper
import androidx.test.filters.SmallTest
import com.android.internal.logging.uiEventLoggerFake
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.dump.dumpManager
+import com.android.systemui.flags.BrokenWithSceneContainer
import com.android.systemui.flags.andSceneContainer
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
-import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.res.R
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.provider.visualStabilityProvider
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager
+import com.android.systemui.statusbar.notification.headsup.HeadsUpManagerImpl.HeadsUpEntry
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper
import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
import com.android.systemui.statusbar.phone.keyguardBypassController
import com.android.systemui.statusbar.policy.configurationController
@@ -41,12 +54,19 @@
import com.android.systemui.testKosmos
import com.android.systemui.util.concurrency.mockExecutorHandler
import com.android.systemui.util.kotlin.JavaAdapter
+import com.android.systemui.util.settings.fakeGlobalSettings
+import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters
@@ -54,19 +74,26 @@
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
@RunWithLooper
-class HeadsUpManagerImplTest(flags: FlagsParameterization) : HeadsUpManagerImplOldTest(flags) {
-
- private val headsUpManagerLogger = HeadsUpManagerLogger(logcatLogBuffer())
+class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() {
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
private val kosmos = testKosmos().useUnconfinedTestDispatcher()
private val testScope = kosmos.testScope
private val groupManager = mock<GroupMembershipManager>()
private val bgHandler = mock<Handler>()
+ private val headsUpManagerLogger = mock<HeadsUpManagerLogger>()
val statusBarStateController = kosmos.sysuiStatusBarStateController
+ private val globalSettings = kosmos.fakeGlobalSettings
+ private val systemClock = kosmos.fakeSystemClock
+ private val executor = kosmos.fakeExecutor
+ private val uiEventLoggerFake = kosmos.uiEventLoggerFake
private val javaAdapter: JavaAdapter = JavaAdapter(testScope.backgroundScope)
+ private lateinit var testHelper: NotificationTestHelper
private lateinit var avalancheController: AvalancheController
private lateinit var underTest: HeadsUpManagerImpl
@@ -90,12 +117,15 @@
)
}
+ allowTestableLooperAsMainThread()
+ testHelper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+
whenever(kosmos.keyguardBypassController.bypassEnabled).thenReturn(false)
kosmos.visualStabilityProvider.isReorderingAllowed = true
avalancheController =
AvalancheController(
kosmos.dumpManager,
- kosmos.uiEventLoggerFake,
+ uiEventLoggerFake,
headsUpManagerLogger,
bgHandler,
)
@@ -113,7 +143,7 @@
systemClock,
executor,
kosmos.accessibilityManagerWrapper,
- kosmos.uiEventLoggerFake,
+ uiEventLoggerFake,
javaAdapter,
kosmos.shadeInteractor,
avalancheController,
@@ -121,6 +151,220 @@
}
@Test
+ fun testHasNotifications_headsUpManagerMapNotEmpty_true() {
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ underTest.showNotification(entry)
+
+ assertThat(underTest.mHeadsUpEntryMap).isNotEmpty()
+ assertThat(underTest.hasNotifications()).isTrue()
+ }
+
+ @Test
+ @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+ fun testHasNotifications_avalancheMapNotEmpty_true() {
+ val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ val headsUpEntry = underTest.createHeadsUpEntry(notifEntry)
+ avalancheController.addToNext(headsUpEntry) {}
+
+ assertThat(avalancheController.getWaitingEntryList()).isNotEmpty()
+ assertThat(underTest.hasNotifications()).isTrue()
+ }
+
+ @Test
+ @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+ fun testHasNotifications_false() {
+ assertThat(underTest.mHeadsUpEntryMap).isEmpty()
+ assertThat(avalancheController.getWaitingEntryList()).isEmpty()
+ assertThat(underTest.hasNotifications()).isFalse()
+ }
+
+ @Test
+ @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+ fun testGetHeadsUpEntryList_includesAvalancheEntryList() {
+ val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ val headsUpEntry = underTest.createHeadsUpEntry(notifEntry)
+ avalancheController.addToNext(headsUpEntry) {}
+
+ assertThat(underTest.headsUpEntryList).contains(headsUpEntry)
+ }
+
+ @Test
+ @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+ fun testGetHeadsUpEntry_returnsAvalancheEntry() {
+ val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ val headsUpEntry = underTest.createHeadsUpEntry(notifEntry)
+ avalancheController.addToNext(headsUpEntry) {}
+
+ assertThat(underTest.getHeadsUpEntry(notifEntry.key)).isEqualTo(headsUpEntry)
+ }
+
+ @Test
+ fun testShowNotification_addsEntry() {
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+ underTest.showNotification(entry)
+
+ assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
+ assertThat(underTest.hasNotifications()).isTrue()
+ assertThat(underTest.getEntry(entry.key)).isEqualTo(entry)
+ }
+
+ @Test
+ fun testShowNotification_autoDismisses() {
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+ underTest.showNotification(entry)
+ systemClock.advanceTime((TEST_AUTO_DISMISS_TIME * 3 / 2).toLong())
+
+ assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse()
+ }
+
+ @Test
+ fun testRemoveNotification_removeDeferred() {
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+ underTest.showNotification(entry)
+
+ val removedImmediately =
+ underTest.removeNotification(
+ entry.key,
+ /* releaseImmediately= */ false,
+ "removeDeferred",
+ )
+ assertThat(removedImmediately).isFalse()
+ assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
+ }
+
+ @Test
+ fun testRemoveNotification_forceRemove() {
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+ underTest.showNotification(entry)
+
+ val removedImmediately =
+ underTest.removeNotification(entry.key, /* releaseImmediately= */ true, "forceRemove")
+ assertThat(removedImmediately).isTrue()
+ assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse()
+ }
+
+ @Test
+ fun testReleaseAllImmediately() {
+ for (i in 0 until 4) {
+ val entry = HeadsUpManagerTestUtil.createEntry(i, mContext)
+ entry.row = mock<ExpandableNotificationRow>()
+ underTest.showNotification(entry)
+ }
+
+ underTest.releaseAllImmediately()
+
+ assertThat(underTest.allEntries.count()).isEqualTo(0)
+ }
+
+ @Test
+ fun testCanRemoveImmediately_notShownLongEnough() {
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+ underTest.showNotification(entry)
+
+ // The entry has just been added so we should not remove immediately.
+ assertThat(underTest.canRemoveImmediately(entry.key)).isFalse()
+ }
+
+ @Test
+ fun testHunRemovedLogging() {
+ val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ val headsUpEntry = underTest.HeadsUpEntry(notifEntry)
+ headsUpEntry.setRowPinnedStatus(PinnedStatus.NotPinned)
+
+ underTest.onEntryRemoved(headsUpEntry, "test")
+
+ verify(headsUpManagerLogger, times(1)).logNotificationActuallyRemoved(eq(notifEntry))
+ }
+
+ @Test
+ fun testShowNotification_autoDismissesIncludingTouchAcceptanceDelay() {
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ useAccessibilityTimeout(false)
+
+ underTest.showNotification(entry)
+ systemClock.advanceTime((TEST_TOUCH_ACCEPTANCE_TIME / 2 + TEST_AUTO_DISMISS_TIME).toLong())
+
+ assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
+ }
+
+ @Test
+ fun testShowNotification_autoDismissesWithDefaultTimeout() {
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ useAccessibilityTimeout(false)
+
+ underTest.showNotification(entry)
+ systemClock.advanceTime(
+ (TEST_TOUCH_ACCEPTANCE_TIME +
+ (TEST_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2)
+ .toLong()
+ )
+
+ assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse()
+ }
+
+ @Test
+ fun testRemoveNotification_beforeMinimumDisplayTime() {
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ useAccessibilityTimeout(false)
+
+ underTest.showNotification(entry)
+
+ val removedImmediately =
+ underTest.removeNotification(
+ entry.key,
+ /* releaseImmediately = */ false,
+ "beforeMinimumDisplayTime",
+ )
+ assertThat(removedImmediately).isFalse()
+ assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
+
+ systemClock.advanceTime(((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2).toLong())
+
+ assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse()
+ }
+
+ @Test
+ fun testRemoveNotification_afterMinimumDisplayTime() {
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ useAccessibilityTimeout(false)
+
+ underTest.showNotification(entry)
+ systemClock.advanceTime(((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2).toLong())
+
+ assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
+
+ val removedImmediately =
+ underTest.removeNotification(
+ entry.key,
+ /* releaseImmediately = */ false,
+ "afterMinimumDisplayTime",
+ )
+ assertThat(removedImmediately).isTrue()
+ assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse()
+ }
+
+ @Test
+ fun testRemoveNotification_releaseImmediately() {
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+ underTest.showNotification(entry)
+
+ val removedImmediately =
+ underTest.removeNotification(
+ entry.key,
+ /* releaseImmediately = */ true,
+ "afterMinimumDisplayTime",
+ )
+ assertThat(removedImmediately).isTrue()
+ assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse()
+ }
+
+ @Test
fun testSnooze() {
val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
underTest.showNotification(entry)
@@ -160,7 +404,7 @@
fun testCanRemoveImmediately_notTopEntry() {
val earlierEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
val laterEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 1, mContext)
- laterEntry.row = mRow
+ laterEntry.row = mock<ExpandableNotificationRow>()
underTest.showNotification(earlierEntry)
underTest.showNotification(laterEntry)
@@ -226,6 +470,122 @@
}
@Test
+ fun testShowNotification_sticky_neverAutoDismisses() {
+ val entry = createStickyEntry(id = 0)
+ useAccessibilityTimeout(false)
+
+ underTest.showNotification(entry)
+ systemClock.advanceTime(
+ (TEST_TOUCH_ACCEPTANCE_TIME + 2 * TEST_A11Y_AUTO_DISMISS_TIME).toLong()
+ )
+
+ assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
+ }
+
+ @Test
+ fun testShowNotification_autoDismissesWithAccessibilityTimeout() {
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ useAccessibilityTimeout(true)
+
+ underTest.showNotification(entry)
+ systemClock.advanceTime(
+ (TEST_TOUCH_ACCEPTANCE_TIME +
+ (TEST_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2)
+ .toLong()
+ )
+
+ assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
+ }
+
+ @Test
+ fun testShowNotification_stickyForSomeTime_autoDismissesWithStickyTimeout() {
+ val entry = createStickyForSomeTimeEntry(id = 0)
+ useAccessibilityTimeout(false)
+
+ underTest.showNotification(entry)
+ systemClock.advanceTime(
+ (TEST_TOUCH_ACCEPTANCE_TIME +
+ (TEST_AUTO_DISMISS_TIME + TEST_STICKY_AUTO_DISMISS_TIME) / 2)
+ .toLong()
+ )
+
+ assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
+ }
+
+ @Test
+ fun testShowNotification_stickyForSomeTime_autoDismissesWithAccessibilityTimeout() {
+ val entry = createStickyForSomeTimeEntry(id = 0)
+ useAccessibilityTimeout(true)
+
+ underTest.showNotification(entry)
+ systemClock.advanceTime(
+ (TEST_TOUCH_ACCEPTANCE_TIME +
+ (TEST_STICKY_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2)
+ .toLong()
+ )
+
+ assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
+ }
+
+ @Test
+ fun testIsSticky_rowPinnedAndExpanded_true() {
+ val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ val row = testHelper.createRow()
+ row.setPinnedStatus(PinnedStatus.PinnedBySystem)
+ notifEntry.row = row
+
+ underTest.showNotification(notifEntry)
+
+ val headsUpEntry = underTest.getHeadsUpEntry(notifEntry.key)
+ headsUpEntry!!.setExpanded(true)
+
+ assertThat(underTest.isSticky(notifEntry.key)).isTrue()
+ }
+
+ @Test
+ fun testIsSticky_remoteInputActive_true() {
+ val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+ underTest.showNotification(notifEntry)
+
+ val headsUpEntry = underTest.getHeadsUpEntry(notifEntry.key)
+ headsUpEntry!!.mRemoteInputActive = true
+
+ assertThat(underTest.isSticky(notifEntry.key)).isTrue()
+ }
+
+ @Test
+ fun testIsSticky_hasFullScreenIntent_true() {
+ val notifEntry = HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id= */ 0, mContext)
+
+ underTest.showNotification(notifEntry)
+
+ assertThat(underTest.isSticky(notifEntry.key)).isTrue()
+ }
+
+ @Test
+ fun testIsSticky_stickyForSomeTime_false() {
+ val entry = createStickyForSomeTimeEntry(id = 0)
+
+ underTest.showNotification(entry)
+
+ assertThat(underTest.isSticky(entry.key)).isFalse()
+ }
+
+ @Test
+ fun testIsSticky_false() {
+ val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+ underTest.showNotification(notifEntry)
+
+ val headsUpEntry = underTest.getHeadsUpEntry(notifEntry.key)
+ headsUpEntry!!.setExpanded(false)
+ headsUpEntry.mRemoteInputActive = false
+
+ assertThat(underTest.isSticky(notifEntry.key)).isFalse()
+ }
+
+ @Test
fun testShouldHeadsUpBecomePinned_noFSI_false() =
kosmos.runTest {
statusBarStateController.setState(StatusBarState.KEYGUARD)
@@ -270,11 +630,13 @@
}
@Test
+ @BrokenWithSceneContainer(381869885) // because `ShadeTestUtil.setShadeExpansion(0f)`
+ // still causes `ShadeInteractor.isAnyExpanded` to emit `true`, when it should emit `false`.
fun shouldHeadsUpBecomePinned_shadeNotExpanded_true() =
kosmos.runTest {
// GIVEN
- shadeTestUtil.setShadeExpansion(0f)
- // TODO(b/381869885): Determine why we need both of these ShadeTestUtil calls.
+ // TODO(b/381869885): We should be able to use `ShadeTestUtil.setShadeExpansion(0f)`
+ // instead.
shadeTestUtil.setLegacyExpandedOrAwaitingInputTransfer(false)
val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
@@ -347,8 +709,183 @@
assertThat(underTest.shouldHeadsUpBecomePinned(entry)).isFalse()
}
+ @Test
+ fun testCompareTo_withNullEntries() {
+ val alertEntry = NotificationEntryBuilder().setTag("alert").build()
+
+ underTest.showNotification(alertEntry)
+
+ assertThat(underTest.compare(alertEntry, null)).isLessThan(0)
+ assertThat(underTest.compare(null, alertEntry)).isGreaterThan(0)
+ assertThat(underTest.compare(null, null)).isEqualTo(0)
+ }
+
+ @Test
+ fun testCompareTo_withNonAlertEntries() {
+ val nonAlertEntry1 = NotificationEntryBuilder().setTag("nae1").build()
+ val nonAlertEntry2 = NotificationEntryBuilder().setTag("nae2").build()
+ val alertEntry = NotificationEntryBuilder().setTag("alert").build()
+ underTest.showNotification(alertEntry)
+
+ assertThat(underTest.compare(alertEntry, nonAlertEntry1)).isLessThan(0)
+ assertThat(underTest.compare(nonAlertEntry1, alertEntry)).isGreaterThan(0)
+ assertThat(underTest.compare(nonAlertEntry1, nonAlertEntry2)).isEqualTo(0)
+ }
+
+ @Test
+ fun testAlertEntryCompareTo_ongoingCallLessThanActiveRemoteInput() {
+ val ongoingCall =
+ underTest.HeadsUpEntry(
+ NotificationEntryBuilder()
+ .setSbn(
+ HeadsUpManagerTestUtil.createSbn(
+ /* id = */ 0,
+ Notification.Builder(mContext, "")
+ .setCategory(Notification.CATEGORY_CALL)
+ .setOngoing(true),
+ )
+ )
+ .build()
+ )
+
+ val activeRemoteInput =
+ underTest.HeadsUpEntry(HeadsUpManagerTestUtil.createEntry(/* id= */ 1, mContext))
+ activeRemoteInput.mRemoteInputActive = true
+
+ assertThat(ongoingCall.compareTo(activeRemoteInput)).isLessThan(0)
+ assertThat(activeRemoteInput.compareTo(ongoingCall)).isGreaterThan(0)
+ }
+
+ @Test
+ fun testAlertEntryCompareTo_incomingCallLessThanActiveRemoteInput() {
+ val person = Person.Builder().setName("person").build()
+ val intent = mock<PendingIntent>()
+ val incomingCall =
+ underTest.HeadsUpEntry(
+ NotificationEntryBuilder()
+ .setSbn(
+ HeadsUpManagerTestUtil.createSbn(
+ /* id = */ 0,
+ Notification.Builder(mContext, "")
+ .setStyle(
+ Notification.CallStyle.forIncomingCall(person, intent, intent)
+ ),
+ )
+ )
+ .build()
+ )
+
+ val activeRemoteInput =
+ underTest.HeadsUpEntry(HeadsUpManagerTestUtil.createEntry(/* id= */ 1, mContext))
+ activeRemoteInput.mRemoteInputActive = true
+
+ assertThat(incomingCall.compareTo(activeRemoteInput)).isLessThan(0)
+ assertThat(activeRemoteInput.compareTo(incomingCall)).isGreaterThan(0)
+ }
+
+ @Test
+ @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+ fun testPinEntry_logsPeek_throttleEnabled() {
+ // Needs full screen intent in order to be pinned
+ val entryToPin =
+ underTest.HeadsUpEntry(
+ HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id= */ 0, mContext)
+ )
+
+ // Note: the standard way to show a notification would be calling showNotification rather
+ // than onAlertEntryAdded. However, in practice showNotification in effect adds
+ // the notification and then updates it; in order to not log twice, the entry needs
+ // to have a functional ExpandableNotificationRow that can keep track of whether it's
+ // pinned or not (via isRowPinned()). That feels like a lot to pull in to test this one bit.
+ underTest.onEntryAdded(entryToPin)
+
+ assertThat(uiEventLoggerFake.numLogs()).isEqualTo(2)
+ assertThat(AvalancheController.ThrottleEvent.AVALANCHE_THROTTLING_HUN_SHOWN.getId())
+ .isEqualTo(uiEventLoggerFake.eventId(0))
+ assertThat(HeadsUpManagerImpl.NotificationPeekEvent.NOTIFICATION_PEEK.id)
+ .isEqualTo(uiEventLoggerFake.eventId(1))
+ }
+
+ @Test
+ @DisableFlags(NotificationThrottleHun.FLAG_NAME)
+ fun testPinEntry_logsPeek_throttleDisabled() {
+ // Needs full screen intent in order to be pinned
+ val entryToPin =
+ underTest.HeadsUpEntry(
+ HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id= */ 0, mContext)
+ )
+
+ // Note: the standard way to show a notification would be calling showNotification rather
+ // than onAlertEntryAdded. However, in practice showNotification in effect adds
+ // the notification and then updates it; in order to not log twice, the entry needs
+ // to have a functional ExpandableNotificationRow that can keep track of whether it's
+ // pinned or not (via isRowPinned()). That feels like a lot to pull in to test this one bit.
+ underTest.onEntryAdded(entryToPin)
+
+ assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
+ assertThat(HeadsUpManagerImpl.NotificationPeekEvent.NOTIFICATION_PEEK.id)
+ .isEqualTo(uiEventLoggerFake.eventId(0))
+ }
+
+ @Test
+ fun testSetUserActionMayIndirectlyRemove() {
+ val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+ underTest.showNotification(notifEntry)
+
+ assertThat(underTest.canRemoveImmediately(notifEntry.key)).isFalse()
+
+ underTest.setUserActionMayIndirectlyRemove(notifEntry)
+
+ assertThat(underTest.canRemoveImmediately(notifEntry.key)).isTrue()
+ }
+
+ private fun createStickyEntry(id: Int): NotificationEntry {
+ val notif =
+ Notification.Builder(mContext, "")
+ .setSmallIcon(R.drawable.ic_person)
+ .setFullScreenIntent(mock<PendingIntent>(), /* highPriority= */ true)
+ .build()
+ return HeadsUpManagerTestUtil.createEntry(id, notif)
+ }
+
+ private fun createStickyForSomeTimeEntry(id: Int): NotificationEntry {
+ val notif =
+ Notification.Builder(mContext, "")
+ .setSmallIcon(R.drawable.ic_person)
+ .setFlag(Notification.FLAG_FSI_REQUESTED_BUT_DENIED, true)
+ .build()
+ return HeadsUpManagerTestUtil.createEntry(id, notif)
+ }
+
+ private fun useAccessibilityTimeout(use: Boolean) {
+ if (use) {
+ whenever(kosmos.accessibilityManager.getRecommendedTimeoutMillis(any(), any()))
+ .thenReturn(TEST_A11Y_AUTO_DISMISS_TIME)
+ } else {
+ doAnswer { it.getArgument(0) as Int }
+ .whenever(kosmos.accessibilityManager)
+ .getRecommendedTimeoutMillis(any(), any())
+ }
+ }
+
companion object {
+ const val TEST_TOUCH_ACCEPTANCE_TIME = 200
+ const val TEST_A11Y_AUTO_DISMISS_TIME = 1000
+ const val TEST_EXTENSION_TIME = 500
+
+ const val TEST_MINIMUM_DISPLAY_TIME = 400
+ const val TEST_AUTO_DISMISS_TIME = 600
+ const val TEST_STICKY_AUTO_DISMISS_TIME = 800
+
+ init {
+ assertThat(TEST_MINIMUM_DISPLAY_TIME).isLessThan(TEST_AUTO_DISMISS_TIME)
+ assertThat(TEST_AUTO_DISMISS_TIME).isLessThan(TEST_STICKY_AUTO_DISMISS_TIME)
+ assertThat(TEST_STICKY_AUTO_DISMISS_TIME).isLessThan(TEST_A11Y_AUTO_DISMISS_TIME)
+ }
+
@get:Parameters(name = "{0}")
+ @JvmStatic
val flags: List<FlagsParameterization>
get() = buildList {
addAll(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
index c0a206a..9ad2315 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
@@ -49,11 +49,7 @@
private val dispatcher = StandardTestDispatcher()
private val testScope = TestScope(dispatcher)
- private val iconsInteractor =
- FakeMobileIconsInteractor(
- FakeMobileMappingsProxy(),
- mock(),
- )
+ private val iconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
private val repo = FakeDeviceBasedSatelliteRepository()
private val connectivityRepository = FakeConnectivityRepository()
@@ -515,7 +511,7 @@
// GIVEN, 2 connection
val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
- val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+ val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(2)
// WHEN all connections are NOT OOS.
i1.isInService.value = true
@@ -547,7 +543,7 @@
// GIVEN a condition that should return true (all conections OOS)
val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
- val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+ val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(2)
i1.isInService.value = true
i2.isInService.value = true
@@ -579,4 +575,40 @@
// THEN the interactor returns true due to the wifi network being active
assertThat(latest).isTrue()
}
+
+ @Test
+ @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ fun isAnyConnectionNtn_trueWhenAnyNtn() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.isAnyConnectionNtn)
+
+ // GIVEN, 2 connection
+ val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+ val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(2)
+
+ // WHEN at least one connection is using ntn
+ i1.isNonTerrestrial.value = true
+ i2.isNonTerrestrial.value = false
+
+ // THEN the value is propagated to this interactor
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ fun isAnyConnectionNtn_falseWhenNoNtn() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.isAnyConnectionNtn)
+
+ // GIVEN, 2 connection
+ val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+ val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(2)
+
+ // WHEN at no connection is using ntn
+ i1.isNonTerrestrial.value = false
+ i2.isNonTerrestrial.value = false
+
+ // THEN the value is propagated to this interactor
+ assertThat(latest).isFalse()
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
index 509aa7a..fe5b56a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
@@ -327,10 +327,11 @@
// GIVEN satellite is allowed
repo.isSatelliteAllowedForCurrentLocation.value = true
- // GIVEN all icons are OOS
+ // GIVEN all icons are OOS and not ntn
val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
i1.isInService.value = false
i1.isEmergencyOnly.value = false
+ i1.isNonTerrestrial.value = false
// GIVEN apm is disabled
airplaneModeRepository.setIsAirplaneMode(false)
@@ -344,6 +345,29 @@
}
@Test
+ fun icon_nullWhenConnected_mobileNtnConnectionExists() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.icon)
+
+ // GIVEN satellite is allowed
+ repo.isSatelliteAllowedForCurrentLocation.value = true
+
+ // GIVEN ntn connection exists
+ val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+ i1.isNonTerrestrial.value = true
+
+ // GIVEN apm is disabled
+ airplaneModeRepository.setIsAirplaneMode(false)
+
+ // GIVEN satellite reports that it is Connected
+ repo.connectionState.value = SatelliteConnectionState.On
+
+ // THEN icon is null because despite being connected, the mobile stack is reporting a
+ // nonTerrestrial network, and therefore will have its own icon
+ assertThat(latest).isNull()
+ }
+
+ @Test
fun icon_satelliteIsProvisioned() =
testScope.runTest {
val latest by collectLastValue(underTest.icon)
diff --git a/packages/SystemUI/res/layout/volume_dialog_slider.xml b/packages/SystemUI/res/layout/volume_dialog_slider.xml
index c1852b1..9ac456c 100644
--- a/packages/SystemUI/res/layout/volume_dialog_slider.xml
+++ b/packages/SystemUI/res/layout/volume_dialog_slider.xml
@@ -14,15 +14,21 @@
limitations under the License.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="@dimen/volume_dialog_slider_width"
- android:layout_height="@dimen/volume_dialog_slider_height">
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
<com.google.android.material.slider.Slider
- style="@style/SystemUI.Material3.Slider.Volume"
android:id="@+id/volume_dialog_slider"
- android:layout_width="@dimen/volume_dialog_slider_height"
- android:layout_height="match_parent"
+ style="@style/SystemUI.Material3.Slider.Volume"
+ android:layout_width="@dimen/volume_dialog_slider_width"
+ android:layout_height="@dimen/volume_dialog_slider_height"
android:layout_gravity="center"
- android:rotation="270"
- android:theme="@style/Theme.Material3.Light" />
-</FrameLayout>
+ android:theme="@style/Theme.Material3.Light"
+ android:orientation="vertical"
+ app:thumbHeight="52dp"
+ app:trackCornerSize="12dp"
+ app:trackHeight="40dp"
+ app:trackStopIndicatorSize="6dp"
+ app:trackInsideCornerSize="2dp" />
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt
index b2d02ed..a31e61f 100644
--- a/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt
@@ -107,6 +107,7 @@
activityOptions.setDisallowEnterPictureInPictureWhileLaunching(true)
activityOptions.rotationAnimationHint =
WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS
+ intent.collectExtraIntentKeys()
try {
activityTaskManager.startActivityAsUser(
null,
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
index 7242770..e264635 100644
--- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
@@ -21,6 +21,9 @@
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.contextualeducation.GestureType
import com.android.systemui.contextualeducation.GestureType.ALL_APPS
+import com.android.systemui.contextualeducation.GestureType.BACK
+import com.android.systemui.contextualeducation.GestureType.HOME
+import com.android.systemui.contextualeducation.GestureType.OVERVIEW
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.education.ContextualEducationMetricsLogger
@@ -37,6 +40,7 @@
import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import java.time.Clock
+import java.time.Instant
import javax.inject.Inject
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
@@ -48,6 +52,7 @@
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.merge
@@ -71,6 +76,8 @@
const val TAG = "KeyboardTouchpadEduInteractor"
const val MAX_SIGNAL_COUNT: Int = 2
const val MAX_EDUCATION_SHOW_COUNT: Int = 2
+ const val MAX_TOAST_PER_USAGE_SESSION: Int = 2
+
val usageSessionDuration =
getDurationForConfig("persist.contextual_edu.usage_session_sec", 3.days)
val minIntervalBetweenEdu =
@@ -110,6 +117,16 @@
awaitClose { overviewProxyService.removeCallback(listener) }
}
+ private val gestureModelMap: Flow<Map<GestureType, GestureEduModel>> =
+ combine(
+ contextualEducationInteractor.backGestureModelFlow,
+ contextualEducationInteractor.homeGestureModelFlow,
+ contextualEducationInteractor.overviewGestureModelFlow,
+ contextualEducationInteractor.allAppsGestureModelFlow,
+ ) { back, home, overview, allApps ->
+ mapOf(BACK to back, HOME to home, OVERVIEW to overview, ALL_APPS to allApps)
+ }
+
@OptIn(ExperimentalCoroutinesApi::class)
override fun start() {
backgroundScope.launch {
@@ -211,7 +228,11 @@
private suspend fun incrementSignalCount(gestureType: GestureType) {
val targetDevice = getTargetDevice(gestureType)
- if (isTargetDeviceConnected(targetDevice) && hasInitialDelayElapsed(targetDevice)) {
+ if (
+ isTargetDeviceConnected(targetDevice) &&
+ hasInitialDelayElapsed(targetDevice) &&
+ isMinIntervalForToastEduElapsed(gestureType)
+ ) {
contextualEducationInteractor.incrementSignalCount(gestureType)
}
}
@@ -223,6 +244,28 @@
}
}
+ private suspend fun isMinIntervalForToastEduElapsed(gestureType: GestureType): Boolean {
+ val gestureModelMap = gestureModelMap.first()
+ // Only perform checking if the next edu is toast (i.e. no education is shown yet)
+ if (gestureModelMap[gestureType]?.educationShownCount != 0) {
+ return true
+ }
+
+ val wasLastEduToast = { gesture: GestureEduModel -> gesture.educationShownCount == 1 }
+ val toastEduTimesInCurrentSession: List<Instant> =
+ gestureModelMap.values
+ .filter { wasLastEduToast(it) }
+ .mapNotNull { it.lastEducationTime }
+ .filter { it >= clock.instant().minusSeconds(usageSessionDuration.inWholeSeconds) }
+
+ return if (toastEduTimesInCurrentSession.size >= MAX_TOAST_PER_USAGE_SESSION) {
+ val lastToastTime: Instant? = toastEduTimesInCurrentSession.maxOrNull()
+ clock.instant().isAfter(lastToastTime?.plusSeconds(usageSessionDuration.inWholeSeconds))
+ } else {
+ true
+ }
+ }
+
/**
* Keyboard shortcut education would be provided for All Apps. Touchpad gesture education would
* be provided for the rest of the gesture types (i.e. Home, Overview, Back). This method maps
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt
index 17b78eb..e8c4274 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt
@@ -17,6 +17,7 @@
package com.android.systemui.qs.tiles.base.interactor
import android.annotation.WorkerThread
+import com.android.systemui.plugins.qs.TileDetailsViewModel
interface QSTileUserActionInteractor<DATA_TYPE> {
/**
@@ -27,4 +28,17 @@
* It's safe to run long running computations inside this function.
*/
@WorkerThread suspend fun handleInput(input: QSTileInput<DATA_TYPE>)
+
+ /**
+ * Provides the [TileDetailsViewModel] for constructing the corresponding details view.
+ *
+ * This property is defined here to reuse the business logic. For example, reusing the user
+ * long-click as the go-to-settings callback in the details view.
+ * Subclasses can override this property to provide a specific [TileDetailsViewModel]
+ * implementation.
+ *
+ * @return The [TileDetailsViewModel] instance, or null if not implemented.
+ */
+ val detailsViewModel: TileDetailsViewModel?
+ get() = null
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
index aeb6cef..224fa10 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
@@ -20,6 +20,7 @@
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.Dumpable
import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.qs.TileDetailsViewModel
import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
import com.android.systemui.qs.tiles.base.interactor.DisabledByPolicyInteractor
@@ -115,6 +116,9 @@
.flowOn(backgroundDispatcher)
.stateIn(tileScope, SharingStarted.WhileSubscribed(), true)
+ override val detailsViewModel: TileDetailsViewModel?
+ get() = userActionInteractor().detailsViewModel
+
override fun forceUpdate() {
tileScope.launch(context = backgroundDispatcher) { forceUpdates.emit(Unit) }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
index a963b28..fdb15b9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
@@ -18,15 +18,23 @@
import android.content.Intent
import android.provider.Settings
+import com.android.systemui.animation.Expandable
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.qs.TileDetailsViewModel
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
import com.android.systemui.qs.tiles.base.interactor.QSTileInput
import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel
import com.android.systemui.qs.tiles.dialog.InternetDialogManager
import com.android.systemui.qs.tiles.dialog.WifiStateWorker
import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
import com.android.systemui.statusbar.connectivity.AccessPointController
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.withContext
@@ -61,11 +69,18 @@
wifiStateWorker.isWifiEnabled = !wifiStateWorker.isWifiEnabled
}
is QSTileUserAction.LongClick -> {
- qsTileIntentUserActionHandler.handle(
- action.expandable,
- Intent(Settings.ACTION_WIFI_SETTINGS)
- )
+ handleLongClick(action.expandable)
}
}
}
+
+ override val detailsViewModel: TileDetailsViewModel =
+ InternetDetailsViewModel { handleLongClick(null) }
+
+ private fun handleLongClick(expandable:Expandable?){
+ qsTileIntentUserActionHandler.handle(
+ expandable,
+ Intent(Settings.ACTION_WIFI_SETTINGS)
+ )
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
index b1b0001..e8b9926 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
@@ -17,6 +17,7 @@
package com.android.systemui.qs.tiles.viewmodel
import android.os.UserHandle
+import com.android.systemui.plugins.qs.TileDetailsViewModel
import kotlinx.coroutines.flow.StateFlow
/**
@@ -37,6 +38,10 @@
/** Specifies whether this device currently supports this tile. */
val isAvailable: StateFlow<Boolean>
+ /** Specifies the [TileDetailsViewModel] for constructing the corresponding details view. */
+ val detailsViewModel: TileDetailsViewModel?
+ get() = null
+
/**
* Notifies about the user change. Implementations should avoid using 3rd party userId sources
* and use this value instead. This is to maintain consistent and concurrency-free behaviour
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index 9d902d3..632eeef 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -27,6 +27,7 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.UiBackground
import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.plugins.qs.TileDetailsViewModel
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon
import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIconWithRes
@@ -154,6 +155,10 @@
qsTileViewModel.onUserChanged(UserHandle.of(currentUser))
}
+ override fun getDetailsViewModel(): TileDetailsViewModel? {
+ return qsTileViewModel.detailsViewModel
+ }
+
@Deprecated(
"Not needed as {@link com.android.internal.logging.UiEvent} will use #getMetricsSpec",
replaceWith = ReplaceWith("getMetricsSpec"),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt
index d1338ea..f2ef2f0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt
@@ -313,6 +313,7 @@
// if it is volume panel.
options.setDisallowEnterPictureInPictureWhileLaunching(true)
}
+ intent.collectExtraIntentKeys()
try {
result[0] =
ActivityTaskManager.getService()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt
index 1cca3ae..d7cc65d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt
@@ -180,6 +180,7 @@
// if it is volume panel.
options.setDisallowEnterPictureInPictureWhileLaunching(true)
}
+ intent.collectExtraIntentKeys()
try {
result[0] =
ActivityTaskManager.getService()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
index 08a98c3..12f578c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
@@ -161,6 +161,13 @@
)
.stateIn(scope, SharingStarted.WhileSubscribed(), true)
+ /** True if any known mobile network is currently using a non terrestrial network */
+ val isAnyConnectionNtn =
+ iconsInteractor.icons.aggregateOver(selector = { it.isNonTerrestrial }, false) {
+ nonTerrestrialNetworks ->
+ nonTerrestrialNetworks.any { it == true }
+ }
+
companion object {
const val TAG = "DeviceBasedSatelliteInteractor"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
index f3d5139..ea915ef 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
@@ -114,34 +114,39 @@
private val showIcon =
if (interactor.isOpportunisticSatelliteIconEnabled) {
- canShowIcon
- .flatMapLatest { canShow ->
- if (!canShow) {
- flowOf(false)
- } else {
- combine(
- shouldShowIconForOosAfterHysteresis,
- interactor.connectionState,
- interactor.isWifiActive,
- airplaneModeRepository.isAirplaneMode,
- ) { showForOos, connectionState, isWifiActive, isAirplaneMode ->
- if (isWifiActive || isAirplaneMode) {
- false
- } else {
- showForOos ||
- connectionState == SatelliteConnectionState.On ||
- connectionState == SatelliteConnectionState.Connected
+ canShowIcon
+ .flatMapLatest { canShow ->
+ if (!canShow) {
+ flowOf(false)
+ } else {
+ combine(
+ shouldShowIconForOosAfterHysteresis,
+ interactor.isAnyConnectionNtn,
+ interactor.connectionState,
+ interactor.isWifiActive,
+ airplaneModeRepository.isAirplaneMode,
+ ) { showForOos, anyNtn, connectionState, isWifiActive, isAirplaneMode ->
+ // anyNtn means that there is some mobile network using ntn, and the
+ // mobile icon will show its own satellite icon
+ if (isWifiActive || isAirplaneMode || anyNtn) {
+ false
+ } else {
+ // Show for out of service (which has a hysteresis), or ignore
+ // the hysteresis if we're already connected
+ showForOos ||
+ connectionState == SatelliteConnectionState.On ||
+ connectionState == SatelliteConnectionState.Connected
+ }
}
}
}
- }
- .distinctUntilChanged()
- .logDiffsForTable(
- tableLog,
- columnPrefix = "vm",
- columnName = COL_VISIBLE,
- initialValue = false,
- )
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ tableLog,
+ columnPrefix = "vm",
+ columnName = COL_VISIBLE,
+ initialValue = false,
+ )
} else {
flowOf(false)
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt
index b83613b..4071918 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt
@@ -20,7 +20,6 @@
import android.media.AudioManager.RINGER_MODE_NORMAL
import android.media.AudioManager.RINGER_MODE_SILENT
import android.media.AudioManager.RINGER_MODE_VIBRATE
-import android.provider.Settings
import com.android.settingslib.volume.data.repository.AudioSystemRepository
import com.android.settingslib.volume.shared.model.RingerMode
import com.android.systemui.plugins.VolumeDialogController
@@ -66,11 +65,6 @@
}
},
currentRingerMode = RingerMode(state.ringerModeInternal),
- isEnabled =
- !(state.zenMode == Settings.Global.ZEN_MODE_ALARMS ||
- state.zenMode == Settings.Global.ZEN_MODE_NO_INTERRUPTIONS ||
- (state.zenMode == Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS &&
- state.disallowRinger)),
isMuted = it.level == 0 || it.muted,
level = it.level,
levelMax = it.levelMax,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/shared/model/VolumeDialogRingerModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/shared/model/VolumeDialogRingerModel.kt
index 3c24e02..84a8280 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/shared/model/VolumeDialogRingerModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/shared/model/VolumeDialogRingerModel.kt
@@ -23,8 +23,6 @@
val availableModes: List<RingerMode>,
/** Current ringer mode internal */
val currentRingerMode: RingerMode,
- /** whether the ringer is allowed given the current ZenMode */
- val isEnabled: Boolean,
/** Whether the current ring stream level is zero or the controller state is muted */
val isMuted: Boolean,
/** Ring stream level */
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
index 3bd2721..9eee91b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
@@ -34,6 +34,7 @@
import com.android.systemui.res.R
import com.android.systemui.util.children
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import com.android.systemui.volume.dialog.ringer.ui.util.VolumeDialogRingerDrawerTransitionListener
import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerButtonUiModel
import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerButtonViewModel
import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerDrawerState
@@ -42,6 +43,7 @@
import com.android.systemui.volume.dialog.ringer.ui.viewmodel.VolumeDialogRingerDrawerViewModel
import com.android.systemui.volume.dialog.ui.utils.suspendAnimate
import javax.inject.Inject
+import kotlin.properties.Delegates
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.launchIn
@@ -71,6 +73,27 @@
val drawerContainer = view.requireViewById<MotionLayout>(R.id.volume_ringer_drawer)
val unselectedButtonUiModel = RingerButtonUiModel.getUnselectedButton(view.context)
val selectedButtonUiModel = RingerButtonUiModel.getSelectedButton(view.context)
+ val volumeDialogBgSmallRadius =
+ view.context.resources.getDimensionPixelSize(
+ R.dimen.volume_dialog_background_square_corner_radius
+ )
+ val volumeDialogBgFullRadius =
+ view.context.resources.getDimensionPixelSize(
+ R.dimen.volume_dialog_background_corner_radius
+ )
+ var backgroundAnimationProgress: Float by
+ Delegates.observable(0F) { _, _, progress ->
+ volumeDialogBackgroundView.applyCorners(
+ fullRadius = volumeDialogBgFullRadius,
+ diff = volumeDialogBgFullRadius - volumeDialogBgSmallRadius,
+ progress,
+ )
+ }
+ val ringerDrawerTransitionListener = VolumeDialogRingerDrawerTransitionListener {
+ backgroundAnimationProgress = it
+ }
+ drawerContainer.setTransitionListener(ringerDrawerTransitionListener)
+ volumeDialogBackgroundView.background = volumeDialogBackgroundView.background.mutate()
viewModel.ringerViewModel
.onEach { ringerState ->
when (ringerState) {
@@ -87,10 +110,8 @@
selectedButtonUiModel,
unselectedButtonUiModel,
)
+ ringerDrawerTransitionListener.setProgressChangeEnabled(true)
drawerContainer.closeDrawer(uiModel.currentButtonIndex)
- volumeDialogBackgroundView.setBackgroundResource(
- R.drawable.volume_dialog_background
- )
}
is RingerDrawerState.Closed -> {
@@ -103,11 +124,31 @@
uiModel,
selectedButtonUiModel,
unselectedButtonUiModel,
+ onProgressChanged = { progress, isReverse ->
+ // Let's make button progress when switching matches
+ // motionLayout transition progress. When full radius,
+ // progress is 0.0. When small radius, progress is 1.0.
+ backgroundAnimationProgress =
+ if (isReverse) {
+ 1F - progress
+ } else {
+ progress
+ }
+ },
) {
+ if (
+ uiModel.currentButtonIndex ==
+ uiModel.availableButtons.size - 1
+ ) {
+ ringerDrawerTransitionListener.setProgressChangeEnabled(
+ false
+ )
+ } else {
+ ringerDrawerTransitionListener.setProgressChangeEnabled(
+ true
+ )
+ }
drawerContainer.closeDrawer(uiModel.currentButtonIndex)
- volumeDialogBackgroundView.setBackgroundResource(
- R.drawable.volume_dialog_background
- )
}
}
}
@@ -120,16 +161,18 @@
unselectedButtonUiModel,
)
// Open drawer
+ if (
+ uiModel.currentButtonIndex == uiModel.availableButtons.size - 1
+ ) {
+ ringerDrawerTransitionListener.setProgressChangeEnabled(false)
+ } else {
+ ringerDrawerTransitionListener.setProgressChangeEnabled(true)
+ }
drawerContainer.transitionToState(
R.id.volume_dialog_ringer_drawer_open
)
- if (
- uiModel.currentButtonIndex != uiModel.availableButtons.size - 1
- ) {
- volumeDialogBackgroundView.setBackgroundResource(
- R.drawable.volume_dialog_background_small_radius
- )
- }
+ volumeDialogBackgroundView.background =
+ volumeDialogBackgroundView.background.mutate()
}
}
}
@@ -150,6 +193,7 @@
uiModel: RingerViewModel,
selectedButtonUiModel: RingerButtonUiModel,
unselectedButtonUiModel: RingerButtonUiModel,
+ onProgressChanged: (Float, Boolean) -> Unit = { _, _ -> },
onAnimationEnd: Runnable? = null,
) {
ensureChildCount(R.layout.volume_ringer_button, uiModel.availableButtons.size)
@@ -177,10 +221,26 @@
CLOSE_DRAWER_DELAY,
)
}
-
- // We only need to execute on roundness animation end once.
- selectedButton.animateTo(selectedButtonUiModel, roundnessAnimationEndListener)
- unselectedButton.animateTo(unselectedButtonUiModel)
+ // We only need to execute on roundness animation end and volume dialog background
+ // progress update once because these changes should be applied once on volume dialog
+ // background and ringer drawer views.
+ selectedButton.animateTo(
+ selectedButtonUiModel,
+ if (uiModel.currentButtonIndex == count - 1) {
+ onProgressChanged
+ } else {
+ { _, _ -> }
+ },
+ roundnessAnimationEndListener,
+ )
+ unselectedButton.animateTo(
+ unselectedButtonUiModel,
+ if (previousIndex == count - 1) {
+ onProgressChanged
+ } else {
+ { _, _ -> }
+ },
+ )
} else {
bindButtons(viewModel, uiModel, onAnimationEnd)
}
@@ -366,6 +426,7 @@
private suspend fun ImageButton.animateTo(
ringerButtonUiModel: RingerButtonUiModel,
+ onProgressChanged: (Float, Boolean) -> Unit = { _, _ -> },
roundnessAnimationEndListener: DynamicAnimation.OnAnimationEndListener? = null,
) {
val roundnessAnimation =
@@ -376,6 +437,7 @@
ringerButtonUiModel.cornerRadius - (background as GradientDrawable).cornerRadius
val roundnessAnimationUpdateListener =
DynamicAnimation.OnAnimationUpdateListener { _, value, _ ->
+ onProgressChanged(value, cornerRadiusDiff > 0F)
(background as GradientDrawable).cornerRadius = radius + value * cornerRadiusDiff
background.invalidateSelf()
}
@@ -406,4 +468,9 @@
)
}
}
+
+ private fun View.applyCorners(fullRadius: Int, diff: Int, progress: Float) {
+ (background as GradientDrawable).cornerRadius = fullRadius - progress * diff
+ background.invalidateSelf()
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/util/VolumeDialogRingerDrawerTransitionListener.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/util/VolumeDialogRingerDrawerTransitionListener.kt
new file mode 100644
index 0000000..6e3db0a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/util/VolumeDialogRingerDrawerTransitionListener.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.volume.dialog.ringer.ui.util
+
+import androidx.constraintlayout.motion.widget.MotionLayout
+
+class VolumeDialogRingerDrawerTransitionListener(private val onProgressChanged: (Float) -> Unit) :
+ MotionLayout.TransitionListener {
+
+ private var notifyProgressChangeEnabled = true
+
+ fun setProgressChangeEnabled(enabled: Boolean) {
+ notifyProgressChangeEnabled = enabled
+ }
+
+ override fun onTransitionStarted(motionLayout: MotionLayout?, startId: Int, endId: Int) {}
+
+ override fun onTransitionChange(
+ motionLayout: MotionLayout?,
+ startId: Int,
+ endId: Int,
+ progress: Float,
+ ) {
+ if (notifyProgressChangeEnabled) {
+ onProgressChanged(progress)
+ }
+ }
+
+ override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {}
+
+ override fun onTransitionTrigger(
+ motionLayout: MotionLayout?,
+ triggerId: Int,
+ positive: Boolean,
+ progress: Float,
+ ) {}
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
index e646636..627d75e 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
@@ -21,10 +21,13 @@
import android.media.AudioManager.RINGER_MODE_NORMAL
import android.media.AudioManager.RINGER_MODE_SILENT
import android.media.AudioManager.RINGER_MODE_VIBRATE
+import android.media.AudioManager.STREAM_RING
import android.os.VibrationEffect
import android.widget.Toast
import com.android.internal.R as internalR
import com.android.settingslib.Utils
+import com.android.settingslib.notification.domain.interactor.NotificationsSoundPolicyInteractor
+import com.android.settingslib.volume.shared.model.AudioStream
import com.android.settingslib.volume.shared.model.RingerMode
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
@@ -57,7 +60,8 @@
@Application private val applicationContext: Context,
@VolumeDialog private val coroutineScope: CoroutineScope,
@Background private val backgroundDispatcher: CoroutineDispatcher,
- private val interactor: VolumeDialogRingerInteractor,
+ soundPolicyInteractor: NotificationsSoundPolicyInteractor,
+ private val ringerInteractor: VolumeDialogRingerInteractor,
private val vibrator: VibratorHelper,
private val volumeDialogLogger: VolumeDialogLogger,
private val visibilityInteractor: VolumeDialogVisibilityInteractor,
@@ -66,10 +70,14 @@
private val drawerState = MutableStateFlow<RingerDrawerState>(RingerDrawerState.Initial)
val ringerViewModel: StateFlow<RingerViewModelState> =
- combine(interactor.ringerModel, drawerState) { ringerModel, state ->
+ combine(
+ soundPolicyInteractor.isZenMuted(AudioStream(STREAM_RING)),
+ ringerInteractor.ringerModel,
+ drawerState,
+ ) { isZenMuted, ringerModel, state ->
level = ringerModel.level
levelMax = ringerModel.levelMax
- ringerModel.toViewModel(state)
+ ringerModel.toViewModel(state, isZenMuted)
}
.flowOn(backgroundDispatcher)
.stateIn(coroutineScope, SharingStarted.Eagerly, RingerViewModelState.Unavailable)
@@ -90,7 +98,7 @@
Events.writeEvent(Events.EVENT_RINGER_TOGGLE, ringerMode.value)
provideTouchFeedback(ringerMode)
maybeShowToast(ringerMode)
- interactor.setRingerMode(ringerMode)
+ ringerInteractor.setRingerMode(ringerMode)
}
visibilityInteractor.resetDismissTimeout()
drawerState.value =
@@ -113,7 +121,7 @@
private fun provideTouchFeedback(ringerMode: RingerMode) {
when (ringerMode.value) {
RINGER_MODE_NORMAL -> {
- interactor.scheduleTouchFeedback()
+ ringerInteractor.scheduleTouchFeedback()
null
}
RINGER_MODE_SILENT -> VibrationEffect.get(VibrationEffect.EFFECT_CLICK)
@@ -123,7 +131,8 @@
}
private fun VolumeDialogRingerModel.toViewModel(
- drawerState: RingerDrawerState
+ drawerState: RingerDrawerState,
+ isZenMuted: Boolean,
): RingerViewModelState {
val currentIndex = availableModes.indexOf(currentRingerMode)
if (currentIndex == -1) {
@@ -132,10 +141,11 @@
return if (currentIndex == -1 || isSingleVolume) {
RingerViewModelState.Unavailable
} else {
- toButtonViewModel(currentRingerMode, isSelectedButton = true)?.let {
+ toButtonViewModel(currentRingerMode, isZenMuted, isSelectedButton = true)?.let {
RingerViewModelState.Available(
RingerViewModel(
- availableButtons = availableModes.map { mode -> toButtonViewModel(mode) },
+ availableButtons =
+ availableModes.map { mode -> toButtonViewModel(mode, isZenMuted) },
currentButtonIndex = currentIndex,
selectedButton = it,
drawerState = drawerState,
@@ -147,6 +157,7 @@
private fun VolumeDialogRingerModel.toButtonViewModel(
ringerMode: RingerMode,
+ isZenMuted: Boolean,
isSelectedButton: Boolean = false,
): RingerButtonViewModel? {
return when (ringerMode.value) {
@@ -176,7 +187,7 @@
)
RINGER_MODE_NORMAL ->
when {
- isMuted && isEnabled ->
+ isMuted && !isZenMuted ->
RingerButtonViewModel(
imageResId =
if (isSelectedButton) {
@@ -226,7 +237,7 @@
private fun maybeShowToast(ringerMode: RingerMode) {
coroutineScope.launch {
- val seenToastCount = interactor.getToastCount()
+ val seenToastCount = ringerInteractor.getToastCount()
if (seenToastCount > SHOW_RINGER_TOAST_COUNT) {
return@launch
}
@@ -260,7 +271,7 @@
)
}
toastText?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_SHORT).show() }
- interactor.updateToastCount(seenToastCount)
+ ringerInteractor.updateToastCount(seenToastCount)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
index e52bad9..f305246 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
@@ -18,11 +18,12 @@
import android.animation.Animator
import android.animation.ObjectAnimator
+import android.annotation.SuppressLint
import android.view.View
import android.view.animation.DecelerateInterpolator
import com.android.systemui.res.R
-import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel
import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderStateModel
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel
import com.android.systemui.volume.dialog.ui.utils.JankListenerFactory
import com.android.systemui.volume.dialog.ui.utils.awaitAnimation
@@ -48,24 +49,27 @@
val sliderView: Slider =
view.requireViewById<Slider>(R.id.volume_dialog_slider).apply {
labelBehavior = LabelFormatter.LABEL_GONE
+ trackIconActiveColor = trackInactiveTintList
}
sliderView.addOnChangeListener { _, value, fromUser ->
viewModel.setStreamVolume(value.roundToInt(), fromUser)
}
- viewModel.model.onEach { it.bindToSlider(sliderView) }.launchIn(this)
+ viewModel.state.onEach { it.bindToSlider(sliderView) }.launchIn(this)
}
- private suspend fun VolumeDialogStreamModel.bindToSlider(slider: Slider) {
+ @SuppressLint("UseCompatLoadingForDrawables")
+ private suspend fun VolumeDialogSliderStateModel.bindToSlider(slider: Slider) {
with(slider) {
- valueFrom = levelMin.toFloat()
- valueTo = levelMax.toFloat()
+ valueFrom = minValue
+ valueTo = maxValue
// coerce the current value to the new value range before animating it
value = value.coerceIn(valueFrom, valueTo)
setValueAnimated(
- level.toFloat(),
+ value,
jankListenerFactory.update(this, PROGRESS_CHANGE_ANIMATION_DURATION_MS),
)
+ trackIconActiveEnd = context.getDrawable(iconRes)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt
new file mode 100644
index 0000000..5c39b6f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt
@@ -0,0 +1,122 @@
+/*
+ * 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.volume.dialog.sliders.ui.viewmodel
+
+import android.media.AudioManager
+import androidx.annotation.DrawableRes
+import com.android.settingslib.notification.domain.interactor.NotificationsSoundPolicyInteractor
+import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor
+import com.android.settingslib.volume.shared.model.AudioStream
+import com.android.settingslib.volume.shared.model.RingerMode
+import com.android.systemui.res.R
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOf
+
+class VolumeDialogSliderIconProvider
+@Inject
+constructor(
+ private val notificationsSoundPolicyInteractor: NotificationsSoundPolicyInteractor,
+ private val audioVolumeInteractor: AudioVolumeInteractor,
+) {
+
+ @DrawableRes
+ fun getStreamIcon(
+ stream: Int,
+ level: Int,
+ levelMin: Int,
+ levelMax: Int,
+ isMuted: Boolean,
+ isRoutedToBluetooth: Boolean,
+ ): Flow<Int> {
+ return combine(
+ notificationsSoundPolicyInteractor.isZenMuted(AudioStream(stream)),
+ ringerModeForStream(stream),
+ ) { isZenMuted, ringerMode ->
+ val isStreamOffline = level == 0 || isMuted
+ if (isZenMuted) {
+ // TODO(b/372466264) use icon for the corresponding zenmode
+ return@combine com.android.internal.R.drawable.ic_qs_dnd
+ }
+ when (ringerMode?.value) {
+ AudioManager.RINGER_MODE_VIBRATE ->
+ return@combine R.drawable.ic_volume_ringer_vibrate
+ AudioManager.RINGER_MODE_SILENT -> return@combine R.drawable.ic_ring_volume_off
+ }
+ if (isRoutedToBluetooth) {
+ return@combine if (stream == AudioManager.STREAM_VOICE_CALL) {
+ R.drawable.ic_volume_bt_sco
+ } else {
+ if (isStreamOffline) {
+ R.drawable.ic_volume_media_bt_mute
+ } else {
+ R.drawable.ic_volume_media_bt
+ }
+ }
+ }
+
+ return@combine if (isStreamOffline) {
+ getMutedIconForStream(stream) ?: getIconForStream(stream)
+ } else {
+ if (level < (levelMax + levelMin) / 2) {
+ // This icon is different on TV
+ R.drawable.ic_volume_media_low
+ } else {
+ getIconForStream(stream)
+ }
+ }
+ }
+ }
+
+ @DrawableRes
+ private fun getMutedIconForStream(stream: Int): Int? {
+ return when (stream) {
+ AudioManager.STREAM_MUSIC -> R.drawable.ic_volume_media_mute
+ AudioManager.STREAM_NOTIFICATION -> R.drawable.ic_volume_ringer_mute
+ AudioManager.STREAM_ALARM -> R.drawable.ic_volume_alarm_mute
+ AudioManager.STREAM_SYSTEM -> R.drawable.ic_volume_system_mute
+ else -> null
+ }
+ }
+
+ @DrawableRes
+ private fun getIconForStream(stream: Int): Int {
+ return when (stream) {
+ AudioManager.STREAM_ACCESSIBILITY -> R.drawable.ic_volume_accessibility
+ AudioManager.STREAM_MUSIC -> R.drawable.ic_volume_media
+ AudioManager.STREAM_RING -> R.drawable.ic_ring_volume
+ AudioManager.STREAM_NOTIFICATION -> R.drawable.ic_volume_ringer
+ AudioManager.STREAM_ALARM -> R.drawable.ic_alarm
+ AudioManager.STREAM_VOICE_CALL -> com.android.internal.R.drawable.ic_phone
+ AudioManager.STREAM_SYSTEM -> R.drawable.ic_volume_system
+ else -> error("Unsupported stream: $stream")
+ }
+ }
+
+ /**
+ * Emits [RingerMode] for the [stream] if it's affecting it and null when [RingerMode] doesn't
+ * affect the [stream]
+ */
+ private fun ringerModeForStream(stream: Int): Flow<RingerMode?> {
+ return if (stream == AudioManager.STREAM_RING) {
+ audioVolumeInteractor.ringerMode
+ } else {
+ flowOf(null)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt
new file mode 100644
index 0000000..5750c04
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.volume.dialog.sliders.ui.viewmodel
+
+import androidx.annotation.DrawableRes
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel
+
+data class VolumeDialogSliderStateModel(
+ val minValue: Float,
+ val maxValue: Float,
+ val value: Float,
+ @DrawableRes val iconRes: Int,
+)
+
+fun VolumeDialogStreamModel.toStateModel(@DrawableRes iconRes: Int): VolumeDialogSliderStateModel {
+ return VolumeDialogSliderStateModel(
+ minValue = levelMin.toFloat(),
+ value = level.toFloat(),
+ maxValue = levelMax.toFloat(),
+ iconRes = iconRes,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt
index 6dd5b63..2d56524 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt
@@ -32,7 +32,9 @@
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
@@ -56,12 +58,12 @@
private val interactor: VolumeDialogSliderInteractor,
private val visibilityInteractor: VolumeDialogVisibilityInteractor,
@VolumeDialog private val coroutineScope: CoroutineScope,
+ private val volumeDialogSliderIconProvider: VolumeDialogSliderIconProvider,
private val systemClock: SystemClock,
) {
private val userVolumeUpdates = MutableStateFlow<VolumeUpdate?>(null)
-
- val model: Flow<VolumeDialogStreamModel> =
+ private val model: Flow<VolumeDialogStreamModel> =
interactor.slider
.filter {
val lastVolumeUpdateTime = userVolumeUpdates.value?.timestampMillis ?: 0
@@ -70,6 +72,21 @@
.stateIn(coroutineScope, SharingStarted.Eagerly, null)
.filterNotNull()
+ val state: Flow<VolumeDialogSliderStateModel> =
+ model.flatMapLatest { streamModel ->
+ with(streamModel) {
+ volumeDialogSliderIconProvider.getStreamIcon(
+ stream = stream,
+ level = level,
+ levelMin = levelMin,
+ levelMax = levelMax,
+ isMuted = muted,
+ isRoutedToBluetooth = routedToBluetooth,
+ )
+ }
+ .map { icon -> streamModel.toStateModel(icon) }
+ }
+
init {
userVolumeUpdates
.filterNotNull()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt
new file mode 100644
index 0000000..d7fcb6a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt
@@ -0,0 +1,471 @@
+/*
+ * 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.systemui.education.domain.interactor
+
+import android.content.pm.UserInfo
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.contextualeducation.GestureType
+import com.android.systemui.contextualeducation.GestureType.ALL_APPS
+import com.android.systemui.contextualeducation.GestureType.BACK
+import com.android.systemui.contextualeducation.GestureType.HOME
+import com.android.systemui.contextualeducation.GestureType.OVERVIEW
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.education.data.model.GestureEduModel
+import com.android.systemui.education.data.repository.contextualEducationRepository
+import com.android.systemui.education.data.repository.fakeEduClock
+import com.android.systemui.education.shared.model.EducationUiType
+import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType
+import com.android.systemui.inputdevice.tutorial.tutorialSchedulerRepository
+import com.android.systemui.keyboard.data.repository.keyboardRepository
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
+import com.android.systemui.testKosmos
+import com.android.systemui.touchpad.data.repository.touchpadRepository
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.hours
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.verify
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+@SmallTest
+@RunWith(ParameterizedAndroidJunit4::class)
+@kotlinx.coroutines.ExperimentalCoroutinesApi
+class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: GestureType) :
+ SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val contextualEduInteractor = kosmos.contextualEducationInteractor
+ private val repository = kosmos.contextualEducationRepository
+ private val touchpadRepository = kosmos.touchpadRepository
+ private val keyboardRepository = kosmos.keyboardRepository
+ private val tutorialSchedulerRepository = kosmos.tutorialSchedulerRepository
+ private val userRepository = kosmos.fakeUserRepository
+ private val overviewProxyService = kosmos.mockOverviewProxyService
+
+ private val underTest: KeyboardTouchpadEduInteractor = kosmos.keyboardTouchpadEduInteractor
+ private val eduClock = kosmos.fakeEduClock
+ private val minDurationForNextEdu =
+ KeyboardTouchpadEduInteractor.minIntervalBetweenEdu + 1.seconds
+ private val initialDelayElapsedDuration =
+ KeyboardTouchpadEduInteractor.initialDelayDuration + 1.seconds
+
+ @Before
+ fun setup() {
+ underTest.start()
+ contextualEduInteractor.start()
+ userRepository.setUserInfos(USER_INFOS)
+ testScope.launch {
+ contextualEduInteractor.updateKeyboardFirstConnectionTime()
+ contextualEduInteractor.updateTouchpadFirstConnectionTime()
+ }
+ }
+
+ @Test
+ fun newEducationInfoOnMaxSignalCountReached() =
+ testScope.runTest {
+ triggerMaxEducationSignals(gestureType)
+ val model by collectLastValue(underTest.educationTriggered)
+
+ assertThat(model?.gestureType).isEqualTo(gestureType)
+ }
+
+ @Test
+ fun newEducationToastOn1stEducation() =
+ testScope.runTest {
+ val model by collectLastValue(underTest.educationTriggered)
+ triggerMaxEducationSignals(gestureType)
+
+ assertThat(model?.educationUiType).isEqualTo(EducationUiType.Toast)
+ }
+
+ @Test
+ fun newEducationNotificationOn2ndEducation() =
+ testScope.runTest {
+ val model by collectLastValue(underTest.educationTriggered)
+ triggerMaxEducationSignals(gestureType)
+ // runCurrent() to trigger 1st education
+ runCurrent()
+
+ eduClock.offset(minDurationForNextEdu)
+ triggerMaxEducationSignals(gestureType)
+
+ assertThat(model?.educationUiType).isEqualTo(EducationUiType.Notification)
+ }
+
+ @Test
+ fun noEducationInfoBeforeMaxSignalCountReached() =
+ testScope.runTest {
+ contextualEduInteractor.incrementSignalCount(gestureType)
+ val model by collectLastValue(underTest.educationTriggered)
+ assertThat(model).isNull()
+ }
+
+ @Test
+ fun noEducationInfoWhenShortcutTriggeredPreviously() =
+ testScope.runTest {
+ val model by collectLastValue(underTest.educationTriggered)
+ contextualEduInteractor.updateShortcutTriggerTime(gestureType)
+ triggerMaxEducationSignals(gestureType)
+ assertThat(model).isNull()
+ }
+
+ @Test
+ fun no2ndEducationBeforeMinEduIntervalReached() =
+ testScope.runTest {
+ val models by collectValues(underTest.educationTriggered)
+ triggerMaxEducationSignals(gestureType)
+ runCurrent()
+
+ // Offset a duration that is less than the required education interval
+ eduClock.offset(1.seconds)
+ triggerMaxEducationSignals(gestureType)
+ runCurrent()
+
+ assertThat(models.filterNotNull().size).isEqualTo(1)
+ }
+
+ @Test
+ fun noNewEducationInfoAfterMaxEducationCountReached() =
+ testScope.runTest {
+ val models by collectValues(underTest.educationTriggered)
+ // Trigger 2 educations
+ triggerMaxEducationSignals(gestureType)
+ runCurrent()
+ eduClock.offset(minDurationForNextEdu)
+ triggerMaxEducationSignals(gestureType)
+ runCurrent()
+
+ // Try triggering 3rd education
+ eduClock.offset(minDurationForNextEdu)
+ triggerMaxEducationSignals(gestureType)
+
+ assertThat(models.filterNotNull().size).isEqualTo(2)
+ }
+
+ @Test
+ fun startNewUsageSessionWhen2ndSignalReceivedAfterSessionDeadline() =
+ testScope.runTest {
+ val model by
+ collectLastValue(
+ kosmos.contextualEducationRepository.readGestureEduModelFlow(gestureType)
+ )
+ contextualEduInteractor.incrementSignalCount(gestureType)
+ eduClock.offset(KeyboardTouchpadEduInteractor.usageSessionDuration.plus(1.seconds))
+ val secondSignalReceivedTime = eduClock.instant()
+ contextualEduInteractor.incrementSignalCount(gestureType)
+
+ assertThat(model)
+ .isEqualTo(
+ GestureEduModel(
+ signalCount = 1,
+ usageSessionStartTime = secondSignalReceivedTime,
+ userId = 0,
+ gestureType = gestureType,
+ )
+ )
+ }
+
+ @Test
+ fun newTouchpadConnectionTimeOnFirstTouchpadConnected() =
+ testScope.runTest {
+ setIsAnyTouchpadConnected(true)
+ val model = contextualEduInteractor.getEduDeviceConnectionTime()
+ assertThat(model.touchpadFirstConnectionTime).isEqualTo(eduClock.instant())
+ }
+
+ @Test
+ fun unchangedTouchpadConnectionTimeOnSecondConnection() =
+ testScope.runTest {
+ val firstConnectionTime = eduClock.instant()
+ setIsAnyTouchpadConnected(true)
+ setIsAnyTouchpadConnected(false)
+
+ eduClock.offset(1.hours)
+ setIsAnyTouchpadConnected(true)
+
+ val model = contextualEduInteractor.getEduDeviceConnectionTime()
+ assertThat(model.touchpadFirstConnectionTime).isEqualTo(firstConnectionTime)
+ }
+
+ @Test
+ fun newTouchpadConnectionTimeOnUserChanged() =
+ testScope.runTest {
+ // Touchpad connected for user 0
+ setIsAnyTouchpadConnected(true)
+
+ // Change user
+ eduClock.offset(1.hours)
+ val newUserFirstConnectionTime = eduClock.instant()
+ userRepository.setSelectedUserInfo(USER_INFOS[0])
+ runCurrent()
+
+ val model = contextualEduInteractor.getEduDeviceConnectionTime()
+ assertThat(model.touchpadFirstConnectionTime).isEqualTo(newUserFirstConnectionTime)
+ }
+
+ @Test
+ fun newKeyboardConnectionTimeOnKeyboardConnected() =
+ testScope.runTest {
+ setIsAnyKeyboardConnected(true)
+ val model = contextualEduInteractor.getEduDeviceConnectionTime()
+ assertThat(model.keyboardFirstConnectionTime).isEqualTo(eduClock.instant())
+ }
+
+ @Test
+ fun unchangedKeyboardConnectionTimeOnSecondConnection() =
+ testScope.runTest {
+ val firstConnectionTime = eduClock.instant()
+ setIsAnyKeyboardConnected(true)
+ setIsAnyKeyboardConnected(false)
+
+ eduClock.offset(1.hours)
+ setIsAnyKeyboardConnected(true)
+
+ val model = contextualEduInteractor.getEduDeviceConnectionTime()
+ assertThat(model.keyboardFirstConnectionTime).isEqualTo(firstConnectionTime)
+ }
+
+ @Test
+ fun newKeyboardConnectionTimeOnUserChanged() =
+ testScope.runTest {
+ // Keyboard connected for user 0
+ setIsAnyKeyboardConnected(true)
+
+ // Change user
+ eduClock.offset(1.hours)
+ val newUserFirstConnectionTime = eduClock.instant()
+ userRepository.setSelectedUserInfo(USER_INFOS[0])
+ runCurrent()
+
+ val model = contextualEduInteractor.getEduDeviceConnectionTime()
+ assertThat(model.keyboardFirstConnectionTime).isEqualTo(newUserFirstConnectionTime)
+ }
+
+ @Test
+ fun updateShortcutTimeOnKeyboardShortcutTriggered() =
+ testScope.runTest {
+ // Only All Apps needs to update the keyboard shortcut
+ assumeTrue(gestureType == ALL_APPS)
+ kosmos.contextualEducationRepository.setKeyboardShortcutTriggered(ALL_APPS)
+
+ val model by
+ collectLastValue(
+ kosmos.contextualEducationRepository.readGestureEduModelFlow(ALL_APPS)
+ )
+ assertThat(model?.lastShortcutTriggeredTime).isEqualTo(eduClock.instant())
+ }
+
+ @Test
+ fun dataUpdatedOnIncrementSignalCountWhenTouchpadConnected() =
+ testScope.runTest {
+ assumeTrue(gestureType != ALL_APPS)
+ setUpForInitialDelayElapse()
+ touchpadRepository.setIsAnyTouchpadConnected(true)
+
+ val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+ val originalValue = model!!.signalCount
+ val listener = getOverviewProxyListener()
+ listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+
+ assertThat(model?.signalCount).isEqualTo(originalValue + 1)
+ }
+
+ @Test
+ fun dataUnchangedOnIncrementSignalCountWhenTouchpadDisconnected() =
+ testScope.runTest {
+ setUpForInitialDelayElapse()
+ touchpadRepository.setIsAnyTouchpadConnected(false)
+
+ val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+ val originalValue = model!!.signalCount
+ val listener = getOverviewProxyListener()
+ listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+
+ assertThat(model?.signalCount).isEqualTo(originalValue)
+ }
+
+ @Test
+ fun dataUpdatedOnIncrementSignalCountWhenKeyboardConnected() =
+ testScope.runTest {
+ assumeTrue(gestureType == ALL_APPS)
+ setUpForInitialDelayElapse()
+ keyboardRepository.setIsAnyKeyboardConnected(true)
+
+ val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+ val originalValue = model!!.signalCount
+ val listener = getOverviewProxyListener()
+ listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+
+ assertThat(model?.signalCount).isEqualTo(originalValue + 1)
+ }
+
+ @Test
+ fun dataUnchangedOnIncrementSignalCountWhenKeyboardDisconnected() =
+ testScope.runTest {
+ setUpForInitialDelayElapse()
+ keyboardRepository.setIsAnyKeyboardConnected(false)
+
+ val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+ val originalValue = model!!.signalCount
+ val listener = getOverviewProxyListener()
+ listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+
+ assertThat(model?.signalCount).isEqualTo(originalValue)
+ }
+
+ @Test
+ fun dataAddedOnUpdateShortcutTriggerTime() =
+ testScope.runTest {
+ val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+ assertThat(model?.lastShortcutTriggeredTime).isNull()
+
+ val listener = getOverviewProxyListener()
+ listener.updateContextualEduStats(/* isTrackpadGesture= */ true, gestureType)
+
+ assertThat(model?.lastShortcutTriggeredTime).isEqualTo(kosmos.fakeEduClock.instant())
+ }
+
+ @Test
+ fun dataUpdatedOnIncrementSignalCountAfterInitialDelay() =
+ testScope.runTest {
+ setUpForDeviceConnection()
+ tutorialSchedulerRepository.updateLaunchTime(DeviceType.TOUCHPAD, eduClock.instant())
+
+ val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+ val originalValue = model!!.signalCount
+ eduClock.offset(initialDelayElapsedDuration)
+ val listener = getOverviewProxyListener()
+ listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+
+ assertThat(model?.signalCount).isEqualTo(originalValue + 1)
+ }
+
+ @Test
+ fun dataUnchangedOnIncrementSignalCountBeforeInitialDelay() =
+ testScope.runTest {
+ setUpForDeviceConnection()
+ tutorialSchedulerRepository.updateLaunchTime(DeviceType.TOUCHPAD, eduClock.instant())
+
+ val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+ val originalValue = model!!.signalCount
+ // No offset to the clock to simulate update before initial delay
+ val listener = getOverviewProxyListener()
+ listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+
+ assertThat(model?.signalCount).isEqualTo(originalValue)
+ }
+
+ @Test
+ fun dataUnchangedOnIncrementSignalCountWithoutOobeLaunchTime() =
+ testScope.runTest {
+ // No update to OOBE launch time to simulate no OOBE is launched yet
+ setUpForDeviceConnection()
+
+ val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+ val originalValue = model!!.signalCount
+ val listener = getOverviewProxyListener()
+ listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+
+ assertThat(model?.signalCount).isEqualTo(originalValue)
+ }
+
+ private suspend fun setUpForInitialDelayElapse() {
+ tutorialSchedulerRepository.updateLaunchTime(DeviceType.TOUCHPAD, eduClock.instant())
+ tutorialSchedulerRepository.updateLaunchTime(DeviceType.KEYBOARD, eduClock.instant())
+ eduClock.offset(initialDelayElapsedDuration)
+ }
+
+ fun logMetricsForToastEducation() =
+ testScope.runTest {
+ triggerMaxEducationSignals(gestureType)
+ runCurrent()
+
+ verify(kosmos.mockEduMetricsLogger)
+ .logContextualEducationTriggered(gestureType, EducationUiType.Toast)
+ }
+
+ @Test
+ fun logMetricsForNotificationEducation() =
+ testScope.runTest {
+ triggerMaxEducationSignals(gestureType)
+ runCurrent()
+
+ eduClock.offset(minDurationForNextEdu)
+ triggerMaxEducationSignals(gestureType)
+ runCurrent()
+
+ verify(kosmos.mockEduMetricsLogger)
+ .logContextualEducationTriggered(gestureType, EducationUiType.Notification)
+ }
+
+ @After
+ fun clear() {
+ testScope.launch { tutorialSchedulerRepository.clear() }
+ }
+
+ private suspend fun triggerMaxEducationSignals(gestureType: GestureType) {
+ // Increment max number of signal to try triggering education
+ for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) {
+ contextualEduInteractor.incrementSignalCount(gestureType)
+ }
+ }
+
+ private fun TestScope.setIsAnyTouchpadConnected(isConnected: Boolean) {
+ touchpadRepository.setIsAnyTouchpadConnected(isConnected)
+ runCurrent()
+ }
+
+ private fun TestScope.setIsAnyKeyboardConnected(isConnected: Boolean) {
+ keyboardRepository.setIsAnyKeyboardConnected(isConnected)
+ runCurrent()
+ }
+
+ private fun setUpForDeviceConnection() {
+ touchpadRepository.setIsAnyTouchpadConnected(true)
+ keyboardRepository.setIsAnyKeyboardConnected(true)
+ }
+
+ private fun getOverviewProxyListener(): OverviewProxyListener {
+ val listenerCaptor = argumentCaptor<OverviewProxyListener>()
+ verify(overviewProxyService).addCallback(listenerCaptor.capture())
+ return listenerCaptor.firstValue
+ }
+
+ companion object {
+ private val USER_INFOS = listOf(UserInfo(101, "Second User", 0))
+
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getGestureTypes(): List<GestureType> {
+ return listOf(BACK, HOME, OVERVIEW, ALL_APPS)
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
index 2a6d29c..580f631 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 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,19 +16,17 @@
package com.android.systemui.education.domain.interactor
-import android.content.pm.UserInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.contextualeducation.GestureType
-import com.android.systemui.contextualeducation.GestureType.ALL_APPS
import com.android.systemui.contextualeducation.GestureType.BACK
import com.android.systemui.contextualeducation.GestureType.HOME
import com.android.systemui.contextualeducation.GestureType.OVERVIEW
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
-import com.android.systemui.education.data.model.GestureEduModel
-import com.android.systemui.education.data.repository.contextualEducationRepository
import com.android.systemui.education.data.repository.fakeEduClock
+import com.android.systemui.education.shared.model.EducationInfo
import com.android.systemui.education.shared.model.EducationUiType
import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType
import com.android.systemui.inputdevice.tutorial.tutorialSchedulerRepository
@@ -37,50 +35,42 @@
import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
import com.android.systemui.testKosmos
import com.android.systemui.touchpad.data.repository.touchpadRepository
-import com.android.systemui.user.data.repository.fakeUserRepository
import com.google.common.truth.Truth.assertThat
-import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
-import org.junit.After
-import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.verify
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4
-import platform.test.runner.parameterized.Parameters
@SmallTest
-@RunWith(ParameterizedAndroidJunit4::class)
+@RunWith(AndroidJUnit4::class)
@kotlinx.coroutines.ExperimentalCoroutinesApi
-class KeyboardTouchpadEduInteractorTest(private val gestureType: GestureType) : SysuiTestCase() {
+class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
private val contextualEduInteractor = kosmos.contextualEducationInteractor
- private val repository = kosmos.contextualEducationRepository
private val touchpadRepository = kosmos.touchpadRepository
private val keyboardRepository = kosmos.keyboardRepository
private val tutorialSchedulerRepository = kosmos.tutorialSchedulerRepository
- private val userRepository = kosmos.fakeUserRepository
private val overviewProxyService = kosmos.mockOverviewProxyService
private val underTest: KeyboardTouchpadEduInteractor = kosmos.keyboardTouchpadEduInteractor
private val eduClock = kosmos.fakeEduClock
- private val minDurationForNextEdu =
- KeyboardTouchpadEduInteractor.minIntervalBetweenEdu + 1.seconds
private val initialDelayElapsedDuration =
KeyboardTouchpadEduInteractor.initialDelayDuration + 1.seconds
+ private val minIntervalForEduNotification =
+ KeyboardTouchpadEduInteractor.minIntervalBetweenEdu + 1.seconds
@Before
fun setup() {
underTest.start()
contextualEduInteractor.start()
- userRepository.setUserInfos(USER_INFOS)
testScope.launch {
contextualEduInteractor.updateKeyboardFirstConnectionTime()
contextualEduInteractor.updateTouchpadFirstConnectionTime()
@@ -88,312 +78,76 @@
}
@Test
- fun newEducationInfoOnMaxSignalCountReached() =
- testScope.runTest {
- triggerMaxEducationSignals(gestureType)
- val model by collectLastValue(underTest.educationTriggered)
-
- assertThat(model?.gestureType).isEqualTo(gestureType)
- }
-
- @Test
- fun newEducationToastOn1stEducation() =
- testScope.runTest {
- val model by collectLastValue(underTest.educationTriggered)
- triggerMaxEducationSignals(gestureType)
-
- assertThat(model?.educationUiType).isEqualTo(EducationUiType.Toast)
- }
-
- @Test
- fun newEducationNotificationOn2ndEducation() =
- testScope.runTest {
- val model by collectLastValue(underTest.educationTriggered)
- triggerMaxEducationSignals(gestureType)
- // runCurrent() to trigger 1st education
- runCurrent()
-
- eduClock.offset(minDurationForNextEdu)
- triggerMaxEducationSignals(gestureType)
-
- assertThat(model?.educationUiType).isEqualTo(EducationUiType.Notification)
- }
-
- @Test
- fun noEducationInfoBeforeMaxSignalCountReached() =
- testScope.runTest {
- contextualEduInteractor.incrementSignalCount(gestureType)
- val model by collectLastValue(underTest.educationTriggered)
- assertThat(model).isNull()
- }
-
- @Test
- fun noEducationInfoWhenShortcutTriggeredPreviously() =
- testScope.runTest {
- val model by collectLastValue(underTest.educationTriggered)
- contextualEduInteractor.updateShortcutTriggerTime(gestureType)
- triggerMaxEducationSignals(gestureType)
- assertThat(model).isNull()
- }
-
- @Test
- fun no2ndEducationBeforeMinEduIntervalReached() =
- testScope.runTest {
- val models by collectValues(underTest.educationTriggered)
- triggerMaxEducationSignals(gestureType)
- runCurrent()
-
- // Offset a duration that is less than the required education interval
- eduClock.offset(1.seconds)
- triggerMaxEducationSignals(gestureType)
- runCurrent()
-
- assertThat(models.filterNotNull().size).isEqualTo(1)
- }
-
- @Test
- fun noNewEducationInfoAfterMaxEducationCountReached() =
- testScope.runTest {
- val models by collectValues(underTest.educationTriggered)
- // Trigger 2 educations
- triggerMaxEducationSignals(gestureType)
- runCurrent()
- eduClock.offset(minDurationForNextEdu)
- triggerMaxEducationSignals(gestureType)
- runCurrent()
-
- // Try triggering 3rd education
- eduClock.offset(minDurationForNextEdu)
- triggerMaxEducationSignals(gestureType)
-
- assertThat(models.filterNotNull().size).isEqualTo(2)
- }
-
- @Test
- fun startNewUsageSessionWhen2ndSignalReceivedAfterSessionDeadline() =
- testScope.runTest {
- val model by
- collectLastValue(
- kosmos.contextualEducationRepository.readGestureEduModelFlow(gestureType)
- )
- contextualEduInteractor.incrementSignalCount(gestureType)
- eduClock.offset(KeyboardTouchpadEduInteractor.usageSessionDuration.plus(1.seconds))
- val secondSignalReceivedTime = eduClock.instant()
- contextualEduInteractor.incrementSignalCount(gestureType)
-
- assertThat(model)
- .isEqualTo(
- GestureEduModel(
- signalCount = 1,
- usageSessionStartTime = secondSignalReceivedTime,
- userId = 0,
- gestureType = gestureType,
- )
- )
- }
-
- @Test
- fun newTouchpadConnectionTimeOnFirstTouchpadConnected() =
- testScope.runTest {
- setIsAnyTouchpadConnected(true)
- val model = contextualEduInteractor.getEduDeviceConnectionTime()
- assertThat(model.touchpadFirstConnectionTime).isEqualTo(eduClock.instant())
- }
-
- @Test
- fun unchangedTouchpadConnectionTimeOnSecondConnection() =
- testScope.runTest {
- val firstConnectionTime = eduClock.instant()
- setIsAnyTouchpadConnected(true)
- setIsAnyTouchpadConnected(false)
-
- eduClock.offset(1.hours)
- setIsAnyTouchpadConnected(true)
-
- val model = contextualEduInteractor.getEduDeviceConnectionTime()
- assertThat(model.touchpadFirstConnectionTime).isEqualTo(firstConnectionTime)
- }
-
- @Test
- fun newTouchpadConnectionTimeOnUserChanged() =
- testScope.runTest {
- // Touchpad connected for user 0
- setIsAnyTouchpadConnected(true)
-
- // Change user
- eduClock.offset(1.hours)
- val newUserFirstConnectionTime = eduClock.instant()
- userRepository.setSelectedUserInfo(USER_INFOS[0])
- runCurrent()
-
- val model = contextualEduInteractor.getEduDeviceConnectionTime()
- assertThat(model.touchpadFirstConnectionTime).isEqualTo(newUserFirstConnectionTime)
- }
-
- @Test
- fun newKeyboardConnectionTimeOnKeyboardConnected() =
- testScope.runTest {
- setIsAnyKeyboardConnected(true)
- val model = contextualEduInteractor.getEduDeviceConnectionTime()
- assertThat(model.keyboardFirstConnectionTime).isEqualTo(eduClock.instant())
- }
-
- @Test
- fun unchangedKeyboardConnectionTimeOnSecondConnection() =
- testScope.runTest {
- val firstConnectionTime = eduClock.instant()
- setIsAnyKeyboardConnected(true)
- setIsAnyKeyboardConnected(false)
-
- eduClock.offset(1.hours)
- setIsAnyKeyboardConnected(true)
-
- val model = contextualEduInteractor.getEduDeviceConnectionTime()
- assertThat(model.keyboardFirstConnectionTime).isEqualTo(firstConnectionTime)
- }
-
- @Test
- fun newKeyboardConnectionTimeOnUserChanged() =
- testScope.runTest {
- // Keyboard connected for user 0
- setIsAnyKeyboardConnected(true)
-
- // Change user
- eduClock.offset(1.hours)
- val newUserFirstConnectionTime = eduClock.instant()
- userRepository.setSelectedUserInfo(USER_INFOS[0])
- runCurrent()
-
- val model = contextualEduInteractor.getEduDeviceConnectionTime()
- assertThat(model.keyboardFirstConnectionTime).isEqualTo(newUserFirstConnectionTime)
- }
-
- @Test
- fun updateShortcutTimeOnKeyboardShortcutTriggered() =
- testScope.runTest {
- // Only All Apps needs to update the keyboard shortcut
- assumeTrue(gestureType == ALL_APPS)
- kosmos.contextualEducationRepository.setKeyboardShortcutTriggered(ALL_APPS)
-
- val model by
- collectLastValue(
- kosmos.contextualEducationRepository.readGestureEduModelFlow(ALL_APPS)
- )
- assertThat(model?.lastShortcutTriggeredTime).isEqualTo(eduClock.instant())
- }
-
- @Test
- fun dataUpdatedOnIncrementSignalCountWhenTouchpadConnected() =
- testScope.runTest {
- assumeTrue(gestureType != ALL_APPS)
- setUpForInitialDelayElapse()
- touchpadRepository.setIsAnyTouchpadConnected(true)
-
- val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
- val originalValue = model!!.signalCount
- val listener = getOverviewProxyListener()
- listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
-
- assertThat(model?.signalCount).isEqualTo(originalValue + 1)
- }
-
- @Test
- fun dataUnchangedOnIncrementSignalCountWhenTouchpadDisconnected() =
- testScope.runTest {
- setUpForInitialDelayElapse()
- touchpadRepository.setIsAnyTouchpadConnected(false)
-
- val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
- val originalValue = model!!.signalCount
- val listener = getOverviewProxyListener()
- listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
-
- assertThat(model?.signalCount).isEqualTo(originalValue)
- }
-
- @Test
- fun dataUpdatedOnIncrementSignalCountWhenKeyboardConnected() =
- testScope.runTest {
- assumeTrue(gestureType == ALL_APPS)
- setUpForInitialDelayElapse()
- keyboardRepository.setIsAnyKeyboardConnected(true)
-
- val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
- val originalValue = model!!.signalCount
- val listener = getOverviewProxyListener()
- listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
-
- assertThat(model?.signalCount).isEqualTo(originalValue + 1)
- }
-
- @Test
- fun dataUnchangedOnIncrementSignalCountWhenKeyboardDisconnected() =
- testScope.runTest {
- setUpForInitialDelayElapse()
- keyboardRepository.setIsAnyKeyboardConnected(false)
-
- val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
- val originalValue = model!!.signalCount
- val listener = getOverviewProxyListener()
- listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
-
- assertThat(model?.signalCount).isEqualTo(originalValue)
- }
-
- @Test
- fun dataAddedOnUpdateShortcutTriggerTime() =
- testScope.runTest {
- val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
- assertThat(model?.lastShortcutTriggeredTime).isNull()
-
- val listener = getOverviewProxyListener()
- listener.updateContextualEduStats(/* isTrackpadGesture= */ true, gestureType)
-
- assertThat(model?.lastShortcutTriggeredTime).isEqualTo(kosmos.fakeEduClock.instant())
- }
-
- @Test
- fun dataUpdatedOnIncrementSignalCountAfterInitialDelay() =
+ fun newEducationToastBeforeMaxToastsPerSessionTriggered() =
testScope.runTest {
setUpForDeviceConnection()
- tutorialSchedulerRepository.updateLaunchTime(DeviceType.TOUCHPAD, eduClock.instant())
+ setUpForInitialDelayElapse()
+ val model by collectLastValue(underTest.educationTriggered)
- val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
- val originalValue = model!!.signalCount
- eduClock.offset(initialDelayElapsedDuration)
- val listener = getOverviewProxyListener()
- listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+ triggerEducation(HOME)
- assertThat(model?.signalCount).isEqualTo(originalValue + 1)
+ assertThat(model).isEqualTo(EducationInfo(HOME, EducationUiType.Toast, userId = 0))
}
@Test
- fun dataUnchangedOnIncrementSignalCountBeforeInitialDelay() =
+ fun noEducationToastAfterMaxToastsPerSessionTriggered() =
testScope.runTest {
setUpForDeviceConnection()
- tutorialSchedulerRepository.updateLaunchTime(DeviceType.TOUCHPAD, eduClock.instant())
+ setUpForInitialDelayElapse()
+ val models by collectValues(underTest.educationTriggered.filterNotNull())
+ // Show two toasts of other gestures
+ triggerEducation(HOME)
+ triggerEducation(BACK)
- val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
- val originalValue = model!!.signalCount
- // No offset to the clock to simulate update before initial delay
- val listener = getOverviewProxyListener()
- listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+ triggerEducation(OVERVIEW)
- assertThat(model?.signalCount).isEqualTo(originalValue)
+ // No new toast education besides the 2 triggered at first
+ val firstEdu = EducationInfo(HOME, EducationUiType.Toast, userId = 0)
+ val secondEdu = EducationInfo(BACK, EducationUiType.Toast, userId = 0)
+ assertThat(models).containsExactly(firstEdu, secondEdu).inOrder()
}
@Test
- fun dataUnchangedOnIncrementSignalCountWithoutOobeLaunchTime() =
+ fun newEducationToastAfterMinIntervalElapsedWhenMaxToastsPerSessionTriggered() =
testScope.runTest {
- // No update to OOBE launch time to simulate no OOBE is launched yet
setUpForDeviceConnection()
+ setUpForInitialDelayElapse()
+ val models by collectValues(underTest.educationTriggered.filterNotNull())
+ // Show two toasts of other gestures
+ triggerEducation(HOME)
+ triggerEducation(BACK)
- val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
- val originalValue = model!!.signalCount
- val listener = getOverviewProxyListener()
- listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+ // Trigger toast after an usage session has elapsed
+ eduClock.offset(KeyboardTouchpadEduInteractor.usageSessionDuration + 1.seconds)
+ triggerEducation(OVERVIEW)
- assertThat(model?.signalCount).isEqualTo(originalValue)
+ val firstEdu = EducationInfo(HOME, EducationUiType.Toast, userId = 0)
+ val secondEdu = EducationInfo(BACK, EducationUiType.Toast, userId = 0)
+ val thirdEdu = EducationInfo(OVERVIEW, EducationUiType.Toast, userId = 0)
+ assertThat(models).containsExactly(firstEdu, secondEdu, thirdEdu).inOrder()
+ }
+
+ @Test
+ fun newEducationNotificationAfterMaxToastsPerSessionTriggered() =
+ testScope.runTest {
+ setUpForDeviceConnection()
+ setUpForInitialDelayElapse()
+ val models by collectValues(underTest.educationTriggered.filterNotNull())
+ triggerEducation(BACK)
+
+ // Offset to let min interval for notification elapse so we could show edu notification
+ // for BACK. It would be a new usage session too because the interval (7 days) is
+ // longer than a usage session (3 days)
+ eduClock.offset(minIntervalForEduNotification)
+ triggerEducation(HOME)
+ triggerEducation(OVERVIEW)
+ triggerEducation(BACK)
+
+ val firstEdu = EducationInfo(BACK, EducationUiType.Toast, userId = 0)
+ val secondEdu = EducationInfo(HOME, EducationUiType.Toast, userId = 0)
+ val thirdEdu = EducationInfo(OVERVIEW, EducationUiType.Toast, userId = 0)
+ val fourthEdu = EducationInfo(BACK, EducationUiType.Notification, userId = 0)
+ assertThat(models).containsExactly(firstEdu, secondEdu, thirdEdu, fourthEdu).inOrder()
}
private suspend fun setUpForInitialDelayElapse() {
@@ -402,51 +156,6 @@
eduClock.offset(initialDelayElapsedDuration)
}
- fun logMetricsForToastEducation() =
- testScope.runTest {
- triggerMaxEducationSignals(gestureType)
- runCurrent()
-
- verify(kosmos.mockEduMetricsLogger)
- .logContextualEducationTriggered(gestureType, EducationUiType.Toast)
- }
-
- @Test
- fun logMetricsForNotificationEducation() =
- testScope.runTest {
- triggerMaxEducationSignals(gestureType)
- runCurrent()
-
- eduClock.offset(minDurationForNextEdu)
- triggerMaxEducationSignals(gestureType)
- runCurrent()
-
- verify(kosmos.mockEduMetricsLogger)
- .logContextualEducationTriggered(gestureType, EducationUiType.Notification)
- }
-
- @After
- fun clear() {
- testScope.launch { tutorialSchedulerRepository.clear() }
- }
-
- private suspend fun triggerMaxEducationSignals(gestureType: GestureType) {
- // Increment max number of signal to try triggering education
- for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) {
- contextualEduInteractor.incrementSignalCount(gestureType)
- }
- }
-
- private fun TestScope.setIsAnyTouchpadConnected(isConnected: Boolean) {
- touchpadRepository.setIsAnyTouchpadConnected(isConnected)
- runCurrent()
- }
-
- private fun TestScope.setIsAnyKeyboardConnected(isConnected: Boolean) {
- keyboardRepository.setIsAnyKeyboardConnected(isConnected)
- runCurrent()
- }
-
private fun setUpForDeviceConnection() {
touchpadRepository.setIsAnyTouchpadConnected(true)
keyboardRepository.setIsAnyKeyboardConnected(true)
@@ -458,13 +167,12 @@
return listenerCaptor.firstValue
}
- companion object {
- private val USER_INFOS = listOf(UserInfo(101, "Second User", 0))
-
- @JvmStatic
- @Parameters(name = "{0}")
- fun getGestureTypes(): List<GestureType> {
- return listOf(BACK, HOME, OVERVIEW, ALL_APPS)
+ private fun TestScope.triggerEducation(gestureType: GestureType) {
+ // Increment max number of signal to try triggering education
+ for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) {
+ val listener = getOverviewProxyListener()
+ listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
}
+ runCurrent()
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
index d88d69d..d2317e4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
@@ -22,7 +22,10 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.filter
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
@@ -182,11 +185,14 @@
}
private fun ComposeContentTestRule.assertTileGridContainsExactly(specs: List<String>) {
- onNodeWithTag(CURRENT_TILES_GRID_TEST_TAG).onChildren().apply {
- fetchSemanticsNodes().forEachIndexed { index, _ ->
- get(index).assert(hasContentDescription(specs[index]))
+ onNodeWithTag(CURRENT_TILES_GRID_TEST_TAG)
+ .onChildren()
+ .filter(SemanticsMatcher.keyIsDefined(SemanticsProperties.ContentDescription))
+ .apply {
+ fetchSemanticsNodes().forEachIndexed { index, _ ->
+ get(index).assert(hasContentDescription(specs[index]))
+ }
}
- }
}
companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt
index 9a924ed..d090c01 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt
@@ -22,8 +22,10 @@
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.QsEventLogger
+import com.android.systemui.qs.flags.QsInCompose.isEnabled
import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIconWithRes
import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.BluetoothController
import com.android.systemui.util.mockito.any
@@ -81,7 +83,7 @@
qsLogger,
bluetoothController,
featureFlags,
- bluetoothTileDialogViewModel
+ bluetoothTileDialogViewModel,
)
tile.initialize()
@@ -109,8 +111,7 @@
tile.handleUpdateState(state, /* arg= */ null)
- assertThat(state.icon)
- .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_bluetooth_icon_off))
+ assertThat(state.icon).isEqualTo(createExpectedIcon(R.drawable.qs_bluetooth_icon_off))
}
@Test
@@ -121,8 +122,7 @@
tile.handleUpdateState(state, /* arg= */ null)
- assertThat(state.icon)
- .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_bluetooth_icon_off))
+ assertThat(state.icon).isEqualTo(createExpectedIcon(R.drawable.qs_bluetooth_icon_off))
}
@Test
@@ -133,8 +133,7 @@
tile.handleUpdateState(state, /* arg= */ null)
- assertThat(state.icon)
- .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_bluetooth_icon_on))
+ assertThat(state.icon).isEqualTo(createExpectedIcon(R.drawable.qs_bluetooth_icon_on))
}
@Test
@@ -145,8 +144,7 @@
tile.handleUpdateState(state, /* arg= */ null)
- assertThat(state.icon)
- .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_bluetooth_icon_search))
+ assertThat(state.icon).isEqualTo(createExpectedIcon(R.drawable.qs_bluetooth_icon_search))
}
@Test
@@ -161,11 +159,10 @@
.isEqualTo(
mContext.getString(
R.string.quick_settings_bluetooth_secondary_label_battery_level,
- Utils.formatPercentage(50)
+ Utils.formatPercentage(50),
)
)
- verify(bluetoothController)
- .addOnMetadataChangedListener(eq(cachedDevice), any(), any())
+ verify(bluetoothController).addOnMetadataChangedListener(eq(cachedDevice), any(), any())
}
@Test
@@ -186,7 +183,7 @@
.isEqualTo(
mContext.getString(
R.string.quick_settings_bluetooth_secondary_label_battery_level,
- Utils.formatPercentage(25)
+ Utils.formatPercentage(25),
)
)
verify(bluetoothController, times(1))
@@ -197,7 +194,7 @@
fun handleClick_hasSatelliteFeatureButNoQsTileDialogAndClickIsProcessing_doNothing() {
mSetFlagsRule.enableFlags(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
`when`(featureFlags.isEnabled(com.android.systemui.flags.Flags.BLUETOOTH_QS_TILE_DIALOG))
- .thenReturn(false)
+ .thenReturn(false)
`when`(clickJob.isCompleted).thenReturn(false)
tile.mClickJob = clickJob
@@ -210,7 +207,7 @@
fun handleClick_noSatelliteFeatureAndNoQsTileDialog_directSetBtEnable() {
mSetFlagsRule.disableFlags(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
`when`(featureFlags.isEnabled(com.android.systemui.flags.Flags.BLUETOOTH_QS_TILE_DIALOG))
- .thenReturn(false)
+ .thenReturn(false)
tile.handleClick(null)
@@ -221,7 +218,7 @@
fun handleClick_noSatelliteFeatureButHasQsTileDialog_showDialog() {
mSetFlagsRule.disableFlags(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
`when`(featureFlags.isEnabled(com.android.systemui.flags.Flags.BLUETOOTH_QS_TILE_DIALOG))
- .thenReturn(true)
+ .thenReturn(true)
tile.handleClick(null)
@@ -265,7 +262,7 @@
qsLogger: QSLogger,
bluetoothController: BluetoothController,
featureFlags: FeatureFlagsClassic,
- bluetoothTileDialogViewModel: BluetoothTileDialogViewModel
+ bluetoothTileDialogViewModel: BluetoothTileDialogViewModel,
) :
BluetoothTile(
qsHost,
@@ -279,13 +276,13 @@
qsLogger,
bluetoothController,
featureFlags,
- bluetoothTileDialogViewModel
+ bluetoothTileDialogViewModel,
) {
var restrictionChecked: String? = null
override fun checkIfRestrictionEnforcedByAdminOnly(
state: QSTile.State?,
- userRestriction: String?
+ userRestriction: String?,
) {
restrictionChecked = userRestriction
}
@@ -321,7 +318,7 @@
fun listenToDeviceMetadata(
state: QSTile.BooleanState,
cachedDevice: CachedBluetoothDevice,
- batteryLevel: Int
+ batteryLevel: Int,
) {
val btDevice = mock<BluetoothDevice>()
whenever(cachedDevice.device).thenReturn(btDevice)
@@ -332,4 +329,12 @@
addConnectedDevice(cachedDevice)
tile.handleUpdateState(state, /* arg= */ null)
}
+
+ private fun createExpectedIcon(resId: Int): QSTile.Icon {
+ return if (isEnabled) {
+ DrawableIconWithRes(mContext.getDrawable(resId), resId)
+ } else {
+ QSTileImpl.ResourceIcon.get(resId)
+ }
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
index 6a43a61..56b7631 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
@@ -29,7 +29,6 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.MetricsLogger
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.animation.Expandable
@@ -39,14 +38,18 @@
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.QsEventLogger
+import com.android.systemui.qs.flags.QsInCompose.isEnabled
import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIconWithRes
+import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.ZenModeController
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.settings.FakeSettings
import com.android.systemui.util.settings.SecureSettings
import com.google.common.truth.Truth.assertThat
+import java.io.File
import org.junit.After
import org.junit.Before
import org.junit.Test
@@ -55,9 +58,8 @@
import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-import java.io.File
import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -70,41 +72,29 @@
private const val KEY = Settings.Secure.ZEN_DURATION
}
- @Mock
- private lateinit var qsHost: QSHost
+ @Mock private lateinit var qsHost: QSHost
- @Mock
- private lateinit var metricsLogger: MetricsLogger
+ @Mock private lateinit var metricsLogger: MetricsLogger
- @Mock
- private lateinit var statusBarStateController: StatusBarStateController
+ @Mock private lateinit var statusBarStateController: StatusBarStateController
- @Mock
- private lateinit var activityStarter: ActivityStarter
+ @Mock private lateinit var activityStarter: ActivityStarter
- @Mock
- private lateinit var qsLogger: QSLogger
+ @Mock private lateinit var qsLogger: QSLogger
- @Mock
- private lateinit var uiEventLogger: QsEventLogger
+ @Mock private lateinit var uiEventLogger: QsEventLogger
- @Mock
- private lateinit var zenModeController: ZenModeController
+ @Mock private lateinit var zenModeController: ZenModeController
- @Mock
- private lateinit var sharedPreferences: SharedPreferences
+ @Mock private lateinit var sharedPreferences: SharedPreferences
- @Mock
- private lateinit var mDialogTransitionAnimator: DialogTransitionAnimator
+ @Mock private lateinit var mDialogTransitionAnimator: DialogTransitionAnimator
- @Mock
- private lateinit var hostDialog: Dialog
+ @Mock private lateinit var hostDialog: Dialog
- @Mock
- private lateinit var expandable: Expandable
+ @Mock private lateinit var expandable: Expandable
- @Mock
- private lateinit var controller: DialogTransitionAnimator.Controller
+ @Mock private lateinit var controller: DialogTransitionAnimator.Controller
private lateinit var secureSettings: SecureSettings
private lateinit var testableLooper: TestableLooper
@@ -118,31 +108,32 @@
whenever(qsHost.userId).thenReturn(DEFAULT_USER)
- val wrappedContext = object : ContextWrapper(
- ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)
- ) {
- override fun getSharedPreferences(file: File?, mode: Int): SharedPreferences {
- return sharedPreferences
+ val wrappedContext =
+ object :
+ ContextWrapper(ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)) {
+ override fun getSharedPreferences(file: File?, mode: Int): SharedPreferences {
+ return sharedPreferences
+ }
}
- }
whenever(qsHost.context).thenReturn(wrappedContext)
whenever(expandable.dialogTransitionController(any())).thenReturn(controller)
- tile = DndTile(
- qsHost,
- uiEventLogger,
- testableLooper.looper,
- Handler(testableLooper.looper),
- FalsingManagerFake(),
- metricsLogger,
- statusBarStateController,
- activityStarter,
- qsLogger,
- zenModeController,
- sharedPreferences,
- secureSettings,
- mDialogTransitionAnimator
- )
+ tile =
+ DndTile(
+ qsHost,
+ uiEventLogger,
+ testableLooper.looper,
+ Handler(testableLooper.looper),
+ FalsingManagerFake(),
+ metricsLogger,
+ statusBarStateController,
+ activityStarter,
+ qsLogger,
+ zenModeController,
+ sharedPreferences,
+ secureSettings,
+ mDialogTransitionAnimator,
+ )
}
@After
@@ -222,7 +213,7 @@
tile.handleUpdateState(state, /* arg= */ null)
- assertThat(state.icon).isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_dnd_icon_off))
+ assertThat(state.icon).isEqualTo(createExpectedIcon(R.drawable.qs_dnd_icon_off))
}
@Test
@@ -232,6 +223,14 @@
tile.handleUpdateState(state, /* arg= */ null)
- assertThat(state.icon).isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_dnd_icon_on))
+ assertThat(state.icon).isEqualTo(createExpectedIcon(R.drawable.qs_dnd_icon_on))
+ }
+
+ private fun createExpectedIcon(resId: Int): QSTile.Icon {
+ return if (isEnabled) {
+ DrawableIconWithRes(mContext.getDrawable(resId), resId)
+ } else {
+ QSTileImpl.ResourceIcon.get(resId)
+ }
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DreamTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DreamTileTest.java
index 190d80f..f043f63 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DreamTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DreamTileTest.java
@@ -45,9 +45,11 @@
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.classifier.FalsingManagerFake;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.QsEventLogger;
+import com.android.systemui.qs.flags.QsInCompose;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.res.R;
@@ -246,13 +248,13 @@
dockIntent.putExtra(Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_DESK);
receiver.onReceive(mContext, dockIntent);
mTestableLooper.processAllMessages();
- assertEquals(QSTileImpl.ResourceIcon.get(R.drawable.ic_qs_screen_saver),
+ assertEquals(createExpectedIcon(R.drawable.ic_qs_screen_saver),
dockedTile.getState().icon);
dockIntent.putExtra(Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_UNDOCKED);
receiver.onReceive(mContext, dockIntent);
mTestableLooper.processAllMessages();
- assertEquals(QSTileImpl.ResourceIcon.get(R.drawable.ic_qs_screen_saver_undocked),
+ assertEquals(createExpectedIcon(R.drawable.ic_qs_screen_saver_undocked),
dockedTile.getState().icon);
destroyTile(dockedTile);
@@ -268,6 +270,14 @@
mTestableLooper.processAllMessages();
}
+ private QSTile.Icon createExpectedIcon(int resId) {
+ if (QsInCompose.isEnabled()) {
+ return new QSTileImpl.DrawableIconWithRes(mContext.getDrawable(resId), resId);
+ } else {
+ return QSTileImpl.ResourceIcon.get(resId);
+ }
+ }
+
private DreamTile constructTileForTest(boolean dreamSupported,
boolean dreamOnlyEnabledForSystemUser) {
return new DreamTile(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HotspotTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HotspotTileTest.java
index 5bd6944..2b4cf5d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HotspotTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HotspotTileTest.java
@@ -38,6 +38,7 @@
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.QsEventLogger;
+import com.android.systemui.qs.flags.QsInCompose;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.res.R;
@@ -144,7 +145,7 @@
mTile.handleUpdateState(state, /* arg= */ null);
assertThat(state.icon)
- .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_hotspot_icon_off));
+ .isEqualTo(createExpectedIcon(R.drawable.qs_hotspot_icon_off));
}
@Test
@@ -156,7 +157,7 @@
mTile.handleUpdateState(state, /* arg= */ null);
assertThat(state.icon)
- .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_hotspot_icon_search));
+ .isEqualTo(createExpectedIcon(R.drawable.qs_hotspot_icon_search));
}
@Test
@@ -168,6 +169,14 @@
mTile.handleUpdateState(state, /* arg= */ null);
assertThat(state.icon)
- .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_hotspot_icon_on));
+ .isEqualTo(createExpectedIcon(R.drawable.qs_hotspot_icon_on));
+ }
+
+ private QSTile.Icon createExpectedIcon(int resId) {
+ if (QsInCompose.isEnabled()) {
+ return new QSTileImpl.DrawableIconWithRes(mContext.getDrawable(resId), resId);
+ } else {
+ return QSTileImpl.ResourceIcon.get(resId);
+ }
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt
index 1df3ef4..1021169 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt
@@ -17,9 +17,11 @@
package com.android.systemui.education.data.repository
import com.android.systemui.kosmos.Kosmos
+import java.time.Duration
import java.time.Instant
var Kosmos.contextualEducationRepository: FakeContextualEducationRepository by
Kosmos.Fixture { FakeContextualEducationRepository() }
-var Kosmos.fakeEduClock: FakeEduClock by Kosmos.Fixture { FakeEduClock(Instant.MIN) }
+var Kosmos.fakeEduClock: FakeEduClock by
+ Kosmos.Fixture { FakeEduClock(Instant.ofEpochSecond(Duration.ofDays(30).seconds)) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt
index 597d52d..bc1c60c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt
@@ -16,6 +16,8 @@
package com.android.systemui.qs.tiles.base.interactor
+import com.android.systemui.plugins.qs.TileDetailsViewModel
+import com.android.systemui.qs.FakeTileDetailsViewModel
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -31,4 +33,7 @@
override suspend fun handleInput(input: QSTileInput<T>) {
mutex.withLock { mutableInputs.add(input) }
}
+
+ override var detailsViewModel: TileDetailsViewModel? =
+ FakeTileDetailsViewModel("FakeQSTileUserActionInteractor")
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelKosmos.kt
index c8ba551..34661ce 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelKosmos.kt
@@ -21,6 +21,7 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.statusbar.notification.domain.interactor.notificationsSoundPolicyInteractor
import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor
import com.android.systemui.volume.dialog.ringer.domain.volumeDialogRingerInteractor
import com.android.systemui.volume.dialog.shared.volumeDialogLogger
@@ -31,7 +32,8 @@
applicationContext = applicationContext,
backgroundDispatcher = testDispatcher,
coroutineScope = applicationCoroutineScope,
- interactor = volumeDialogRingerInteractor,
+ soundPolicyInteractor = notificationsSoundPolicyInteractor,
+ ringerInteractor = volumeDialogRingerInteractor,
vibrator = vibratorHelper,
volumeDialogLogger = volumeDialogLogger,
visibilityInteractor = volumeDialogVisibilityInteractor,
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index eba9a25..bd7a0ac 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -449,6 +449,8 @@
private AtomicBoolean mIsSatelliteEnabled;
private AtomicBoolean mWasSatelliteEnabledNotified;
+ private final int mPid = Process.myPid();
+
/**
* Per-phone map of precise data connection state. The key of the map is the pair of transport
* type and APN setting. This is the cache to prevent redundant callbacks to the listeners.
@@ -1441,7 +1443,17 @@
}
if (events.contains(TelephonyCallback.EVENT_CALL_ATTRIBUTES_CHANGED)) {
try {
- r.callback.onCallStatesChanged(mCallStateLists.get(r.phoneId));
+ if (Flags.passCopiedCallStateList()) {
+ List<CallState> callList;
+ if (r.callerPid == mPid) {
+ callList = List.copyOf(mCallStateLists.get(r.phoneId));
+ } else {
+ callList = mCallStateLists.get(r.phoneId);
+ }
+ r.callback.onCallStatesChanged(callList);
+ } else {
+ r.callback.onCallStatesChanged(mCallStateLists.get(r.phoneId));
+ }
} catch (RemoteException ex) {
remove(r.binder);
}
@@ -2573,12 +2585,25 @@
}
if (notifyCallState) {
+ List<CallState> copyList = null;
for (Record r : mRecords) {
if (r.matchTelephonyCallbackEvent(
TelephonyCallback.EVENT_CALL_ATTRIBUTES_CHANGED)
&& idMatch(r, subId, phoneId)) {
+ // If listener is in the same process, original instance can be passed
+ // to the listener via AIDL without serialization/de-serialization. We
+ // will pass the copied list. Since the element is newly created instead
+ // of modification for the change, we can use shallow copy for this.
try {
- r.callback.onCallStatesChanged(mCallStateLists.get(phoneId));
+ if (Flags.passCopiedCallStateList()) {
+ if (r.callerPid == mPid && copyList == null) {
+ copyList = List.copyOf(mCallStateLists.get(phoneId));
+ }
+ r.callback.onCallStatesChanged(copyList == null
+ ? mCallStateLists.get(phoneId) : copyList);
+ } else {
+ r.callback.onCallStatesChanged(mCallStateLists.get(phoneId));
+ }
} catch (RemoteException ex) {
mRemoveList.add(r.binder);
}
@@ -2906,13 +2931,21 @@
log("There is no active call to report CallQuality");
return;
}
-
+ List<CallState> copyList = null;
for (Record r : mRecords) {
if (r.matchTelephonyCallbackEvent(
TelephonyCallback.EVENT_CALL_ATTRIBUTES_CHANGED)
&& idMatch(r, subId, phoneId)) {
try {
- r.callback.onCallStatesChanged(mCallStateLists.get(phoneId));
+ if (Flags.passCopiedCallStateList()) {
+ if (r.callerPid == mPid && copyList == null) {
+ copyList = List.copyOf(mCallStateLists.get(phoneId));
+ }
+ r.callback.onCallStatesChanged(copyList == null
+ ? mCallStateLists.get(phoneId) : copyList);
+ } else {
+ r.callback.onCallStatesChanged(mCallStateLists.get(phoneId));
+ }
} catch (RemoteException ex) {
mRemoveList.add(r.binder);
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 06883e8..cd929c1 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -361,6 +361,8 @@
import android.os.incremental.IIncrementalService;
import android.os.incremental.IncrementalManager;
import android.os.incremental.IncrementalMetrics;
+import android.os.instrumentation.IOffsetCallback;
+import android.os.instrumentation.MethodDescriptor;
import android.os.storage.IStorageManager;
import android.os.storage.StorageManager;
import android.provider.DeviceConfig;
@@ -509,6 +511,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
@@ -18042,6 +18045,26 @@
}
@Override
+ public void getExecutableMethodFileOffsets(@NonNull String processName,
+ int pid, int uid, @NonNull MethodDescriptor methodDescriptor,
+ @NonNull IOffsetCallback callback) {
+ final IApplicationThread thread;
+ synchronized (ActivityManagerService.this) {
+ ProcessRecord record = mProcessList.getProcessRecordLocked(processName, uid);
+ if (record == null || record.getPid() != pid) {
+ throw new NoSuchElementException();
+ }
+ thread = record.getThread();
+ }
+ try {
+ thread.getExecutableMethodFileOffsets(methodDescriptor, callback);
+ } catch (RemoteException e) {
+ throw new RuntimeException(
+ "IApplicationThread.getExecutableMethodFileOffsets failed", e);
+ }
+ }
+
+ @Override
public void addCreatorToken(Intent intent, String creatorPackage) {
ActivityManagerService.this.addCreatorToken(intent, creatorPackage);
}
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
index 0b47a61..d916eda 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -798,7 +798,7 @@
throws RemoteException {
super.registerEndpoint_enforcePermission();
if (mEndpointManager == null) {
- Log.e(TAG, "ContextHubService.registerEndpoint: endpoint manager failed to initialize");
+ Log.e(TAG, "Endpoint manager failed to initialize");
throw new UnsupportedOperationException("Endpoint registration is not supported");
}
return mEndpointManager.registerEndpoint(pendingHubEndpointInfo, callback);
@@ -809,7 +809,8 @@
public void registerEndpointDiscoveryCallbackId(
long endpointId, IContextHubEndpointDiscoveryCallback callback) throws RemoteException {
super.registerEndpointDiscoveryCallbackId_enforcePermission();
- // TODO(b/375487784): Implement this
+ checkEndpointDiscoveryPreconditions();
+ mHubInfoRegistry.registerEndpointDiscoveryCallback(endpointId, callback);
}
@android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@@ -818,7 +819,8 @@
String serviceDescriptor, IContextHubEndpointDiscoveryCallback callback)
throws RemoteException {
super.registerEndpointDiscoveryCallbackDescriptor_enforcePermission();
- // TODO(b/375487784): Implement this
+ checkEndpointDiscoveryPreconditions();
+ mHubInfoRegistry.registerEndpointDiscoveryCallback(serviceDescriptor, callback);
}
@android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@@ -826,7 +828,15 @@
public void unregisterEndpointDiscoveryCallback(IContextHubEndpointDiscoveryCallback callback)
throws RemoteException {
super.unregisterEndpointDiscoveryCallback_enforcePermission();
- // TODO(b/375487784): Implement this
+ checkEndpointDiscoveryPreconditions();
+ mHubInfoRegistry.unregisterEndpointDiscoveryCallback(callback);
+ }
+
+ private void checkEndpointDiscoveryPreconditions() {
+ if (mHubInfoRegistry == null) {
+ Log.e(TAG, "Hub endpoint registry failed to initialize");
+ throw new UnsupportedOperationException("Endpoint discovery is not supported");
+ }
}
/**
diff --git a/services/core/java/com/android/server/location/contexthub/HubInfoRegistry.java b/services/core/java/com/android/server/location/contexthub/HubInfoRegistry.java
index b912492..6f5f191 100644
--- a/services/core/java/com/android/server/location/contexthub/HubInfoRegistry.java
+++ b/services/core/java/com/android/server/location/contexthub/HubInfoRegistry.java
@@ -18,7 +18,9 @@
import android.hardware.contexthub.HubEndpointInfo;
import android.hardware.contexthub.HubServiceInfo;
+import android.hardware.contexthub.IContextHubEndpointDiscoveryCallback;
import android.hardware.location.HubInfo;
+import android.os.DeadObjectException;
import android.os.RemoteException;
import android.util.ArrayMap;
import android.util.IndentingPrintWriter;
@@ -29,6 +31,9 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.BiConsumer;
class HubInfoRegistry implements ContextHubHalEndpointCallback.IEndpointLifecycleCallback {
private static final String TAG = "HubInfoRegistry";
@@ -43,6 +48,56 @@
private final ArrayMap<HubEndpointInfo.HubEndpointIdentifier, HubEndpointInfo>
mHubEndpointInfos = new ArrayMap<>();
+ /**
+ * A wrapper class that is used to store arguments to
+ * ContextHubManager.registerEndpointCallback.
+ */
+ private static class DiscoveryCallback {
+ private final IContextHubEndpointDiscoveryCallback mCallback;
+ private final Optional<Long> mEndpointId;
+ private final Optional<String> mServiceDescriptor;
+
+ DiscoveryCallback(IContextHubEndpointDiscoveryCallback callback, long endpointId) {
+ mCallback = callback;
+ mEndpointId = Optional.of(endpointId);
+ mServiceDescriptor = Optional.empty();
+ }
+
+ DiscoveryCallback(IContextHubEndpointDiscoveryCallback callback, String serviceDescriptor) {
+ mCallback = callback;
+ mEndpointId = Optional.empty();
+ mServiceDescriptor = Optional.of(serviceDescriptor);
+ }
+
+ public IContextHubEndpointDiscoveryCallback getCallback() {
+ return mCallback;
+ }
+
+ /**
+ * @param info The hub endpoint info to check
+ * @return true if info matches
+ */
+ public boolean isMatch(HubEndpointInfo info) {
+ if (mEndpointId.isPresent()) {
+ return mEndpointId.get() == info.getIdentifier().getEndpoint();
+ }
+ if (mServiceDescriptor.isPresent()) {
+ for (HubServiceInfo serviceInfo : info.getServiceInfoCollection()) {
+ if (mServiceDescriptor.get().equals(serviceInfo.getServiceDescriptor())) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ }
+
+ /* The list of discovery callbacks registered with the service */
+ @GuardedBy("mCallbackLock")
+ private final List<DiscoveryCallback> mEndpointDiscoveryCallbacks = new ArrayList<>();
+
+ private final Object mCallbackLock = new Object();
+
HubInfoRegistry(IContextHubWrapper contextHubWrapper) {
mContextHubWrapper = contextHubWrapper;
refreshCachedHubs();
@@ -109,16 +164,50 @@
mHubEndpointInfos.put(endpointInfo.getIdentifier(), endpointInfo);
}
}
+
+ invokeForMatchingEndpoints(
+ endpointInfos,
+ (cb, infoList) -> {
+ try {
+ cb.onEndpointsStarted(infoList);
+ } catch (RemoteException e) {
+ if (e instanceof DeadObjectException) {
+ Log.w(TAG, "onEndpointStarted: callback died, unregistering");
+ unregisterEndpointDiscoveryCallback(cb);
+ } else {
+ Log.e(TAG, "Exception while calling onEndpointsStarted", e);
+ }
+ }
+ });
}
@Override
public void onEndpointStopped(
HubEndpointInfo.HubEndpointIdentifier[] endpointIds, byte reason) {
+ ArrayList<HubEndpointInfo> removedInfoList = new ArrayList<>();
synchronized (mLock) {
for (HubEndpointInfo.HubEndpointIdentifier endpointId : endpointIds) {
- mHubEndpointInfos.remove(endpointId);
+ HubEndpointInfo info = mHubEndpointInfos.remove(endpointId);
+ if (info != null) {
+ removedInfoList.add(info);
+ }
}
}
+
+ invokeForMatchingEndpoints(
+ removedInfoList.toArray(new HubEndpointInfo[removedInfoList.size()]),
+ (cb, infoList) -> {
+ try {
+ cb.onEndpointsStopped(infoList, reason);
+ } catch (RemoteException e) {
+ if (e instanceof DeadObjectException) {
+ Log.w(TAG, "onEndpointStopped: callback died, unregistering");
+ unregisterEndpointDiscoveryCallback(cb);
+ } else {
+ Log.e(TAG, "Exception while calling onEndpointsStopped", e);
+ }
+ }
+ });
}
/** Return a list of {@link HubEndpointInfo} that represents endpoints with the matching id. */
@@ -151,6 +240,77 @@
return searchResult;
}
+ /* package */
+ void registerEndpointDiscoveryCallback(
+ long endpointId, IContextHubEndpointDiscoveryCallback callback) {
+ Objects.requireNonNull(callback, "callback cannot be null");
+ synchronized (mCallbackLock) {
+ checkCallbackAlreadyRegistered(callback);
+ mEndpointDiscoveryCallbacks.add(new DiscoveryCallback(callback, endpointId));
+ }
+ }
+
+ /* package */
+ void registerEndpointDiscoveryCallback(
+ String serviceDescriptor, IContextHubEndpointDiscoveryCallback callback) {
+ Objects.requireNonNull(callback, "callback cannot be null");
+ synchronized (mCallbackLock) {
+ checkCallbackAlreadyRegistered(callback);
+ mEndpointDiscoveryCallbacks.add(new DiscoveryCallback(callback, serviceDescriptor));
+ }
+ }
+
+ /* package */
+ void unregisterEndpointDiscoveryCallback(IContextHubEndpointDiscoveryCallback callback) {
+ Objects.requireNonNull(callback, "callback cannot be null");
+ synchronized (mCallbackLock) {
+ for (DiscoveryCallback discoveryCallback : mEndpointDiscoveryCallbacks) {
+ if (discoveryCallback.getCallback().asBinder() == callback.asBinder()) {
+ mEndpointDiscoveryCallbacks.remove(discoveryCallback);
+ break;
+ }
+ }
+ }
+ }
+
+ private void checkCallbackAlreadyRegistered(
+ IContextHubEndpointDiscoveryCallback callback) {
+ synchronized (mCallbackLock) {
+ for (DiscoveryCallback discoveryCallback : mEndpointDiscoveryCallbacks) {
+ if (discoveryCallback.mCallback.asBinder() == callback.asBinder()) {
+ throw new IllegalArgumentException("Callback is already registered");
+ }
+ }
+ }
+ }
+
+ /**
+ * Iterates through all registered discovery callbacks and invokes a given callback for those
+ * that match the endpoints the callback is targeted for.
+ *
+ * @param endpointInfos The list of endpoint infos to check for a match.
+ * @param consumer The callback to invoke, which consumes the callback object and the list of
+ * matched endpoint infos.
+ */
+ private void invokeForMatchingEndpoints(
+ HubEndpointInfo[] endpointInfos,
+ BiConsumer<IContextHubEndpointDiscoveryCallback, HubEndpointInfo[]> consumer) {
+ synchronized (mCallbackLock) {
+ for (DiscoveryCallback discoveryCallback : mEndpointDiscoveryCallbacks) {
+ ArrayList<HubEndpointInfo> infoList = new ArrayList<>();
+ for (HubEndpointInfo endpointInfo : endpointInfos) {
+ if (discoveryCallback.isMatch(endpointInfo)) {
+ infoList.add(endpointInfo);
+ }
+ }
+
+ consumer.accept(
+ discoveryCallback.getCallback(),
+ infoList.toArray(new HubEndpointInfo[infoList.size()]));
+ }
+ }
+ }
+
void dump(IndentingPrintWriter ipw) {
synchronized (mLock) {
dumpLocked(ipw);
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java
index 849751b..1673b8e 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityService.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java
@@ -32,6 +32,7 @@
import android.media.quality.SoundProfile;
import android.media.quality.SoundProfileHandle;
import android.os.PersistableBundle;
+import android.os.UserHandle;
import android.util.Log;
import com.android.server.SystemService;
@@ -81,7 +82,7 @@
private final class BinderService extends IMediaQualityManager.Stub {
@Override
- public PictureProfile createPictureProfile(PictureProfile pp, int userId) {
+ public PictureProfile createPictureProfile(PictureProfile pp, UserHandle user) {
SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
@@ -100,12 +101,12 @@
}
@Override
- public void updatePictureProfile(String id, PictureProfile pp, int userId) {
+ public void updatePictureProfile(String id, PictureProfile pp, UserHandle user) {
// TODO: implement
}
@Override
- public void removePictureProfile(String id, int userId) {
+ public void removePictureProfile(String id, UserHandle user) {
Long intId = mPictureProfileTempIdMap.inverse().get(id);
if (intId != null) {
SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase();
@@ -118,7 +119,8 @@
}
@Override
- public PictureProfile getPictureProfile(int type, String name, int userId) {
+ public PictureProfile getPictureProfile(int type, String name, boolean includeParams,
+ UserHandle user) {
String selection = BaseParameters.PARAMETER_TYPE + " = ? AND "
+ BaseParameters.PARAMETER_NAME + " = ?";
String[] selectionArguments = {Integer.toString(type), name};
@@ -144,7 +146,8 @@
}
@Override
- public List<PictureProfile> getPictureProfilesByPackage(String packageName, int userId) {
+ public List<PictureProfile> getPictureProfilesByPackage(
+ String packageName, boolean includeParams, UserHandle user) {
String selection = BaseParameters.PARAMETER_PACKAGE + " = ?";
String[] selectionArguments = {packageName};
return getPictureProfilesBasedOnConditions(getAllMediaProfileColumns(), selection,
@@ -152,18 +155,19 @@
}
@Override
- public List<PictureProfile> getAvailablePictureProfiles(int userId) {
+ public List<PictureProfile> getAvailablePictureProfiles(
+ boolean includeParams, UserHandle user) {
return new ArrayList<>();
}
@Override
- public boolean setDefaultPictureProfile(String profileId, int userId) {
+ public boolean setDefaultPictureProfile(String profileId, UserHandle user) {
// TODO: pass the profile ID to MediaQuality HAL when ready.
return false;
}
@Override
- public List<String> getPictureProfilePackageNames(int userId) {
+ public List<String> getPictureProfilePackageNames(UserHandle user) {
String [] column = {BaseParameters.PARAMETER_PACKAGE};
List<PictureProfile> pictureProfiles = getPictureProfilesBasedOnConditions(column,
null, null);
@@ -174,17 +178,17 @@
}
@Override
- public List<PictureProfileHandle> getPictureProfileHandle(String[] id, int userId) {
+ public List<PictureProfileHandle> getPictureProfileHandle(String[] id, UserHandle user) {
return new ArrayList<>();
}
@Override
- public List<SoundProfileHandle> getSoundProfileHandle(String[] id, int userId) {
+ public List<SoundProfileHandle> getSoundProfileHandle(String[] id, UserHandle user) {
return new ArrayList<>();
}
@Override
- public SoundProfile createSoundProfile(SoundProfile sp, int userId) {
+ public SoundProfile createSoundProfile(SoundProfile sp, UserHandle user) {
SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
@@ -203,12 +207,12 @@
}
@Override
- public void updateSoundProfile(String id, SoundProfile pp, int userId) {
+ public void updateSoundProfile(String id, SoundProfile pp, UserHandle user) {
// TODO: implement
}
@Override
- public void removeSoundProfile(String id, int userId) {
+ public void removeSoundProfile(String id, UserHandle user) {
Long intId = mSoundProfileTempIdMap.inverse().get(id);
if (intId != null) {
SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase();
@@ -221,9 +225,10 @@
}
@Override
- public SoundProfile getSoundProfile(int type, String id, int userId) {
+ public SoundProfile getSoundProfile(int type, String id, boolean includeParams,
+ UserHandle user) {
String selection = BaseParameters.PARAMETER_TYPE + " = ? AND "
- + BaseParameters.PARAMETER_NAME + " = ?";
+ + BaseParameters.PARAMETER_ID + " = ?";
String[] selectionArguments = {String.valueOf(type), id};
try (
@@ -247,7 +252,8 @@
}
@Override
- public List<SoundProfile> getSoundProfilesByPackage(String packageName, int userId) {
+ public List<SoundProfile> getSoundProfilesByPackage(
+ String packageName, boolean includeParams, UserHandle user) {
String selection = BaseParameters.PARAMETER_PACKAGE + " = ?";
String[] selectionArguments = {packageName};
return getSoundProfilesBasedOnConditions(getAllMediaProfileColumns(), selection,
@@ -255,18 +261,19 @@
}
@Override
- public List<SoundProfile> getAvailableSoundProfiles(int userId) {
+ public List<SoundProfile> getAvailableSoundProfiles(
+ boolean includeParams, UserHandle user) {
return new ArrayList<>();
}
@Override
- public boolean setDefaultSoundProfile(String profileId, int userId) {
+ public boolean setDefaultSoundProfile(String profileId, UserHandle user) {
// TODO: pass the profile ID to MediaQuality HAL when ready.
return false;
}
@Override
- public List<String> getSoundProfilePackageNames(int userId) {
+ public List<String> getSoundProfilePackageNames(UserHandle user) {
String [] column = {BaseParameters.PARAMETER_NAME};
List<SoundProfile> soundProfiles = getSoundProfilesBasedOnConditions(column,
null, null);
@@ -456,70 +463,71 @@
}
@Override
- public void setAmbientBacklightSettings(AmbientBacklightSettings settings, int userId) {
+ public void setAmbientBacklightSettings(
+ AmbientBacklightSettings settings, UserHandle user) {
}
@Override
- public void setAmbientBacklightEnabled(boolean enabled, int userId) {
+ public void setAmbientBacklightEnabled(boolean enabled, UserHandle user) {
}
@Override
- public List<ParamCapability> getParamCapabilities(List<String> names, int userId) {
+ public List<ParamCapability> getParamCapabilities(List<String> names, UserHandle user) {
return new ArrayList<>();
}
@Override
- public List<String> getPictureProfileAllowList(int userId) {
+ public List<String> getPictureProfileAllowList(UserHandle user) {
return new ArrayList<>();
}
@Override
- public void setPictureProfileAllowList(List<String> packages, int userId) {
+ public void setPictureProfileAllowList(List<String> packages, UserHandle user) {
}
@Override
- public List<String> getSoundProfileAllowList(int userId) {
+ public List<String> getSoundProfileAllowList(UserHandle user) {
return new ArrayList<>();
}
@Override
- public void setSoundProfileAllowList(List<String> packages, int userId) {
+ public void setSoundProfileAllowList(List<String> packages, UserHandle user) {
}
@Override
- public boolean isSupported(int userId) {
+ public boolean isSupported(UserHandle user) {
return false;
}
@Override
- public void setAutoPictureQualityEnabled(boolean enabled, int userId) {
+ public void setAutoPictureQualityEnabled(boolean enabled, UserHandle user) {
}
@Override
- public boolean isAutoPictureQualityEnabled(int userId) {
+ public boolean isAutoPictureQualityEnabled(UserHandle user) {
return false;
}
@Override
- public void setSuperResolutionEnabled(boolean enabled, int userId) {
+ public void setSuperResolutionEnabled(boolean enabled, UserHandle user) {
}
@Override
- public boolean isSuperResolutionEnabled(int userId) {
+ public boolean isSuperResolutionEnabled(UserHandle user) {
return false;
}
@Override
- public void setAutoSoundQualityEnabled(boolean enabled, int userId) {
+ public void setAutoSoundQualityEnabled(boolean enabled, UserHandle user) {
}
@Override
- public boolean isAutoSoundQualityEnabled(int userId) {
+ public boolean isAutoSoundQualityEnabled(UserHandle user) {
return false;
}
@Override
- public boolean isAmbientBacklightEnabled(int userId) {
+ public boolean isAmbientBacklightEnabled(UserHandle user) {
return false;
}
}
diff --git a/services/core/java/com/android/server/os/instrumentation/DynamicInstrumentationManagerService.java b/services/core/java/com/android/server/os/instrumentation/DynamicInstrumentationManagerService.java
index 8ec7160..871d12e 100644
--- a/services/core/java/com/android/server/os/instrumentation/DynamicInstrumentationManagerService.java
+++ b/services/core/java/com/android/server/os/instrumentation/DynamicInstrumentationManagerService.java
@@ -20,116 +20,100 @@
import static android.content.Context.DYNAMIC_INSTRUMENTATION_SERVICE;
import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.annotation.PermissionManuallyEnforced;
+import android.annotation.RequiresPermission;
+import android.app.ActivityManagerInternal;
import android.content.Context;
+import android.os.RemoteException;
import android.os.instrumentation.ExecutableMethodFileOffsets;
import android.os.instrumentation.IDynamicInstrumentationManager;
+import android.os.instrumentation.IOffsetCallback;
import android.os.instrumentation.MethodDescriptor;
+import android.os.instrumentation.MethodDescriptorParser;
import android.os.instrumentation.TargetProcess;
-import com.android.internal.annotations.VisibleForTesting;
+
+import com.android.server.LocalServices;
import com.android.server.SystemService;
import dalvik.system.VMDebug;
import java.lang.reflect.Method;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+
/**
* System private implementation of the {@link IDynamicInstrumentationManager interface}.
*/
public class DynamicInstrumentationManagerService extends SystemService {
+
+ private ActivityManagerInternal mAmInternal;
+
public DynamicInstrumentationManagerService(@NonNull Context context) {
super(context);
}
@Override
public void onStart() {
+ mAmInternal = LocalServices.getService(ActivityManagerInternal.class);
publishBinderService(DYNAMIC_INSTRUMENTATION_SERVICE, new BinderService());
}
private final class BinderService extends IDynamicInstrumentationManager.Stub {
@Override
@PermissionManuallyEnforced
- public @Nullable ExecutableMethodFileOffsets getExecutableMethodFileOffsets(
- @NonNull TargetProcess targetProcess, @NonNull MethodDescriptor methodDescriptor) {
+ @RequiresPermission(value = android.Manifest.permission.DYNAMIC_INSTRUMENTATION)
+ public void getExecutableMethodFileOffsets(
+ @NonNull TargetProcess targetProcess, @NonNull MethodDescriptor methodDescriptor,
+ @NonNull IOffsetCallback callback) {
if (!com.android.art.flags.Flags.executableMethodFileOffsets()) {
throw new UnsupportedOperationException();
}
getContext().enforceCallingOrSelfPermission(
DYNAMIC_INSTRUMENTATION, "Caller must have DYNAMIC_INSTRUMENTATION permission");
+ Objects.requireNonNull(targetProcess.processName);
- if (targetProcess.processName == null
- || !targetProcess.processName.equals("system_server")) {
- throw new UnsupportedOperationException(
- "system_server is the only supported target process");
+ if (!targetProcess.processName.equals("system_server")) {
+ try {
+ mAmInternal.getExecutableMethodFileOffsets(targetProcess.processName,
+ targetProcess.pid, targetProcess.uid, methodDescriptor,
+ new IOffsetCallback.Stub() {
+ @Override
+ public void onResult(ExecutableMethodFileOffsets result) {
+ try {
+ callback.onResult(result);
+ } catch (RemoteException e) {
+ /* ignore */
+ }
+ }
+ });
+ return;
+ } catch (NoSuchElementException e) {
+ throw new IllegalArgumentException(
+ "The specified app process cannot be found." , e);
+ }
}
- Method method = parseMethodDescriptor(
+ Method method = MethodDescriptorParser.parseMethodDescriptor(
getClass().getClassLoader(), methodDescriptor);
VMDebug.ExecutableMethodFileOffsets location =
VMDebug.getExecutableMethodFileOffsets(method);
- if (location == null) {
- return null;
- }
-
- ExecutableMethodFileOffsets ret = new ExecutableMethodFileOffsets();
- ret.containerPath = location.getContainerPath();
- ret.containerOffset = location.getContainerOffset();
- ret.methodOffset = location.getMethodOffset();
- return ret;
- }
- }
-
- @VisibleForTesting
- static Method parseMethodDescriptor(ClassLoader classLoader,
- @NonNull MethodDescriptor descriptor) {
- try {
- Class<?> javaClass = classLoader.loadClass(descriptor.fullyQualifiedClassName);
- Class<?>[] parameters = new Class[descriptor.fullyQualifiedParameters.length];
- for (int i = 0; i < descriptor.fullyQualifiedParameters.length; i++) {
- String typeName = descriptor.fullyQualifiedParameters[i];
- boolean isArrayType = typeName.endsWith("[]");
- if (isArrayType) {
- typeName = typeName.substring(0, typeName.length() - 2);
+ try {
+ if (location == null) {
+ callback.onResult(null);
+ return;
}
- switch (typeName) {
- case "boolean":
- parameters[i] = isArrayType ? boolean.class.arrayType() : boolean.class;
- break;
- case "byte":
- parameters[i] = isArrayType ? byte.class.arrayType() : byte.class;
- break;
- case "char":
- parameters[i] = isArrayType ? char.class.arrayType() : char.class;
- break;
- case "short":
- parameters[i] = isArrayType ? short.class.arrayType() : short.class;
- break;
- case "int":
- parameters[i] = isArrayType ? int.class.arrayType() : int.class;
- break;
- case "long":
- parameters[i] = isArrayType ? long.class.arrayType() : long.class;
- break;
- case "float":
- parameters[i] = isArrayType ? float.class.arrayType() : float.class;
- break;
- case "double":
- parameters[i] = isArrayType ? double.class.arrayType() : double.class;
- break;
- default:
- parameters[i] = isArrayType ? classLoader.loadClass(typeName).arrayType()
- : classLoader.loadClass(typeName);
- }
- }
- return javaClass.getDeclaredMethod(descriptor.methodName, parameters);
- } catch (ClassNotFoundException | NoSuchMethodException e) {
- throw new IllegalArgumentException(
- "The specified method cannot be found. Is this descriptor valid? "
- + descriptor, e);
+ ExecutableMethodFileOffsets ret = new ExecutableMethodFileOffsets();
+ ret.containerPath = location.getContainerPath();
+ ret.containerOffset = location.getContainerOffset();
+ ret.methodOffset = location.getMethodOffset();
+ callback.onResult(ret);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Failed to invoke result callback", e);
+ }
}
}
}
diff --git a/services/core/java/com/android/server/pm/SaferIntentUtils.java b/services/core/java/com/android/server/pm/SaferIntentUtils.java
index 854e142..ec91da9 100644
--- a/services/core/java/com/android/server/pm/SaferIntentUtils.java
+++ b/services/core/java/com/android/server/pm/SaferIntentUtils.java
@@ -213,6 +213,7 @@
* CTS tests. The code in this method shall properly avoid control flows using these arguments.
*/
public static void blockNullAction(IntentArgs args, List componentList) {
+ if (args.intent.getAction() != null) return;
if (ActivityManager.canAccessUnexportedComponents(args.callingUid)) return;
final Computer computer = (Computer) args.snapshot;
@@ -235,14 +236,11 @@
}
final ParsedMainComponent comp = infoToComponent(
resolveInfo.getComponentInfo(), resolver, args.isReceiver);
- if (comp != null && !comp.getIntents().isEmpty()
- && args.intent.getAction() == null) {
+ if (comp != null && !comp.getIntents().isEmpty()) {
match = false;
}
} else if (c instanceof IntentFilter) {
- if (args.intent.getAction() == null) {
- match = false;
- }
+ match = false;
}
if (!match) {
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 90d3834..2781592 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -27,6 +27,7 @@
import static android.app.ActivityManager.START_RETURN_LOCK_TASK_MODE_VIOLATION;
import static android.app.ActivityManager.START_SUCCESS;
import static android.app.ActivityManager.START_TASK_TO_FRONT;
+import static android.app.ActivityManager.isStartResultSuccessful;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
import static android.app.PendingIntent.FLAG_ONE_SHOT;
@@ -891,7 +892,10 @@
final ActivityOptions originalOptions = mRequest.activityOptions != null
? mRequest.activityOptions.getOriginalOptions() : null;
// Only track the launch time of activity that will be resumed.
- launchingRecord = mDoResume ? mLastStartActivityRecord : null;
+ if (mDoResume || (isStartResultSuccessful(res)
+ && mLastStartActivityRecord.getTask().isVisibleRequested())) {
+ launchingRecord = mLastStartActivityRecord;
+ }
// If the new record is the one that started, a new activity has created.
final boolean newActivityCreated = mStartActivity == launchingRecord;
// Notify ActivityMetricsLogger that the activity has launched.
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 4ed1206..810aa04 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -4489,7 +4489,7 @@
}
void onPictureInPictureParamsChanged() {
- if (inPinnedWindowingMode()) {
+ if (inPinnedWindowingMode() || Flags.enableDesktopWindowingPip()) {
dispatchTaskInfoChangedIfNeeded(true /* force */);
}
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index b42ce64f..bf4cb45 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -9020,16 +9020,19 @@
clearPointerDownOutsideFocusRunnable();
+ final InputTarget focusedInputTarget = mFocusedInputTarget;
if (shouldDelayTouchOutside(t)) {
- mPointerDownOutsideFocusRunnable = () -> handlePointerDownOutsideFocus(t);
+ mPointerDownOutsideFocusRunnable =
+ () -> handlePointerDownOutsideFocus(t, focusedInputTarget);
mH.postDelayed(mPointerDownOutsideFocusRunnable, POINTER_DOWN_OUTSIDE_FOCUS_TIMEOUT_MS);
} else if (!fromHandler) {
// Still post the runnable to handler thread in case there is already a runnable
// in execution, but still waiting to hold the wm lock.
- mPointerDownOutsideFocusRunnable = () -> handlePointerDownOutsideFocus(t);
+ mPointerDownOutsideFocusRunnable =
+ () -> handlePointerDownOutsideFocus(t, focusedInputTarget);
mH.post(mPointerDownOutsideFocusRunnable);
} else {
- handlePointerDownOutsideFocus(t);
+ handlePointerDownOutsideFocus(t, focusedInputTarget);
}
}
@@ -9061,8 +9064,15 @@
return shouldDelayTouchForEmbeddedActivity || shouldDelayTouchForFreeform;
}
- private void handlePointerDownOutsideFocus(InputTarget t) {
+ private void handlePointerDownOutsideFocus(InputTarget t, InputTarget focusedInputTarget) {
synchronized (mGlobalLock) {
+ if (mFocusedInputTarget != focusedInputTarget) {
+ // Skip if the mFocusedInputTarget is already changed. This is possible if the
+ // pointer-down-outside-focus event is delayed to be handled.
+ ProtoLog.i(WM_DEBUG_FOCUS_LIGHT,
+ "Skip onPointerDownOutsideFocusLocked due to input target changed %s", t);
+ return;
+ }
if (mPointerDownOutsideFocusRunnable != null
&& mH.hasCallbacks(mPointerDownOutsideFocusRunnable)) {
// Skip if there's another pending pointer-down-outside-focus event.
diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/os/instrumentation/ParseMethodDescriptorTest.java b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/os/instrumentation/ParseMethodDescriptorTest.java
index 5492ba6..6e14bad 100644
--- a/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/os/instrumentation/ParseMethodDescriptorTest.java
+++ b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/os/instrumentation/ParseMethodDescriptorTest.java
@@ -20,6 +20,7 @@
import static org.junit.Assert.assertThrows;
import android.os.instrumentation.MethodDescriptor;
+import android.os.instrumentation.MethodDescriptorParser;
import android.platform.test.annotations.Presubmit;
import androidx.test.filters.SmallTest;
@@ -37,7 +38,7 @@
/**
* Test class for
- * {@link DynamicInstrumentationManagerService#parseMethodDescriptor(ClassLoader,
+ * {@link MethodDescriptorParser#parseMethodDescriptor(ClassLoader,
* MethodDescriptor)}.
* <p>
* Build/Install/Run:
@@ -119,13 +120,13 @@
}
private Method parseMethodDescriptor(String fqcn, String methodName) {
- return DynamicInstrumentationManagerService.parseMethodDescriptor(
+ return MethodDescriptorParser.parseMethodDescriptor(
getClass().getClassLoader(),
getMethodDescriptor(fqcn, methodName, new String[]{}));
}
private Method parseMethodDescriptor(String fqcn, String methodName, String[] fqParameters) {
- return DynamicInstrumentationManagerService.parseMethodDescriptor(
+ return MethodDescriptorParser.parseMethodDescriptor(
getClass().getClassLoader(),
getMethodDescriptor(fqcn, methodName, fqParameters));
}
diff --git a/telephony/java/android/telephony/Annotation.java b/telephony/java/android/telephony/Annotation.java
index 8fe107c..09b18b6 100644
--- a/telephony/java/android/telephony/Annotation.java
+++ b/telephony/java/android/telephony/Annotation.java
@@ -109,6 +109,7 @@
//TelephonyManager.NETWORK_TYPE_LTE_CA,
TelephonyManager.NETWORK_TYPE_NR,
+ TelephonyManager.NETWORK_TYPE_NB_IOT_NTN,
})
@Retention(RetentionPolicy.SOURCE)
public @interface NetworkType {
diff --git a/telephony/java/android/telephony/RadioAccessFamily.java b/telephony/java/android/telephony/RadioAccessFamily.java
index 90d6f89..8b52f07 100644
--- a/telephony/java/android/telephony/RadioAccessFamily.java
+++ b/telephony/java/android/telephony/RadioAccessFamily.java
@@ -66,6 +66,9 @@
// 5G
public static final int RAF_NR = (int) TelephonyManager.NETWORK_TYPE_BITMASK_NR;
+ /** NB-IOT (Narrowband Internet of Things) over Non-Terrestrial-Networks technology. */
+ public static final int RAF_NB_IOT_NTN = (int) TelephonyManager.NETWORK_TYPE_BITMASK_NB_IOT_NTN;
+
// Grouping of RAFs
// 2G
private static final int GSM = RAF_GSM | RAF_GPRS | RAF_EDGE;
@@ -80,6 +83,9 @@
// 5G
private static final int NR = RAF_NR;
+ /** Non-Terrestrial Network. */
+ private static final int NB_IOT_NTN = RAF_NB_IOT_NTN;
+
/* Phone ID of phone */
private int mPhoneId;
@@ -258,7 +264,7 @@
raf = ((EVDO & raf) > 0) ? (EVDO | raf) : raf;
raf = ((LTE & raf) > 0) ? (LTE | raf) : raf;
raf = ((NR & raf) > 0) ? (NR | raf) : raf;
-
+ raf = ((NB_IOT_NTN & raf) > 0) ? (NB_IOT_NTN | raf) : raf;
return raf;
}
@@ -364,6 +370,7 @@
case "WCDMA": return WCDMA;
case "LTE_CA": return RAF_LTE_CA;
case "NR": return RAF_NR;
+ case "NB_IOT_NTN": return RAF_NB_IOT_NTN;
default: return RAF_UNKNOWN;
}
}
diff --git a/telephony/java/android/telephony/ServiceState.java b/telephony/java/android/telephony/ServiceState.java
index 127bbff..f8c3287 100644
--- a/telephony/java/android/telephony/ServiceState.java
+++ b/telephony/java/android/telephony/ServiceState.java
@@ -233,6 +233,12 @@
public static final int RIL_RADIO_TECHNOLOGY_NR = 20;
/**
+ * 3GPP NB-IOT (Narrowband Internet of Things) over Non-Terrestrial-Networks technology.
+ * @hide
+ */
+ public static final int RIL_RADIO_TECHNOLOGY_NB_IOT_NTN = 21;
+
+ /**
* RIL Radio Annotation
* @hide
*/
@@ -258,14 +264,16 @@
ServiceState.RIL_RADIO_TECHNOLOGY_TD_SCDMA,
ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN,
ServiceState.RIL_RADIO_TECHNOLOGY_LTE_CA,
- ServiceState.RIL_RADIO_TECHNOLOGY_NR})
+ ServiceState.RIL_RADIO_TECHNOLOGY_NR,
+ ServiceState.RIL_RADIO_TECHNOLOGY_NB_IOT_NTN
+ })
public @interface RilRadioTechnology {}
/**
* The number of the radio technologies.
*/
- private static final int NEXT_RIL_RADIO_TECHNOLOGY = 21;
+ private static final int NEXT_RIL_RADIO_TECHNOLOGY = 22;
/** @hide */
public static final int RIL_RADIO_CDMA_TECHNOLOGY_BITMASK =
@@ -1125,6 +1133,9 @@
case RIL_RADIO_TECHNOLOGY_NR:
rtString = "NR_SA";
break;
+ case RIL_RADIO_TECHNOLOGY_NB_IOT_NTN:
+ rtString = "NB_IOT_NTN";
+ break;
default:
rtString = "Unexpected";
Rlog.w(LOG_TAG, "Unexpected radioTechnology=" + rt);
@@ -1668,6 +1679,8 @@
return TelephonyManager.NETWORK_TYPE_LTE_CA;
case RIL_RADIO_TECHNOLOGY_NR:
return TelephonyManager.NETWORK_TYPE_NR;
+ case RIL_RADIO_TECHNOLOGY_NB_IOT_NTN:
+ return TelephonyManager.NETWORK_TYPE_NB_IOT_NTN;
default:
return TelephonyManager.NETWORK_TYPE_UNKNOWN;
}
@@ -1697,6 +1710,7 @@
return AccessNetworkType.CDMA2000;
case RIL_RADIO_TECHNOLOGY_LTE:
case RIL_RADIO_TECHNOLOGY_LTE_CA:
+ case RIL_RADIO_TECHNOLOGY_NB_IOT_NTN:
return AccessNetworkType.EUTRAN;
case RIL_RADIO_TECHNOLOGY_NR:
return AccessNetworkType.NGRAN;
@@ -1757,6 +1771,8 @@
return RIL_RADIO_TECHNOLOGY_LTE_CA;
case TelephonyManager.NETWORK_TYPE_NR:
return RIL_RADIO_TECHNOLOGY_NR;
+ case TelephonyManager.NETWORK_TYPE_NB_IOT_NTN:
+ return RIL_RADIO_TECHNOLOGY_NB_IOT_NTN;
default:
return RIL_RADIO_TECHNOLOGY_UNKNOWN;
}
@@ -1866,7 +1882,8 @@
|| radioTechnology == RIL_RADIO_TECHNOLOGY_TD_SCDMA
|| radioTechnology == RIL_RADIO_TECHNOLOGY_IWLAN
|| radioTechnology == RIL_RADIO_TECHNOLOGY_LTE_CA
- || radioTechnology == RIL_RADIO_TECHNOLOGY_NR;
+ || radioTechnology == RIL_RADIO_TECHNOLOGY_NR
+ || radioTechnology == RIL_RADIO_TECHNOLOGY_NB_IOT_NTN;
}
@@ -1886,7 +1903,8 @@
public static boolean isPsOnlyTech(int radioTechnology) {
return radioTechnology == RIL_RADIO_TECHNOLOGY_LTE
|| radioTechnology == RIL_RADIO_TECHNOLOGY_LTE_CA
- || radioTechnology == RIL_RADIO_TECHNOLOGY_NR;
+ || radioTechnology == RIL_RADIO_TECHNOLOGY_NR
+ || radioTechnology == RIL_RADIO_TECHNOLOGY_NB_IOT_NTN;
}
/** @hide */
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 65a52da..aec11c4 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -3114,6 +3114,12 @@
* For 5G NSA, the network type will be {@link #NETWORK_TYPE_LTE}.
*/
public static final int NETWORK_TYPE_NR = TelephonyProtoEnums.NETWORK_TYPE_NR; // 20.
+ /**
+ * 3GPP NB-IOT (Narrowband Internet of Things) over Non-Terrestrial-Networks technology.
+ */
+ @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
+ public static final int NETWORK_TYPE_NB_IOT_NTN =
+ TelephonyProtoEnums.NETWORK_TYPE_NB_IOT_NTN; // 21
private static final @NetworkType int[] NETWORK_TYPES = {
NETWORK_TYPE_GPRS,
@@ -3190,6 +3196,7 @@
* @see #NETWORK_TYPE_EHRPD
* @see #NETWORK_TYPE_HSPAP
* @see #NETWORK_TYPE_NR
+ * @see #NETWORK_TYPE_NB_IOT_NTN
*
* @hide
*/
@@ -3250,6 +3257,7 @@
* @see #NETWORK_TYPE_EHRPD
* @see #NETWORK_TYPE_HSPAP
* @see #NETWORK_TYPE_NR
+ * @see #NETWORK_TYPE_NB_IOT_NTN
*
* @throws UnsupportedOperationException If the device does not have
* {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}.
@@ -3400,6 +3408,8 @@
return "LTE_CA";
case NETWORK_TYPE_NR:
return "NR";
+ case NETWORK_TYPE_NB_IOT_NTN:
+ return "NB_IOT_NTN";
case NETWORK_TYPE_UNKNOWN:
return "UNKNOWN";
default:
@@ -3450,6 +3460,8 @@
return NETWORK_TYPE_BITMASK_LTE;
case NETWORK_TYPE_NR:
return NETWORK_TYPE_BITMASK_NR;
+ case NETWORK_TYPE_NB_IOT_NTN:
+ return NETWORK_TYPE_BITMASK_NB_IOT_NTN;
case NETWORK_TYPE_IWLAN:
return NETWORK_TYPE_BITMASK_IWLAN;
case NETWORK_TYPE_IDEN:
@@ -10160,6 +10172,9 @@
* This API will result in allowing an intersection of allowed network types for all reasons,
* including the configuration done through other reasons.
*
+ * If device supports satellite service, then
+ * {@link #NETWORK_TYPE_NB_IOT_NTN} is added to allowed network types for reason by default.
+ *
* @param reason the reason the allowed network type change is taking place
* @param allowedNetworkTypes The bitmask of allowed network type
* @throws IllegalStateException if the Telephony process is not currently available.
@@ -10209,6 +10224,10 @@
* <p>Requires permission: android.Manifest.READ_PRIVILEGED_PHONE_STATE or
* that the calling app has carrier privileges (see {@link #hasCarrierPrivileges}).
*
+ * If device supports satellite service, then
+ * {@link #NETWORK_TYPE_NB_IOT_NTN} is added to allowed network types for reason by
+ * default.
+ *
* @param reason the reason the allowed network type change is taking place
* @return the allowed network type bitmask
* @throws IllegalStateException if the Telephony process is not currently available.
@@ -10275,7 +10294,7 @@
*/
public static String convertNetworkTypeBitmaskToString(
@NetworkTypeBitMask long networkTypeBitmask) {
- String networkTypeName = IntStream.rangeClosed(NETWORK_TYPE_GPRS, NETWORK_TYPE_NR)
+ String networkTypeName = IntStream.rangeClosed(NETWORK_TYPE_GPRS, NETWORK_TYPE_NB_IOT_NTN)
.filter(x -> {
return (networkTypeBitmask & getBitMaskForNetworkType(x))
== getBitMaskForNetworkType(x);
@@ -14905,7 +14924,8 @@
NETWORK_TYPE_BITMASK_LTE_CA,
NETWORK_TYPE_BITMASK_NR,
NETWORK_TYPE_BITMASK_IWLAN,
- NETWORK_TYPE_BITMASK_IDEN
+ NETWORK_TYPE_BITMASK_IDEN,
+ NETWORK_TYPE_BITMASK_NB_IOT_NTN
})
public @interface NetworkTypeBitMask {}
@@ -15006,6 +15026,12 @@
*/
public static final long NETWORK_TYPE_BITMASK_IWLAN = (1 << (NETWORK_TYPE_IWLAN -1));
+ /**
+ * network type bitmask indicating the support of readio tech NB IOT NTN.
+ */
+ @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
+ public static final long NETWORK_TYPE_BITMASK_NB_IOT_NTN = (1 << (NETWORK_TYPE_NB_IOT_NTN - 1));
+
/** @hide */
public static final long NETWORK_CLASS_BITMASK_2G = NETWORK_TYPE_BITMASK_GSM
| NETWORK_TYPE_BITMASK_GPRS
@@ -15034,6 +15060,9 @@
public static final long NETWORK_CLASS_BITMASK_5G = NETWORK_TYPE_BITMASK_NR;
/** @hide */
+ public static final long NETWORK_CLASS_BITMASK_NTN = NETWORK_TYPE_BITMASK_NB_IOT_NTN;
+
+ /** @hide */
public static final long NETWORK_STANDARDS_FAMILY_BITMASK_3GPP = NETWORK_TYPE_BITMASK_GSM
| NETWORK_TYPE_BITMASK_GPRS
| NETWORK_TYPE_BITMASK_EDGE
@@ -15045,7 +15074,8 @@
| NETWORK_TYPE_BITMASK_TD_SCDMA
| NETWORK_TYPE_BITMASK_LTE
| NETWORK_TYPE_BITMASK_LTE_CA
- | NETWORK_TYPE_BITMASK_NR;
+ | NETWORK_TYPE_BITMASK_NR
+ | NETWORK_TYPE_BITMASK_NB_IOT_NTN;
/** @hide */
public static final long NETWORK_STANDARDS_FAMILY_BITMASK_3GPP2 = NETWORK_TYPE_BITMASK_CDMA
@@ -18083,7 +18113,7 @@
*/
public static boolean isNetworkTypeValid(@NetworkType int networkType) {
return networkType >= TelephonyManager.NETWORK_TYPE_UNKNOWN &&
- networkType <= TelephonyManager.NETWORK_TYPE_NR;
+ networkType <= TelephonyManager.NETWORK_TYPE_NB_IOT_NTN;
}
/**