diff options
87 files changed, 1756 insertions, 495 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index fbe4905d9754..d582cb79fba1 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -24,6 +24,7 @@ aconfig_declarations_group { "android-sdk-flags-java", "android.adaptiveauth.flags-aconfig-java", "android.app.appfunctions.flags-aconfig-java", + "android.app.assist.flags-aconfig-java", "android.app.contextualsearch.flags-aconfig-java", "android.app.flags-aconfig-java", "android.app.jank.flags-aconfig-java", @@ -1230,6 +1231,20 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +// Assist +aconfig_declarations { + name: "android.app.assist.flags-aconfig", + package: "android.app.assist.flags", + container: "system", + srcs: ["core/java/android/app/assist/flags.aconfig"], +} + +java_aconfig_library { + name: "android.app.assist.flags-aconfig-java", + aconfig_declarations: "android.app.assist.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + // Smartspace aconfig_declarations { name: "android.app.smartspace.flags-aconfig", diff --git a/core/api/current.txt b/core/api/current.txt index 65e4f270121b..a131ea7d29a7 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -4950,7 +4950,7 @@ package android.app { field @Deprecated @FlaggedApi("com.android.window.flags.bal_additional_start_modes") public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOWED = 1; // 0x1 field @FlaggedApi("com.android.window.flags.bal_additional_start_modes") public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS = 3; // 0x3 field @FlaggedApi("com.android.window.flags.bal_additional_start_modes") public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE = 4; // 0x4 - field @FlaggedApi("com.android.window.flags.bal_additional_start_modes") public static final int MODE_BACKGROUND_ACTIVITY_START_DENIED = 2; // 0x2 + field public static final int MODE_BACKGROUND_ACTIVITY_START_DENIED = 2; // 0x2 field public static final int MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED = 0; // 0x0 } @@ -8792,7 +8792,8 @@ package android.app.appfunctions { ctor public AppFunctionService(); method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent); method @Deprecated @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); - method @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); + method @Deprecated @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); + method @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService"; } @@ -54921,6 +54922,7 @@ package android.view.accessibility { method public void setPackageName(CharSequence); method public void setSpeechStateChangeTypes(int); method public void writeToParcel(android.os.Parcel, int); + field @FlaggedApi("android.view.accessibility.tri_state_checked") public static final int CONTENT_CHANGE_TYPE_CHECKED = 8192; // 0x2000 field public static final int CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 4; // 0x4 field public static final int CONTENT_CHANGE_TYPE_CONTENT_INVALID = 1024; // 0x400 field public static final int CONTENT_CHANGE_TYPE_DRAG_CANCELLED = 512; // 0x200 @@ -55066,6 +55068,7 @@ package android.view.accessibility { method @Deprecated public void getBoundsInParent(android.graphics.Rect); method public void getBoundsInScreen(android.graphics.Rect); method public void getBoundsInWindow(@NonNull android.graphics.Rect); + method @FlaggedApi("android.view.accessibility.tri_state_checked") public int getChecked(); method public android.view.accessibility.AccessibilityNodeInfo getChild(int); method @Nullable public android.view.accessibility.AccessibilityNodeInfo getChild(int, int); method public int getChildCount(); @@ -55108,7 +55111,7 @@ package android.view.accessibility { method public boolean isAccessibilityDataSensitive(); method public boolean isAccessibilityFocused(); method public boolean isCheckable(); - method public boolean isChecked(); + method @Deprecated @FlaggedApi("android.view.accessibility.tri_state_checked") public boolean isChecked(); method public boolean isClickable(); method public boolean isContentInvalid(); method public boolean isContextClickable(); @@ -55153,7 +55156,8 @@ package android.view.accessibility { method public void setBoundsInWindow(@NonNull android.graphics.Rect); method public void setCanOpenPopup(boolean); method public void setCheckable(boolean); - method public void setChecked(boolean); + method @Deprecated @FlaggedApi("android.view.accessibility.tri_state_checked") public void setChecked(boolean); + method @FlaggedApi("android.view.accessibility.tri_state_checked") public void setChecked(int); method public void setClassName(CharSequence); method public void setClickable(boolean); method public void setCollectionInfo(android.view.accessibility.AccessibilityNodeInfo.CollectionInfo); @@ -55249,6 +55253,9 @@ package android.view.accessibility { field public static final int ACTION_SELECT = 4; // 0x4 field public static final int ACTION_SET_SELECTION = 131072; // 0x20000 field public static final int ACTION_SET_TEXT = 2097152; // 0x200000 + field @FlaggedApi("android.view.accessibility.tri_state_checked") public static final int CHECKED_STATE_FALSE = 0; // 0x0 + field @FlaggedApi("android.view.accessibility.tri_state_checked") public static final int CHECKED_STATE_PARTIAL = 2; // 0x2 + field @FlaggedApi("android.view.accessibility.tri_state_checked") public static final int CHECKED_STATE_TRUE = 1; // 0x1 field @NonNull public static final android.os.Parcelable.Creator<android.view.accessibility.AccessibilityNodeInfo> CREATOR; field public static final String EXTRA_DATA_RENDERING_INFO_KEY = "android.view.accessibility.extra.DATA_RENDERING_INFO_KEY"; field public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH = "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH"; diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index 6ab39b028032..832c88a795e5 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -120,7 +120,7 @@ public class ActivityOptions extends ComponentOptions { /** * Grants the {@link PendingIntent} background activity start privileges. * - * This behaves the same as {@link #MODE_BACKGROUND_ACTIVITY_START_ALLOWED_ALWAYS}, except it + * This behaves the same as {@link #MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS}, except it * does not grant background activity launch permissions based on the privileged permission * <code>START_ACTIVITIES_FROM_BACKGROUND</code>. * @@ -136,7 +136,6 @@ public class ActivityOptions extends ComponentOptions { /** * Denies the {@link PendingIntent} any background activity start privileges. */ - @FlaggedApi(Flags.FLAG_BAL_ADDITIONAL_START_MODES) public static final int MODE_BACKGROUND_ACTIVITY_START_DENIED = 2; /** * Grants the {@link PendingIntent} all background activity start privileges, including @@ -146,12 +145,12 @@ public class ActivityOptions extends ComponentOptions { * <p><b>Caution:</b> This mode should be used sparingly. Most apps should use * {@link #MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE} instead, relying on notifications * or foreground services for background interactions to minimize user disruption. However, - * this mode is necessary for specific use cases, such as companion apps responding to + * this mode is necessary for specific use cases, such as companion apps responding to * prompts from a connected device. * * <p>For more information on background activity start restrictions, see: * <a href="https://developer.android.com/guide/components/activities/background-starts"> - * Restrictions on starting activities from the background</a> + * Restrictions on starting activities from the background</a> */ @FlaggedApi(Flags.FLAG_BAL_ADDITIONAL_START_MODES) public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS = 3; diff --git a/core/java/android/app/appfunctions/AppFunctionService.java b/core/java/android/app/appfunctions/AppFunctionService.java index 7a68a656564b..ceca850a1037 100644 --- a/core/java/android/app/appfunctions/AppFunctionService.java +++ b/core/java/android/app/appfunctions/AppFunctionService.java @@ -29,11 +29,9 @@ import android.app.Service; import android.content.Context; import android.content.Intent; import android.os.Binder; -import android.os.Bundle; +import android.os.CancellationSignal; import android.os.IBinder; import android.os.ICancellationSignal; -import android.os.CancellationSignal; -import android.os.RemoteCallback; import android.os.RemoteException; import android.util.Log; @@ -80,6 +78,7 @@ public abstract class AppFunctionService extends Service { */ void perform( @NonNull ExecuteAppFunctionRequest request, + @NonNull String callingPackage, @NonNull CancellationSignal cancellationSignal, @NonNull Consumer<ExecuteAppFunctionResponse> callback); } @@ -92,6 +91,7 @@ public abstract class AppFunctionService extends Service { @Override public void executeAppFunction( @NonNull ExecuteAppFunctionRequest request, + @NonNull String callingPackage, @NonNull ICancellationCallback cancellationCallback, @NonNull IExecuteAppFunctionCallback callback) { if (context.checkCallingPermission(BIND_APP_FUNCTION_SERVICE) @@ -103,6 +103,7 @@ public abstract class AppFunctionService extends Service { try { onExecuteFunction.perform( request, + callingPackage, buildCancellationSignal(cancellationCallback), safeCallback::onResult); } catch (Exception ex) { @@ -128,12 +129,11 @@ public abstract class AppFunctionService extends Service { throw e.rethrowFromSystemServer(); } - return cancellationSignal ; + return cancellationSignal; } - private final Binder mBinder = createBinder( - AppFunctionService.this, - AppFunctionService.this::onExecuteFunction); + private final Binder mBinder = + createBinder(AppFunctionService.this, AppFunctionService.this::onExecuteFunction); @NonNull @Override @@ -141,7 +141,6 @@ public abstract class AppFunctionService extends Service { return mBinder; } - /** * Called by the system to execute a specific app function. * @@ -161,7 +160,6 @@ public abstract class AppFunctionService extends Service { * * @param request The function execution request. * @param callback A callback to report back the result. - * * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, CancellationSignal, * Consumer)} instead. This method will be removed once usage references are updated. */ @@ -198,12 +196,50 @@ public abstract class AppFunctionService extends Service { * @param request The function execution request. * @param cancellationSignal A signal to cancel the execution. * @param callback A callback to report back the result. + * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, String, + * CancellationSignal, Consumer)} instead. This method will be removed once usage references + * are updated. */ @MainThread + @Deprecated public void onExecuteFunction( @NonNull ExecuteAppFunctionRequest request, @NonNull CancellationSignal cancellationSignal, @NonNull Consumer<ExecuteAppFunctionResponse> callback) { onExecuteFunction(request, callback); } + + /** + * Called by the system to execute a specific app function. + * + * <p>This method is triggered when the system requests your AppFunctionService to handle a + * particular function you have registered and made available. + * + * <p>To ensure proper routing of function requests, assign a unique identifier to each + * function. This identifier doesn't need to be globally unique, but it must be unique within + * your app. For example, a function to order food could be identified as "orderFood". In most + * cases this identifier should come from the ID automatically generated by the AppFunctions + * SDK. You can determine the specific function to invoke by calling {@link + * ExecuteAppFunctionRequest#getFunctionIdentifier()}. + * + * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker + * thread and dispatch the result with the given callback. You should always report back the + * result using the callback, no matter if the execution was successful or not. + * + * <p>This method also accepts a {@link CancellationSignal} that the app should listen to cancel + * the execution of function if requested by the system. + * + * @param request The function execution request. + * @param callingPackage The package name of the app that is requesting the execution. + * @param cancellationSignal A signal to cancel the execution. + * @param callback A callback to report back the result. + */ + @MainThread + public void onExecuteFunction( + @NonNull ExecuteAppFunctionRequest request, + @NonNull String callingPackage, + @NonNull CancellationSignal cancellationSignal, + @NonNull Consumer<ExecuteAppFunctionResponse> callback) { + onExecuteFunction(request, cancellationSignal, callback); + } } diff --git a/core/java/android/app/appfunctions/IAppFunctionService.aidl b/core/java/android/app/appfunctions/IAppFunctionService.aidl index 291f33ccb1b8..bf935d2a102b 100644 --- a/core/java/android/app/appfunctions/IAppFunctionService.aidl +++ b/core/java/android/app/appfunctions/IAppFunctionService.aidl @@ -34,11 +34,13 @@ oneway interface IAppFunctionService { * Called by the system to execute a specific app function. * * @param request the function execution request. + * @param callingPackage The package name of the app that is requesting the execution. * @param cancellationCallback a callback to send back the cancellation transport. * @param callback a callback to report back the result. */ void executeAppFunction( in ExecuteAppFunctionRequest request, + in String callingPackage, in ICancellationCallback cancellationCallback, in IExecuteAppFunctionCallback callback ); diff --git a/core/java/android/app/assist/AssistStructure.java b/core/java/android/app/assist/AssistStructure.java index 508077ed43cc..1af2437a5d6a 100644 --- a/core/java/android/app/assist/AssistStructure.java +++ b/core/java/android/app/assist/AssistStructure.java @@ -1,5 +1,6 @@ package android.app.assist; +import static android.app.assist.flags.Flags.addPlaceholderViewForNullChild; import static android.credentials.Constants.FAILURE_CREDMAN_SELECTOR; import static android.credentials.Constants.SUCCESS_CREDMAN_SELECTOR; import static android.service.autofill.Flags.FLAG_AUTOFILL_CREDMAN_DEV_INTEGRATION; @@ -284,12 +285,18 @@ public class AssistStructure implements Parcelable { mCurViewStackEntry = entry; } - void writeView(ViewNode child, Parcel out, PooledStringWriter pwriter, int levelAdj) { + void writeView(@Nullable ViewNode child, Parcel out, PooledStringWriter pwriter, + int levelAdj) { if (DEBUG_PARCEL) Log.d(TAG, "write view: at " + out.dataPosition() + ", windows=" + mNumWrittenWindows + ", views=" + mNumWrittenViews + ", level=" + (mCurViewStackPos+levelAdj)); out.writeInt(VALIDATE_VIEW_TOKEN); + if (addPlaceholderViewForNullChild() && child == null) { + if (DEBUG_PARCEL_TREE) Log.d(TAG, "Detected an empty child" + + "; writing a placeholder for the child."); + child = new ViewNode(); + } int flags = child.writeSelfToParcel(out, pwriter, mSanitizeOnWrite, mTmpMatrix, /*willWriteChildren=*/true); mNumWrittenViews++; @@ -2545,7 +2552,7 @@ public class AssistStructure implements Parcelable { ensureData(); } Log.i(TAG, "Task id: " + mTaskId); - Log.i(TAG, "Activity: " + (mActivityComponent != null + Log.i(TAG, "Activity: " + (mActivityComponent != null ? mActivityComponent.flattenToShortString() : null)); Log.i(TAG, "Sanitize on write: " + mSanitizeOnWrite); diff --git a/core/java/android/app/assist/flags.aconfig b/core/java/android/app/assist/flags.aconfig new file mode 100644 index 000000000000..bf0aeacbc7f3 --- /dev/null +++ b/core/java/android/app/assist/flags.aconfig @@ -0,0 +1,13 @@ +package: "android.app.assist.flags" +container: "system" + +flag { + name: "add_placeholder_view_for_null_child" + namespace: "machine_learning" + description: "Flag to add a placeholder view when a child view is null." + bug: "369503426" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/os/OWNERS b/core/java/android/os/OWNERS index 7d3076d6611f..a1b75034442e 100644 --- a/core/java/android/os/OWNERS +++ b/core/java/android/os/OWNERS @@ -115,3 +115,10 @@ per-file OomKillRecord.java = file:/MEMORY_OWNERS # MessageQueue per-file MessageQueue.java = mfasheh@google.com, shayba@google.com per-file Message.java = mfasheh@google.com, shayba@google.com + +# Stats +per-file IStatsBootstrapAtomService.aidl = file:/services/core/java/com/android/server/stats/OWNERS +per-file StatsBootstrapAtom.aidl = file:/services/core/java/com/android/server/stats/OWNERS +per-file StatsBootstrapAtomValue.aidl = file:/services/core/java/com/android/server/stats/OWNERS +per-file StatsBootstrapAtomService.java = file:/services/core/java/com/android/server/stats/OWNERS +per-file StatsServiceManager.java = file:/services/core/java/com/android/server/stats/OWNERS diff --git a/core/java/android/os/StatsBootstrapAtomValue.aidl b/core/java/android/os/StatsBootstrapAtomValue.aidl index a90dfa404ee9..b59bc062648f 100644 --- a/core/java/android/os/StatsBootstrapAtomValue.aidl +++ b/core/java/android/os/StatsBootstrapAtomValue.aidl @@ -26,4 +26,5 @@ union StatsBootstrapAtomValue { float floatValue; String stringValue; byte[] bytesValue; + String[] stringArrayValue; }
\ No newline at end of file diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java index a43906f30ff7..dfac244fcc0d 100644 --- a/core/java/android/view/accessibility/AccessibilityEvent.java +++ b/core/java/android/view/accessibility/AccessibilityEvent.java @@ -16,6 +16,7 @@ package android.view.accessibility; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.compat.annotation.UnsupportedAppUsage; @@ -784,6 +785,19 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par */ public static final int CONTENT_CHANGE_TYPE_ENABLED = 1 << 12; + /** + * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event: + * The source node changed its checked state, which is returned by + * {@link AccessibilityNodeInfo#getChecked()}. + * The view changing its checked state should call + * {@link AccessibilityNodeInfo#setChecked(int)} and then send this event. + * + * @see AccessibilityNodeInfo#getChecked() + * @see AccessibilityNodeInfo#setChecked(int) + */ + @FlaggedApi(Flags.FLAG_TRI_STATE_CHECKED) + public static final int CONTENT_CHANGE_TYPE_CHECKED = 1 << 13; + // Speech state change types. /** Change type for {@link #TYPE_SPEECH_STATE_CHANGE} event: another service is speaking. */ diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index fe6aafbd7e16..c5ca059d1cea 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -860,6 +860,37 @@ public class AccessibilityNodeInfo implements Parcelable { public static final String EXTRA_DATA_REQUESTED_KEY = "android.view.accessibility.AccessibilityNodeInfo.extra_data_requested"; + // Tri-state checked states. + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "CHECKED_STATE" }, value = { + CHECKED_STATE_FALSE, + CHECKED_STATE_TRUE, + CHECKED_STATE_PARTIAL + }) + public @interface CheckedState {} + + /** + * This node is not checked. + */ + @FlaggedApi(Flags.FLAG_TRI_STATE_CHECKED) + public static final int CHECKED_STATE_FALSE = 0; + + /** + * This node is checked. + */ + @FlaggedApi(Flags.FLAG_TRI_STATE_CHECKED) + public static final int CHECKED_STATE_TRUE = 1; + + /** + * This node is partially checked. For example, + * when a checkbox owns a number of sub-options and they have + * different states, then the main checkbox is in a partially-checked state. + */ + @FlaggedApi(Flags.FLAG_TRI_STATE_CHECKED) + public static final int CHECKED_STATE_PARTIAL = 2; + // Boolean attributes. private static final int BOOLEAN_PROPERTY_CHECKABLE = 1 /* << 0 */; @@ -1038,6 +1069,10 @@ public class AccessibilityNodeInfo implements Parcelable { private IBinder mLeashedParent; private long mLeashedParentNodeId = UNDEFINED_NODE_ID; + // TODO(b/369951517) Initialize mChecked explicitly with + // the CHECKED_FALSE state when flagging is removed. + private int mChecked; + /** * Creates a new {@link AccessibilityNodeInfo}. */ @@ -2319,28 +2354,100 @@ public class AccessibilityNodeInfo implements Parcelable { } /** - * Gets whether this node is checked. + * Gets whether this node is checked. This is only meaningful + * when {@link #isCheckable()} returns {@code true}. + * + * @deprecated Use {@link #getChecked()} instead. * * @return True if the node is checked. */ + @FlaggedApi(Flags.FLAG_TRI_STATE_CHECKED) + @Deprecated public boolean isChecked() { return getBooleanProperty(BOOLEAN_PROPERTY_CHECKED); } /** - * Sets whether this node is checked. + * Sets whether this node is checked. This is only meaningful + * when {@link #isCheckable()} returns {@code true}. * <p> * <strong>Note:</strong> Cannot be called from an * {@link android.accessibilityservice.AccessibilityService}. * This class is made immutable before being delivered to an AccessibilityService. * </p> * + * @deprecated Use {@link #setChecked(int)} instead. + * * @param checked True if the node is checked. * * @throws IllegalStateException If called from an AccessibilityService. */ + @FlaggedApi(Flags.FLAG_TRI_STATE_CHECKED) + @Deprecated public void setChecked(boolean checked) { setBooleanProperty(BOOLEAN_PROPERTY_CHECKED, checked); + if (Flags.triStateChecked()) { + mChecked = checked ? CHECKED_STATE_TRUE : CHECKED_STATE_FALSE; + } + } + + /** + * Gets the checked state of this node. This is only meaningful + * when {@link #isCheckable()} returns {@code true}. + * + * @see #setCheckable(boolean) + * @see #isCheckable() + * @see #setChecked(int) + * + * @return The checked state, one of: + * <ul> + * <li>{@link #CHECKED_STATE_FALSE} + * <li>{@link #CHECKED_STATE_TRUE} + * <li>{@link #CHECKED_STATE_PARTIAL} + * </ul> + */ + @FlaggedApi(Flags.FLAG_TRI_STATE_CHECKED) + public @CheckedState int getChecked() { + return mChecked; + } + + /** + * Sets the checked state of this node. This is only meaningful + * when {@link #isCheckable()} returns {@code true}. + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * + * @see #setCheckable(boolean) + * @see #isCheckable() + * @see #getChecked() + * + * @param checked The checked state. One of + * <ul> + * <li>{@link #CHECKED_STATE_FALSE} + * <li>{@link #CHECKED_STATE_TRUE} + * <li>{@link #CHECKED_STATE_PARTIAL} + * </ul> + * + * @throws IllegalStateException If called from an AccessibilityService. + * @throws IllegalArgumentException if checked is not one of {@link #CHECKED_STATE_FALSE}, + * {@link #CHECKED_STATE_TRUE}, or {@link #CHECKED_STATE_PARTIAL}. + */ + @FlaggedApi(Flags.FLAG_TRI_STATE_CHECKED) + public void setChecked(@CheckedState int checked) { + enforceNotSealed(); + switch (checked) { + case CHECKED_STATE_FALSE: + case CHECKED_STATE_TRUE: + case CHECKED_STATE_PARTIAL: + mChecked = checked; + break; + default: + throw new IllegalArgumentException("Unknown checked argument: " + checked); + } + setBooleanProperty(BOOLEAN_PROPERTY_CHECKED, checked == CHECKED_STATE_TRUE); } /** @@ -4515,6 +4622,10 @@ public class AccessibilityNodeInfo implements Parcelable { if (mLeashedParentNodeId != DEFAULT.mLeashedParentNodeId) { nonDefaultFields |= bitAt(fieldIndex); } + fieldIndex++; + if (mChecked != DEFAULT.mChecked) { + nonDefaultFields |= bitAt(fieldIndex); + } int totalFields = fieldIndex; parcel.writeLong(nonDefaultFields); @@ -4683,6 +4794,9 @@ public class AccessibilityNodeInfo implements Parcelable { if (isBitSet(nonDefaultFields, fieldIndex++)) { parcel.writeLong(mLeashedParentNodeId); } + if (isBitSet(nonDefaultFields, fieldIndex++)) { + parcel.writeInt(mChecked); + } if (DEBUG) { fieldIndex--; @@ -4771,6 +4885,7 @@ public class AccessibilityNodeInfo implements Parcelable { mLeashedChild = other.mLeashedChild; mLeashedParent = other.mLeashedParent; mLeashedParentNodeId = other.mLeashedParentNodeId; + mChecked = other.mChecked; } private void initCopyInfos(AccessibilityNodeInfo other) { @@ -4960,6 +5075,9 @@ public class AccessibilityNodeInfo implements Parcelable { if (isBitSet(nonDefaultFields, fieldIndex++)) { mLeashedParentNodeId = parcel.readLong(); } + if (isBitSet(nonDefaultFields, fieldIndex++)) { + mChecked = parcel.readInt(); + } mSealed = sealed; } diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index 69cbb9b2bfb8..8ffae845de1f 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -219,6 +219,13 @@ flag { } flag { + name: "tri_state_checked" + namespace: "accessibility" + description: "Feature flag for adding tri-state checked api" + bug: "333784774" +} + +flag { name: "warning_use_default_dialog_type" namespace: "accessibility" description: "Uses the default type for the A11yService warning dialog, instead of SYSTEM_ALERT_DIALOG" @@ -226,4 +233,4 @@ flag { metadata { purpose: PURPOSE_BUGFIX } -} + } diff --git a/core/jni/android_view_WindowManagerGlobal.cpp b/core/jni/android_view_WindowManagerGlobal.cpp index abc621d8dc90..4202de39adb0 100644 --- a/core/jni/android_view_WindowManagerGlobal.cpp +++ b/core/jni/android_view_WindowManagerGlobal.cpp @@ -69,8 +69,8 @@ void removeInputChannel(const sp<IBinder>& clientToken) { JNIEnv* env = AndroidRuntime::getJNIEnv(); ScopedLocalRef<jobject> clientTokenObj(env, javaObjectForIBinder(env, clientToken)); - env->CallStaticObjectMethod(gWindowManagerGlobal.clazz, gWindowManagerGlobal.removeInputChannel, - clientTokenObj.get()); + env->CallStaticVoidMethod(gWindowManagerGlobal.clazz, gWindowManagerGlobal.removeInputChannel, + clientTokenObj.get()); } int register_android_view_WindowManagerGlobal(JNIEnv* env) { @@ -88,4 +88,4 @@ int register_android_view_WindowManagerGlobal(JNIEnv* env) { return NO_ERROR; } -} // namespace android
\ No newline at end of file +} // namespace android diff --git a/core/jni/platform/host/HostRuntime.cpp b/core/jni/platform/host/HostRuntime.cpp index 88b3e1c1ed9d..27417c0c9929 100644 --- a/core/jni/platform/host/HostRuntime.cpp +++ b/core/jni/platform/host/HostRuntime.cpp @@ -115,9 +115,9 @@ static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = { #ifdef __linux__ {"android.content.res.ApkAssets", REG_JNI(register_android_content_res_ApkAssets)}, {"android.content.res.AssetManager", REG_JNI(register_android_content_AssetManager)}, +#endif {"android.content.res.StringBlock", REG_JNI(register_android_content_StringBlock)}, {"android.content.res.XmlBlock", REG_JNI(register_android_content_XmlBlock)}, -#endif {"android.database.CursorWindow", REG_JNI(register_android_database_CursorWindow)}, {"android.database.sqlite.SQLiteConnection", REG_JNI(register_android_database_SQLiteConnection)}, diff --git a/core/tests/coretests/src/android/app/assist/AssistStructureTest.java b/core/tests/coretests/src/android/app/assist/AssistStructureTest.java index a28b2f66aaa8..51e79e7ac4e1 100644 --- a/core/tests/coretests/src/android/app/assist/AssistStructureTest.java +++ b/core/tests/coretests/src/android/app/assist/AssistStructureTest.java @@ -24,6 +24,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import android.app.assist.AssistStructure.ViewNode; import android.app.assist.AssistStructure.ViewNodeBuilder; @@ -37,6 +38,7 @@ import android.os.Bundle; import android.os.LocaleList; import android.os.OutcomeReceiver; import android.os.Parcel; +import android.os.PooledStringWriter; import android.os.SystemClock; import android.text.InputFilter; import android.util.Log; @@ -355,6 +357,18 @@ public class AssistStructureTest { } + @Test + public void testParcelTransferWriter_writeNull() { + AssistStructure structure = new AssistStructure(mActivity, FOR_AUTOFILL, NO_FLAGS); + Parcel parcel = Parcel.obtain(); + AssistStructure.ParcelTransferWriter writer = + new AssistStructure.ParcelTransferWriter(structure, parcel); + writer.writeView(null, parcel, new PooledStringWriter(parcel), 0); + + // No throw any exception. + assertTrue(true); + } + private EditText newSmallView() { EditText view = new EditText(mContext); view.setText("I AM GROOT"); diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java index 6e563ff44478..da202b63d0f1 100644 --- a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java +++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java @@ -46,7 +46,7 @@ public class AccessibilityNodeInfoTest { // The number of fields tested in the corresponding CTS AccessibilityNodeInfoTest: // See fullyPopulateAccessibilityNodeInfo, assertEqualsAccessibilityNodeInfo, // and assertAccessibilityNodeInfoCleared in that class. - private static final int NUM_MARSHALLED_PROPERTIES = 44; + private static final int NUM_MARSHALLED_PROPERTIES = 45; /** * The number of properties that are purposely not marshalled 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 3f6dc94d6a3f..92535f37094a 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 @@ -465,6 +465,12 @@ class DesktopTasksController( removeWallpaperActivity(wct) } taskRepository.addClosingTask(displayId, taskId) + taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( + doesAnyTaskRequireTaskbarRounding( + displayId, + taskId + ) + ) } fun minimizeTask(taskInfo: RunningTaskInfo) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt index a4bc2fe9460b..0b1bb8f36fa8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt @@ -21,6 +21,7 @@ import android.content.Context import android.os.IBinder import android.view.SurfaceControl import android.view.WindowManager +import android.view.WindowManager.TRANSIT_CLOSE import android.view.WindowManager.TRANSIT_TO_BACK import android.window.TransitionInfo import android.window.WindowContainerTransaction @@ -36,8 +37,8 @@ import com.android.wm.shell.transition.Transitions /** * A [Transitions.TransitionObserver] that observes shell transitions and updates the - * [DesktopRepository] state TODO: b/332682201 This observes transitions related to desktop - * mode and other transitions that originate both within and outside shell. + * [DesktopRepository] state TODO: b/332682201 This observes transitions related to desktop mode and + * other transitions that originate both within and outside shell. */ class DesktopTasksTransitionObserver( private val context: Context, @@ -47,6 +48,8 @@ class DesktopTasksTransitionObserver( shellInit: ShellInit ) : Transitions.TransitionObserver { + private var transitionToCloseWallpaper: IBinder? = null + init { if (DesktopModeStatus.canEnterDesktopMode(context)) { shellInit.addInitCallback(::onInit, this) @@ -70,6 +73,7 @@ class DesktopTasksTransitionObserver( handleBackNavigation(info) removeTaskIfNeeded(info) } + removeWallpaperOnLastTaskClosingIfNeeded(transition, info) } private fun removeTaskIfNeeded(info: TransitionInfo) { @@ -81,13 +85,9 @@ class DesktopTasksTransitionObserver( val taskInfo = change.taskInfo if (taskInfo == null || taskInfo.taskId == -1) continue - if (desktopRepository.isActiveTask(taskInfo.taskId) - && taskInfo.windowingMode != WINDOWING_MODE_FREEFORM - ) { - desktopRepository.removeFreeformTask( - taskInfo.displayId, - taskInfo.taskId - ) + if (desktopRepository.isActiveTask(taskInfo.taskId) && + taskInfo.windowingMode != WINDOWING_MODE_FREEFORM) { + desktopRepository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId) } } } @@ -104,14 +104,32 @@ class DesktopTasksTransitionObserver( if (desktopRepository.getVisibleTaskCount(taskInfo.displayId) > 0 && change.mode == TRANSIT_TO_BACK && - taskInfo.windowingMode == WINDOWING_MODE_FREEFORM - ) { + taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) { desktopRepository.minimizeTask(taskInfo.displayId, taskInfo.taskId) } } } } + private fun removeWallpaperOnLastTaskClosingIfNeeded( + transition: IBinder, + info: TransitionInfo + ) { + for (change in info.changes) { + val taskInfo = change.taskInfo + if (taskInfo == null || taskInfo.taskId == -1) { + continue + } + + if (desktopRepository.getVisibleTaskCount(taskInfo.displayId) == 1 && + change.mode == TRANSIT_CLOSE && + taskInfo.windowingMode == WINDOWING_MODE_FREEFORM && + desktopRepository.wallpaperActivityToken != null) { + transitionToCloseWallpaper = transition + } + } + } + override fun onTransitionStarting(transition: IBinder) { // TODO: b/332682201 Update repository state } @@ -122,6 +140,16 @@ class DesktopTasksTransitionObserver( override fun onTransitionFinished(transition: IBinder, aborted: Boolean) { // TODO: b/332682201 Update repository state + if (transitionToCloseWallpaper == transition) { + // TODO: b/362469671 - Handle merging the animation when desktop is also closing. + desktopRepository.wallpaperActivityToken?.let { wallpaperActivityToken -> + transitions.startTransition( + TRANSIT_CLOSE, + WindowContainerTransaction().removeTask(wallpaperActivityToken), + null) + } + transitionToCloseWallpaper = null + } } private fun updateWallpaperToken(info: TransitionInfo) { @@ -139,10 +167,9 @@ class DesktopTasksTransitionObserver( // task. shellTaskOrganizer.applyTransaction( WindowContainerTransaction() - .setTaskTrimmableFromRecents(taskInfo.token, false) - ) + .setTaskTrimmableFromRecents(taskInfo.token, false)) } - WindowManager.TRANSIT_CLOSE -> + TRANSIT_CLOSE -> desktopRepository.wallpaperActivityToken = null else -> {} } diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt index 2980d5113bba..e176f47d4094 100644 --- a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.flicker.appcompat import android.platform.test.annotations.Postsubmit -import android.tools.Rotation import android.tools.flicker.assertions.FlickerTest import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder @@ -109,9 +108,7 @@ class OpenTransparentActivityTest(flicker: LegacyFlickerTest) : TransparentBaseA @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): Collection<FlickerTest> { - return LegacyFlickerTestFactory.nonRotationTests( - supportedRotations = listOf(Rotation.ROTATION_90) - ) + return LegacyFlickerTestFactory.nonRotationTests() } } } diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt index 2484f675b236..9b8c949a1705 100644 --- a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt @@ -20,7 +20,6 @@ import android.graphics.Rect import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.RequiresDevice import android.tools.NavBar -import android.tools.Rotation import android.tools.flicker.assertions.FlickerTest import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder @@ -266,8 +265,7 @@ class QuickSwitchLauncherToLetterboxAppTest(flicker: LegacyFlickerTest) : BaseAp @JvmStatic fun getParams(): Collection<FlickerTest> { return LegacyFlickerTestFactory.nonRotationTests( - supportedNavigationModes = listOf(NavBar.MODE_GESTURAL), - supportedRotations = listOf(Rotation.ROTATION_90) + supportedNavigationModes = listOf(NavBar.MODE_GESTURAL) ) } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt index 598df34a310d..fe87aa88a8db 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt @@ -22,26 +22,38 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.content.ComponentName import android.content.Context import android.content.Intent +import android.os.IBinder import android.platform.test.annotations.EnableFlags import android.view.Display.DEFAULT_DISPLAY +import android.view.WindowManager +import android.view.WindowManager.TRANSIT_CLOSE import android.view.WindowManager.TRANSIT_OPEN import android.view.WindowManager.TRANSIT_TO_BACK import android.window.IWindowContainerToken import android.window.TransitionInfo import android.window.TransitionInfo.Change import android.window.WindowContainerToken +import android.window.WindowContainerTransaction +import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_TASK import com.android.modules.utils.testing.ExtendedMockitoRule import com.android.window.flags.Flags +import com.android.wm.shell.MockToken import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage import org.junit.Before import org.junit.Rule import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.eq +import org.mockito.ArgumentMatchers.isA import org.mockito.Mockito import org.mockito.kotlin.any +import org.mockito.kotlin.isNull import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.spy @@ -130,6 +142,27 @@ class DesktopTasksTransitionObserverTest { verify(taskRepository).removeFreeformTask(task.displayId, task.taskId) } + @Test + fun closeLastTask_wallpaperTokenExists_wallpaperIsRemoved() { + val mockTransition = Mockito.mock(IBinder::class.java) + val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM) + val wallpaperToken = MockToken().token() + whenever(taskRepository.getVisibleTaskCount(task.displayId)).thenReturn(1) + whenever(taskRepository.wallpaperActivityToken).thenReturn(wallpaperToken) + + transitionObserver.onTransitionReady( + transition = mockTransition, + info = createCloseTransition(task), + startTransaction = mock(), + finishTransaction = mock(), + ) + transitionObserver.onTransitionFinished(mockTransition, false) + + val wct = getLatestWct(type = TRANSIT_CLOSE) + assertThat(wct.hierarchyOps).hasSize(1) + wct.assertRemoveAt(index = 0, wallpaperToken) + } + private fun createBackNavigationTransition( task: RunningTaskInfo? ): TransitionInfo { @@ -160,6 +193,48 @@ class DesktopTasksTransitionObserverTest { } } + private fun createCloseTransition( + task: RunningTaskInfo? + ): TransitionInfo { + return TransitionInfo(TRANSIT_CLOSE, 0 /* flags */).apply { + addChange( + Change(mock(), mock()).apply { + mode = TRANSIT_CLOSE + parent = null + taskInfo = task + flags = flags + } + ) + } + } + + private fun getLatestWct( + @WindowManager.TransitionType type: Int = TRANSIT_OPEN, + handlerClass: Class<out Transitions.TransitionHandler>? = null + ): WindowContainerTransaction { + val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + if (handlerClass == null) { + Mockito.verify(transitions).startTransition(eq(type), arg.capture(), isNull()) + } else { + Mockito.verify(transitions) + .startTransition(eq(type), arg.capture(), isA(handlerClass)) + } + return arg.value + } + + private fun WindowContainerTransaction.assertRemoveAt(index: Int, token: WindowContainerToken) { + assertIndexInBounds(index) + val op = hierarchyOps[index] + assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK) + assertThat(op.container).isEqualTo(token.asBinder()) + } + + private fun WindowContainerTransaction.assertIndexInBounds(index: Int) { + assertWithMessage("WCT does not have a hierarchy operation at index $index") + .that(hierarchyOps.size) + .isGreaterThan(index) + } + private fun createTaskInfo(id: Int, windowingMode: Int = WINDOWING_MODE_FREEFORM) = RunningTaskInfo().apply { taskId = id diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt index bc269fedddfe..e9845c1d9f13 100644 --- a/libs/appfunctions/api/current.txt +++ b/libs/appfunctions/api/current.txt @@ -15,7 +15,8 @@ package com.google.android.appfunctions.sidecar { public abstract class AppFunctionService extends android.app.Service { ctor public AppFunctionService(); method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent); - method @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + method @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + method @Deprecated @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); method @Deprecated @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); field @NonNull public static final String BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE"; field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService"; diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java index 6e91de6bbcf2..2a168e871713 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java @@ -24,8 +24,8 @@ import android.annotation.Nullable; import android.app.Service; import android.content.Intent; import android.os.Binder; -import android.os.IBinder; import android.os.CancellationSignal; +import android.os.IBinder; import android.util.Log; import java.util.function.Consumer; @@ -71,18 +71,21 @@ public abstract class AppFunctionService extends Service { private final Binder mBinder = android.app.appfunctions.AppFunctionService.createBinder( /* context= */ this, - /* onExecuteFunction= */ (platformRequest, cancellationSignal, callback) -> { + /* onExecuteFunction= */ (platformRequest, + callingPackage, + cancellationSignal, + callback) -> { AppFunctionService.this.onExecuteFunction( SidecarConverter.getSidecarExecuteAppFunctionRequest( platformRequest), + callingPackage, cancellationSignal, (sidecarResponse) -> { callback.accept( SidecarConverter.getPlatformExecuteAppFunctionResponse( sidecarResponse)); }); - } - ); + }); @NonNull @Override @@ -107,11 +110,49 @@ public abstract class AppFunctionService extends Service { * thread and dispatch the result with the given callback. You should always report back the * result using the callback, no matter if the execution was successful or not. * + * <p>This method also accepts a {@link CancellationSignal} that the app should listen to cancel + * the execution of function if requested by the system. + * + * @param request The function execution request. + * @param callingPackage The package name of the app that is requesting the execution. + * @param cancellationSignal A signal to cancel the execution. + * @param callback A callback to report back the result. + */ + @MainThread + public void onExecuteFunction( + @NonNull ExecuteAppFunctionRequest request, + @NonNull String callingPackage, + @NonNull CancellationSignal cancellationSignal, + @NonNull Consumer<ExecuteAppFunctionResponse> callback) { + onExecuteFunction(request, cancellationSignal, callback); + } + + /** + * Called by the system to execute a specific app function. + * + * <p>This method is triggered when the system requests your AppFunctionService to handle a + * particular function you have registered and made available. + * + * <p>To ensure proper routing of function requests, assign a unique identifier to each + * function. This identifier doesn't need to be globally unique, but it must be unique within + * your app. For example, a function to order food could be identified as "orderFood". In most + * cases this identifier should come from the ID automatically generated by the AppFunctions + * SDK. You can determine the specific function to invoke by calling {@link + * ExecuteAppFunctionRequest#getFunctionIdentifier()}. + * + * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker + * thread and dispatch the result with the given callback. You should always report back the + * result using the callback, no matter if the execution was successful or not. + * * @param request The function execution request. * @param cancellationSignal A {@link CancellationSignal} to cancel the request. * @param callback A callback to report back the result. + * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, String, + * CancellationSignal, Consumer)} instead. This method will be removed once usage references + * are updated. */ @MainThread + @Deprecated public void onExecuteFunction( @NonNull ExecuteAppFunctionRequest request, @NonNull CancellationSignal cancellationSignal, @@ -138,7 +179,6 @@ public abstract class AppFunctionService extends Service { * * @param request The function execution request. * @param callback A callback to report back the result. - * * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, CancellationSignal, * Consumer)} instead. This method will be removed once usage references are updated. */ diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java index d87fec7985e9..969e5d58b909 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java @@ -234,12 +234,13 @@ public final class ExecuteAppFunctionResponse { @IntDef( prefix = {"RESULT_"}, value = { - RESULT_OK, - RESULT_DENIED, - RESULT_APP_UNKNOWN_ERROR, - RESULT_INTERNAL_ERROR, - RESULT_INVALID_ARGUMENT, - RESULT_DISABLED + RESULT_OK, + RESULT_DENIED, + RESULT_APP_UNKNOWN_ERROR, + RESULT_INTERNAL_ERROR, + RESULT_INVALID_ARGUMENT, + RESULT_DISABLED, + RESULT_CANCELLED }) @Retention(RetentionPolicy.SOURCE) public @interface ResultCode {} diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference.xml b/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference.xml new file mode 100644 index 000000000000..952562e3d8ea --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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. + --> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?android:attr/listPreferredItemHeightSmall" + android:gravity="center_vertical" + android:paddingLeft="?android:attr/listPreferredItemPaddingLeft" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingRight="?android:attr/listPreferredItemPaddingRight" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:background="?android:attr/selectableItemBackground" + android:clipToPadding="false" + android:baselineAligned="false"> + + <include layout="@layout/settingslib_icon_frame"/> + + <include layout="@layout/settingslib_preference_frame"/> + + <!-- Preference should place its actual preference widget here. --> + <LinearLayout + android:id="@android:id/widget_frame" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="end|center_vertical" + android:paddingLeft="16dp" + android:paddingStart="16dp" + android:paddingRight="0dp" + android:paddingEnd="0dp" + android:orientation="vertical"/> + +</LinearLayout>
\ No newline at end of file diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt index 2a251a59e1d8..dfd296fe006f 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt @@ -39,6 +39,7 @@ import com.android.settingslib.spa.gallery.page.LoadingBarPageProvider import com.android.settingslib.spa.gallery.page.ProgressBarPageProvider import com.android.settingslib.spa.gallery.scaffold.NonScrollablePagerPageProvider import com.android.settingslib.spa.gallery.page.SliderPageProvider +import com.android.settingslib.spa.gallery.preference.CheckBoxPreferencePageProvider import com.android.settingslib.spa.gallery.preference.IntroPreferencePageProvider import com.android.settingslib.spa.gallery.preference.ListPreferencePageProvider import com.android.settingslib.spa.gallery.preference.MainSwitchPreferencePageProvider @@ -46,6 +47,7 @@ import com.android.settingslib.spa.gallery.preference.PreferenceMainPageProvider import com.android.settingslib.spa.gallery.preference.PreferencePageProvider import com.android.settingslib.spa.gallery.preference.SwitchPreferencePageProvider import com.android.settingslib.spa.gallery.preference.TopIntroPreferencePageProvider +import com.android.settingslib.spa.gallery.preference.TwoTargetButtonPreferencePageProvider import com.android.settingslib.spa.gallery.preference.TwoTargetSwitchPreferencePageProvider import com.android.settingslib.spa.gallery.preference.ZeroStatePreferencePageProvider import com.android.settingslib.spa.gallery.scaffold.PagerMainPageProvider @@ -105,6 +107,8 @@ class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) { CopyablePageProvider, IntroPreferencePageProvider, TopIntroPreferencePageProvider, + CheckBoxPreferencePageProvider, + TwoTargetButtonPreferencePageProvider, ), rootPages = listOf( HomePageProvider.createSettingsPage(), diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/DialogMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/DialogMainPageProvider.kt index c9c81aac01c3..cb055049e6c4 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/DialogMainPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/DialogMainPageProvider.kt @@ -19,50 +19,93 @@ package com.android.settingslib.spa.gallery.dialog import android.os.Bundle import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import com.android.settingslib.spa.framework.common.SettingsEntry -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.widget.dialog.AlertDialogButton +import com.android.settingslib.spa.widget.dialog.SettingsAlertDialogWithIcon import com.android.settingslib.spa.widget.dialog.rememberAlertDialogPresenter import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.scaffold.RegularScaffold +import com.android.settingslib.spa.widget.ui.Category private const val TITLE = "Category: Dialog" object DialogMainPageProvider : SettingsPageProvider { override val name = "DialogMain" - private val owner = createSettingsPage() - override fun buildEntry(arguments: Bundle?): List<SettingsEntry> = listOf( - SettingsEntryBuilder.create("AlertDialog", owner).setUiLayoutFn { - val alertDialogPresenter = rememberAlertDialogPresenter( - confirmButton = AlertDialogButton("Ok"), - dismissButton = AlertDialogButton("Cancel"), - title = "Title", - text = { Text("Text") }, - ) - Preference(object : PreferenceModel { - override val title = "Show AlertDialog" - override val onClick = alertDialogPresenter::open - }) - }.build(), - SettingsEntryBuilder.create("NavDialog", owner).setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = "Navigate to Dialog" - override val onClick = navigator(route = NavDialogProvider.name) - }) - }.build(), - ) + @Composable + override fun Page(arguments: Bundle?) { + RegularScaffold(TITLE) { + Category { + AlertDialog() + AlertDialogWithIcon() + NavDialog() + } + } + } @Composable fun Entry() { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) + Preference( + object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + } + ) } override fun getTitle(arguments: Bundle?) = TITLE } + +@Composable +private fun AlertDialog() { + val alertDialogPresenter = + rememberAlertDialogPresenter( + confirmButton = AlertDialogButton("Ok"), + dismissButton = AlertDialogButton("Cancel"), + title = "Title", + text = { Text("Text") }, + ) + Preference( + object : PreferenceModel { + override val title = "Show AlertDialog" + override val onClick = alertDialogPresenter::open + } + ) +} + +@Composable +private fun AlertDialogWithIcon() { + var openDialog by rememberSaveable { mutableStateOf(false) } + val close = { openDialog = false } + val open = { openDialog = true } + if (openDialog) { + SettingsAlertDialogWithIcon( + title = "Title", + onDismissRequest = close, + confirmButton = AlertDialogButton("OK", onClick = close), + dismissButton = AlertDialogButton("Dismiss", onClick = close), + ) {} + } + Preference( + object : PreferenceModel { + override val title = "Show AlertDialogWithIcon" + override val onClick = open + } + ) +} + +@Composable +private fun NavDialog() { + Preference( + object : PreferenceModel { + override val title = "Navigate to Dialog" + override val onClick = navigator(route = NavDialogProvider.name) + } + ) +} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/CheckBoxPreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/CheckBoxPreferencePageProvider.kt new file mode 100644 index 000000000000..c2b67cbfe56f --- /dev/null +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/CheckBoxPreferencePageProvider.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.gallery.preference + +import android.os.Bundle +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AirplanemodeActive +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import com.android.settingslib.spa.framework.common.SettingsPageProvider +import com.android.settingslib.spa.framework.compose.navigator +import com.android.settingslib.spa.widget.preference.CheckboxPreference +import com.android.settingslib.spa.widget.preference.CheckboxPreferenceModel +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.scaffold.RegularScaffold +import com.android.settingslib.spa.widget.ui.Category +import com.android.settingslib.spa.widget.ui.SettingsIcon + +private const val TITLE = "Sample CheckBoxPreference" + +object CheckBoxPreferencePageProvider : SettingsPageProvider { + override val name = "CheckBoxPreference" + + @Composable + override fun Page(arguments: Bundle?) { + RegularScaffold(TITLE) { + Category { + var checked1 by rememberSaveable { mutableStateOf(true) } + CheckboxPreference( + object : CheckboxPreferenceModel { + override val title = "Use Dark theme" + override val checked = { checked1 } + override val onCheckedChange = { newChecked: Boolean -> + checked1 = newChecked + } + } + ) + var checked2 by rememberSaveable { mutableStateOf(false) } + CheckboxPreference( + object : CheckboxPreferenceModel { + override val title = "Use Dark theme" + override val summary = { "Summary" } + override val checked = { checked2 } + override val onCheckedChange = { newChecked: Boolean -> + checked2 = newChecked + } + } + ) + var checked3 by rememberSaveable { mutableStateOf(true) } + CheckboxPreference( + object : CheckboxPreferenceModel { + override val title = "Use Dark theme" + override val summary = { "Summary" } + override val checked = { checked3 } + override val onCheckedChange = { newChecked: Boolean -> + checked3 = newChecked + } + override val icon = + @Composable { + SettingsIcon(imageVector = Icons.Outlined.AirplanemodeActive) + } + } + ) + } + } + } + + @Composable + fun Entry() { + Preference( + object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + } + ) + } + + override fun getTitle(arguments: Bundle?): String { + return TITLE + } +} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt index 831b43942e98..3cfb5368e24a 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt @@ -36,11 +36,13 @@ object PreferenceMainPageProvider : SettingsPageProvider { Category { PreferencePageProvider.Entry() ListPreferencePageProvider.Entry() + CheckBoxPreferencePageProvider.Entry() } Category { SwitchPreferencePageProvider.Entry() MainSwitchPreferencePageProvider.Entry() TwoTargetSwitchPreferencePageProvider.Entry() + TwoTargetButtonPreferencePageProvider.Entry() } Category { ZeroStatePreferencePageProvider.Entry() diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetButtonPreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetButtonPreferencePageProvider.kt new file mode 100644 index 000000000000..c6e834a3fa7b --- /dev/null +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetButtonPreferencePageProvider.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.gallery.preference + +import android.os.Bundle +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.outlined.Info +import androidx.compose.runtime.Composable +import com.android.settingslib.spa.framework.common.SettingsPageProvider +import com.android.settingslib.spa.framework.compose.navigator +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.preference.TwoTargetButtonPreference +import com.android.settingslib.spa.widget.scaffold.RegularScaffold +import com.android.settingslib.spa.widget.ui.Category + +private const val TITLE = "Sample TwoTargetButtonPreference" + +object TwoTargetButtonPreferencePageProvider : SettingsPageProvider { + override val name = "TwoTargetButtonPreference" + + @Composable + override fun Page(arguments: Bundle?) { + RegularScaffold(TITLE) { + Category { + SampleTwoTargetButtonPreference() + SampleTwoTargetButtonPreferenceWithSummary() + } + } + } + + @Composable + fun Entry() { + Preference( + object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + } + ) + } +} + +@Composable +private fun SampleTwoTargetButtonPreference() { + TwoTargetButtonPreference( + title = "TwoTargetButton", + summary = { "" }, + buttonIcon = Icons.Outlined.Info, + buttonIconDescription = "info", + onClick = {}, + onButtonClick = {}, + ) +} + +@Composable +private fun SampleTwoTargetButtonPreferenceWithSummary() { + TwoTargetButtonPreference( + title = "TwoTargetButton", + summary = { "summary" }, + buttonIcon = Icons.Outlined.Add, + buttonIconDescription = "info", + onClick = {}, + onButtonClick = {}, + ) +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt index ab95162fb142..5dca63724faf 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt @@ -85,4 +85,6 @@ object SettingsDimension { val illustrationMaxHeight = 300.dp val illustrationPadding = paddingLarge val illustrationCornerRadius = 28.dp + + val preferenceMinHeight = 72.dp } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogWithIcon.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogWithIcon.kt index 58a83fa72532..4cf270dca2bb 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogWithIcon.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogWithIcon.kt @@ -17,7 +17,6 @@ package com.android.settingslib.spa.widget.dialog import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -26,12 +25,13 @@ import androidx.compose.material.icons.filled.WarningAmber import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.window.DialogProperties +import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled @Composable fun SettingsAlertDialogWithIcon( @@ -57,7 +57,9 @@ fun SettingsAlertDialogWithIcon( title?.let { { CenterRow { - Text(it, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center) + if (isSpaExpressiveEnabled) + Text(it, style = MaterialTheme.typography.bodyLarge) + else Text(it) } } }, diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt index c68ec78b1ba6..acb96be64a34 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -35,6 +36,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.min import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsOpacity.alphaForEnabled import com.android.settingslib.spa.framework.theme.SettingsShape @@ -62,7 +64,8 @@ internal fun BaseLayout( .semantics(mergeDescendants = true) {} .then( if (isSpaExpressiveEnabled) - Modifier.clip(SettingsShape.CornerExtraSmall) + Modifier.heightIn(min = SettingsDimension.preferenceMinHeight) + .clip(SettingsShape.CornerExtraSmall) .background(MaterialTheme.colorScheme.surfaceBright) else Modifier ) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt index b28e88eb8af8..e6a23662e9f0 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt @@ -17,6 +17,7 @@ package com.android.settingslib.spa.widget.preference import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme @@ -25,6 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.min import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsShape import com.android.settingslib.spa.framework.theme.SettingsTheme @@ -35,13 +37,19 @@ import com.android.settingslib.spa.framework.util.EntryHighlight fun MainSwitchPreference(model: SwitchPreferenceModel) { EntryHighlight { Surface( - modifier = Modifier.padding(SettingsDimension.itemPaddingEnd), - color = when (model.checked()) { - true -> MaterialTheme.colorScheme.primaryContainer - else -> MaterialTheme.colorScheme.secondaryContainer - }, - shape = if (isSpaExpressiveEnabled) CircleShape - else SettingsShape.CornerExtraLarge, + modifier = + Modifier.padding(SettingsDimension.itemPaddingEnd) + .then( + if (isSpaExpressiveEnabled) + Modifier.heightIn(min = SettingsDimension.preferenceMinHeight) + else Modifier + ), + color = + when (model.checked()) { + true -> MaterialTheme.colorScheme.primaryContainer + else -> MaterialTheme.colorScheme.secondaryContainer + }, + shape = if (isSpaExpressiveEnabled) CircleShape else SettingsShape.CornerExtraLarge, ) { InternalSwitchPreference( title = model.title, @@ -61,16 +69,20 @@ fun MainSwitchPreference(model: SwitchPreferenceModel) { private fun MainSwitchPreferencePreview() { SettingsTheme { Column { - MainSwitchPreference(object : SwitchPreferenceModel { - override val title = "Use Dark theme" - override val checked = { true } - override val onCheckedChange: (Boolean) -> Unit = {} - }) - MainSwitchPreference(object : SwitchPreferenceModel { - override val title = "Use Dark theme" - override val checked = { false } - override val onCheckedChange: (Boolean) -> Unit = {} - }) + MainSwitchPreference( + object : SwitchPreferenceModel { + override val title = "Use Dark theme" + override val checked = { true } + override val onCheckedChange: (Boolean) -> Unit = {} + } + ) + MainSwitchPreference( + object : SwitchPreferenceModel { + override val title = "Use Dark theme" + override val checked = { false } + override val onCheckedChange: (Boolean) -> Unit = {} + } + ) } } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt index b771f367e697..541922335387 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt @@ -54,7 +54,7 @@ fun ZeroStatePreference(icon: ImageVector, text: String? = null, description: St val zeroStateShape = remember { RoundedPolygon.star( numVerticesPerRadius = 6, - innerRadius = 0.75f, + innerRadius = 0.8f, rounding = CornerRounding(0.3f) ) } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java index 8b6351e3c289..7fdb32cb63e9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java @@ -60,7 +60,7 @@ public class CachedBluetoothDeviceManager { mBtManager = localBtManager; mHearingAidDeviceManager = new HearingAidDeviceManager(context, localBtManager, mCachedDevices); - mCsipDeviceManager = new CsipDeviceManager(localBtManager, mCachedDevices); + mCsipDeviceManager = new CsipDeviceManager(context, localBtManager, mCachedDevices); } public synchronized Collection<CachedBluetoothDevice> getCachedDevicesCopy() { diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java index 6dab22454baf..b9f16edf6e77 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java @@ -21,8 +21,10 @@ import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; +import android.content.Context; import android.os.Build; import android.os.ParcelUuid; +import android.os.UserManager; import android.util.Log; import androidx.annotation.ChecksSdkIntAtLeast; @@ -45,9 +47,11 @@ public class CsipDeviceManager { private final LocalBluetoothManager mBtManager; private final List<CachedBluetoothDevice> mCachedDevices; + private final Context mContext; - CsipDeviceManager(LocalBluetoothManager localBtManager, + CsipDeviceManager(Context context, LocalBluetoothManager localBtManager, List<CachedBluetoothDevice> cachedDevices) { + mContext = context; mBtManager = localBtManager; mCachedDevices = cachedDevices; } @@ -379,7 +383,11 @@ public class CsipDeviceManager { preferredMainDevice.refresh(); hasChanged = true; } - syncAudioSharingSourceIfNeeded(preferredMainDevice); + if (isWorkProfile()) { + log("addMemberDevicesIntoMainDevice: skip sync source for work profile"); + } else { + syncAudioSharingSourceIfNeeded(preferredMainDevice); + } } if (hasChanged) { log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: " @@ -388,6 +396,11 @@ public class CsipDeviceManager { return hasChanged; } + private boolean isWorkProfile() { + UserManager userManager = mContext.getSystemService(UserManager.class); + return userManager != null && userManager.isManagedProfile(); + } + private void syncAudioSharingSourceIfNeeded(CachedBluetoothDevice mainDevice) { boolean isAudioSharingEnabled = BluetoothUtils.isAudioSharingEnabled(); if (isAudioSharingEnabled) { diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java index 364e95c61ca8..6a9d5687370e 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java @@ -18,6 +18,8 @@ package com.android.settingslib.bluetooth; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; +import static java.util.stream.Collectors.toList; + import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.bluetooth.BluetoothAdapter; @@ -64,6 +66,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; @@ -84,6 +87,8 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { public static final String EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE = "BT_DEVICE_TO_AUTO_ADD_SOURCE"; public static final String EXTRA_START_LE_AUDIO_SHARING = "START_LE_AUDIO_SHARING"; public static final String EXTRA_PAIR_AND_JOIN_SHARING = "PAIR_AND_JOIN_SHARING"; + public static final String BLUETOOTH_LE_BROADCAST_PRIMARY_DEVICE_GROUP_ID = + "bluetooth_le_broadcast_primary_device_group_id"; public static final int BROADCAST_STATE_UNKNOWN = 0; public static final int BROADCAST_STATE_ON = 1; public static final int BROADCAST_STATE_OFF = 2; @@ -1121,6 +1126,10 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { /** Update fallback active device if needed. */ public void updateFallbackActiveDeviceIfNeeded() { + if (isWorkProfile(mContext)) { + Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded for work profile."); + return; + } if (mServiceBroadcast == null) { Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to broadcast profile is null"); return; @@ -1135,71 +1144,114 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to assistant profile is null"); return; } - List<BluetoothDevice> devicesInBroadcast = getDevicesInBroadcast(); - if (devicesInBroadcast.isEmpty()) { + Map<Integer, List<BluetoothDevice>> deviceGroupsInBroadcast = getDeviceGroupsInBroadcast(); + if (deviceGroupsInBroadcast.isEmpty()) { Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to no sinks in broadcast"); return; } - List<BluetoothDevice> devices = - BluetoothAdapter.getDefaultAdapter().getMostRecentlyConnectedDevices(); - BluetoothDevice targetDevice = null; - // Find the earliest connected device in sharing session. - int targetDeviceIdx = -1; - for (BluetoothDevice device : devicesInBroadcast) { - if (devices.contains(device)) { - int idx = devices.indexOf(device); - if (idx > targetDeviceIdx) { - targetDeviceIdx = idx; - targetDevice = device; + int targetGroupId = BluetoothCsipSetCoordinator.GROUP_ID_INVALID; + int fallbackActiveGroupId = BluetoothUtils.getPrimaryGroupIdForBroadcast( + mContext.getContentResolver()); + if (Flags.audioSharingHysteresisModeFix()) { + int userPreferredPrimaryGroupId = getUserPreferredPrimaryGroupId(); + if (userPreferredPrimaryGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID + && deviceGroupsInBroadcast.containsKey(userPreferredPrimaryGroupId)) { + if (userPreferredPrimaryGroupId == fallbackActiveGroupId) { + Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, already user preferred"); + return; + } else { + targetGroupId = userPreferredPrimaryGroupId; } } - } - if (targetDevice == null) { - Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, target is null"); + if (targetGroupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { + // If there is no user preferred primary device, set the earliest connected + // device in sharing session as the fallback. + targetGroupId = getEarliestConnectedDeviceGroup(deviceGroupsInBroadcast); + } + } else { + // Set the earliest connected device in sharing session as the fallback. + targetGroupId = getEarliestConnectedDeviceGroup(deviceGroupsInBroadcast); + } + Log.d(TAG, "updateFallbackActiveDeviceIfNeeded, target group id = " + targetGroupId); + if (targetGroupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) return; + if (targetGroupId == fallbackActiveGroupId) { + Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, already is fallback"); return; } - CachedBluetoothDevice targetCachedDevice = mDeviceManager.findDevice(targetDevice); + CachedBluetoothDevice targetCachedDevice = getMainDevice( + deviceGroupsInBroadcast.get(targetGroupId)); if (targetCachedDevice == null) { - Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, fail to find cached bt device"); - return; - } - int fallbackActiveGroupId = getFallbackActiveGroupId(); - if (fallbackActiveGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID - && BluetoothUtils.getGroupId(targetCachedDevice) == fallbackActiveGroupId) { - Log.d( - TAG, - "Skip updateFallbackActiveDeviceIfNeeded, already is fallback: " - + fallbackActiveGroupId); + Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, fail to find main device"); return; } Log.d( TAG, "updateFallbackActiveDeviceIfNeeded, set active device: " - + targetDevice.getAnonymizedAddress()); + + targetCachedDevice.getDevice()); targetCachedDevice.setActive(); } - private List<BluetoothDevice> getDevicesInBroadcast() { + @NonNull + private Map<Integer, List<BluetoothDevice>> getDeviceGroupsInBroadcast() { boolean hysteresisModeFixEnabled = Flags.audioSharingHysteresisModeFix(); List<BluetoothDevice> connectedDevices = mServiceBroadcastAssistant.getConnectedDevices(); return connectedDevices.stream() .filter( - bluetoothDevice -> { + device -> { List<BluetoothLeBroadcastReceiveState> sourceList = - mServiceBroadcastAssistant.getAllSources( - bluetoothDevice); + mServiceBroadcastAssistant.getAllSources(device); return !sourceList.isEmpty() && sourceList.stream().anyMatch( source -> hysteresisModeFixEnabled ? BluetoothUtils.isSourceMatched(source, mBroadcastId) : BluetoothUtils.isConnected(source)); }) - .collect(Collectors.toList()); + .collect(Collectors.groupingBy( + device -> BluetoothUtils.getGroupId(mDeviceManager.findDevice(device)))); + } + + private int getEarliestConnectedDeviceGroup( + @NonNull Map<Integer, List<BluetoothDevice>> deviceGroups) { + List<BluetoothDevice> devices = + BluetoothAdapter.getDefaultAdapter().getMostRecentlyConnectedDevices(); + // Find the earliest connected device in sharing session. + int targetDeviceIdx = -1; + int targetGroupId = BluetoothCsipSetCoordinator.GROUP_ID_INVALID; + for (Map.Entry<Integer, List<BluetoothDevice>> entry : deviceGroups.entrySet()) { + for (BluetoothDevice device : entry.getValue()) { + if (devices.contains(device)) { + int idx = devices.indexOf(device); + if (idx > targetDeviceIdx) { + targetDeviceIdx = idx; + targetGroupId = entry.getKey(); + } + } + } + } + return targetGroupId; + } + + @Nullable + private CachedBluetoothDevice getMainDevice(@Nullable List<BluetoothDevice> devices) { + if (devices == null || devices.size() == 1) return null; + List<CachedBluetoothDevice> cachedDevices = + devices.stream() + .map(device -> mDeviceManager.findDevice(device)) + .filter(Objects::nonNull) + .collect(toList()); + for (CachedBluetoothDevice cachedDevice : cachedDevices) { + if (!cachedDevice.getMemberDevice().isEmpty()) { + return cachedDevice; + } + } + CachedBluetoothDevice mainDevice = cachedDevices.isEmpty() ? null : cachedDevices.get(0); + return mainDevice; } - private int getFallbackActiveGroupId() { + private int getUserPreferredPrimaryGroupId() { + // TODO: use real key name in SettingsProvider return Settings.Secure.getInt( - mContext.getContentResolver(), - "bluetooth_le_broadcast_fallback_active_group_id", + mContentResolver, + BLUETOOTH_LE_BROADCAST_PRIMARY_DEVICE_GROUP_ID, BluetoothCsipSetCoordinator.GROUP_ID_INVALID); } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistantCallbackExt.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistantCallbackExt.kt index 24815fabfdf2..91a99aed6db5 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistantCallbackExt.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistantCallbackExt.kt @@ -16,7 +16,6 @@ package com.android.settingslib.bluetooth -import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothLeBroadcastAssistant import android.bluetooth.BluetoothLeBroadcastMetadata @@ -82,9 +81,5 @@ val LocalBluetoothLeBroadcastAssistant.onSourceConnectedOrRemoved: Flow<Unit> ConcurrentUtils.DIRECT_EXECUTOR, callback, ) - awaitClose { - if (BluetoothAdapter.getDefaultAdapter()?.isEnabled == true) { - unregisterServiceCallBack(callback) - } - } + awaitClose { unregisterServiceCallBack(callback) } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl index 9cf49070a62c..c85756ed067a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl @@ -20,5 +20,5 @@ import com.android.settingslib.bluetooth.devicesettings.DeviceInfo; import com.android.settingslib.bluetooth.devicesettings.IGetDeviceSettingsConfigCallback; interface IDeviceSettingsConfigProviderService { - oneway void getDeviceSettingsConfig(in DeviceInfo device, in IGetDeviceSettingsConfigCallback callback); + void getDeviceSettingsConfig(in DeviceInfo device, in IGetDeviceSettingsConfigCallback callback); }
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt index 4af0504bd73a..a33fcc6747b4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt @@ -23,6 +23,7 @@ import android.content.Intent import android.content.ServiceConnection import android.os.IBinder import android.os.IInterface +import android.os.RemoteException import android.text.TextUtils import android.util.Log import com.android.settingslib.bluetooth.BluetoothUtils @@ -55,6 +56,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow @@ -100,6 +102,9 @@ class DeviceSettingServiceConnection( private var isServiceEnabled = coroutineScope.async(backgroundCoroutineContext, start = CoroutineStart.LAZY) { val states = getSettingsProviderServices()?.values ?: return@async false + if (states.isEmpty()) { + return@async true + } combine(states) { it.toList() } .mapNotNull { allStatus -> if (allStatus.any { it is ServiceConnectionStatus.Failed }) { @@ -114,7 +119,7 @@ class DeviceSettingServiceConnection( null } } - .first() + .firstOrNull() ?: false } private var config = @@ -131,9 +136,15 @@ class DeviceSettingServiceConnection( is ServiceConnectionStatus.Connected -> flowOf( getDeviceSettingsConfigFromService( - deviceInfo { setBluetoothAddress(cachedDevice.address) }, - it.service, - ) + deviceInfo { setBluetoothAddress(cachedDevice.address) }, + it.service, + ) + .also { config -> + Log.i( + TAG, + "device setting config for $cachedDevice is $config", + ) + } ) ServiceConnectionStatus.Connecting -> flowOf() ServiceConnectionStatus.Failed -> flowOf(null) @@ -146,21 +157,26 @@ class DeviceSettingServiceConnection( deviceInfo: DeviceInfo, service: IDeviceSettingsConfigProviderService, ): DeviceSettingsConfig? = suspendCancellableCoroutine { continuation -> - service.getDeviceSettingsConfig( - deviceInfo, - object : IGetDeviceSettingsConfigCallback.Stub() { - override fun onResult( - status: DeviceSettingsConfigServiceStatus, - config: DeviceSettingsConfig?, - ) { - if (!status.success) { - continuation.resume(null) - } else { - continuation.resume(config) + try { + service.getDeviceSettingsConfig( + deviceInfo, + object : IGetDeviceSettingsConfigCallback.Stub() { + override fun onResult( + status: DeviceSettingsConfigServiceStatus, + config: DeviceSettingsConfig?, + ) { + if (!status.success) { + continuation.resume(null) + } else { + continuation.resume(config) + } } - } - }, - ) + }, + ) + } catch (e: RemoteException) { + Log.i(TAG, "Fail to get config") + continuation.resume(null) + } } private val settingIdToItemMapping = @@ -298,13 +314,16 @@ class DeviceSettingServiceConnection( val serviceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName, service: IBinder) { + Log.i(TAG, "Service connected for $intent") launch { send(ServiceConnectionStatus.Connected(transform(service))) } } override fun onServiceDisconnected(name: ComponentName?) { + Log.i(TAG, "Service disconnected for $intent") launch { send(ServiceConnectionStatus.Connecting) } } } + Log.i(TAG, "Try to bind service for $intent") if ( !context.bindService( intent, diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt index 43d79466d6ca..23be7baa6496 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt @@ -78,6 +78,10 @@ class FakeZenModeRepository : ZenModeRepository { mutableModesFlow.value = (mutableModesFlow.value.filter { it.id != modeId }) + mode } + fun clearModes() { + mutableModesFlow.value = listOf() + } + fun getMode(id: String): ZenMode? { return mutableModesFlow.value.find { it.id == id } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java index b1489be943e6..22b315084f3f 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java @@ -22,6 +22,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -36,6 +37,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.UserHandle; +import android.os.UserManager; import android.platform.test.flag.junit.SetFlagsRule; import android.telephony.TelephonyManager; @@ -96,6 +98,8 @@ public class BluetoothEventManagerTest { private LocalBluetoothProfileManager mLocalProfileManager; @Mock private BluetoothUtils.ErrorListener mErrorListener; + @Mock + private LocalBluetoothLeBroadcast mBroadcast; private Context mContext; private Intent mIntent; @@ -107,7 +111,7 @@ public class BluetoothEventManagerTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); - mContext = RuntimeEnvironment.application; + mContext = spy(RuntimeEnvironment.application); mBluetoothEventManager = new BluetoothEventManager( @@ -208,28 +212,14 @@ public class BluetoothEventManagerTest { */ @Test public void dispatchProfileConnectionStateChanged_flagOff_noUpdateFallbackDevice() { - ShadowBluetoothAdapter shadowBluetoothAdapter = - Shadow.extract(BluetoothAdapter.getDefaultAdapter()); - shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( - BluetoothStatusCodes.FEATURE_SUPPORTED); - shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( - BluetoothStatusCodes.FEATURE_SUPPORTED); - mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); - LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class); - when(broadcast.isProfileReady()).thenReturn(true); - LocalBluetoothLeBroadcastAssistant assistant = - mock(LocalBluetoothLeBroadcastAssistant.class); - when(assistant.isProfileReady()).thenReturn(true); - LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class); - when(profileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast); - when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant); - when(mBtManager.getProfileManager()).thenReturn(profileManager); + setUpAudioSharing(/* enableFlag= */ false, /* enableFeature= */ true, /* enableProfile= */ + true, /* workProfile= */ false); mBluetoothEventManager.dispatchProfileConnectionStateChanged( mCachedBluetoothDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); - verify(broadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); + verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); } /** @@ -239,28 +229,14 @@ public class BluetoothEventManagerTest { */ @Test public void dispatchProfileConnectionStateChanged_notSupport_noUpdateFallbackDevice() { - ShadowBluetoothAdapter shadowBluetoothAdapter = - Shadow.extract(BluetoothAdapter.getDefaultAdapter()); - shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( - BluetoothStatusCodes.FEATURE_NOT_SUPPORTED); - shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( - BluetoothStatusCodes.FEATURE_SUPPORTED); - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); - LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class); - when(broadcast.isProfileReady()).thenReturn(true); - LocalBluetoothLeBroadcastAssistant assistant = - mock(LocalBluetoothLeBroadcastAssistant.class); - when(assistant.isProfileReady()).thenReturn(true); - LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class); - when(profileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast); - when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant); - when(mBtManager.getProfileManager()).thenReturn(profileManager); + setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ false, /* enableProfile= */ + true, /* workProfile= */ false); mBluetoothEventManager.dispatchProfileConnectionStateChanged( mCachedBluetoothDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); - verify(broadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); + verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); } /** @@ -270,28 +246,14 @@ public class BluetoothEventManagerTest { */ @Test public void dispatchProfileConnectionStateChanged_profileNotReady_noUpdateFallbackDevice() { - ShadowBluetoothAdapter shadowBluetoothAdapter = - Shadow.extract(BluetoothAdapter.getDefaultAdapter()); - shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( - BluetoothStatusCodes.FEATURE_SUPPORTED); - shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( - BluetoothStatusCodes.FEATURE_SUPPORTED); - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); - LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class); - when(broadcast.isProfileReady()).thenReturn(false); - LocalBluetoothLeBroadcastAssistant assistant = - mock(LocalBluetoothLeBroadcastAssistant.class); - when(assistant.isProfileReady()).thenReturn(true); - LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class); - when(profileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast); - when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant); - when(mBtManager.getProfileManager()).thenReturn(profileManager); + setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */ + false, /* workProfile= */ false); mBluetoothEventManager.dispatchProfileConnectionStateChanged( mCachedBluetoothDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); - verify(broadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); + verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); } /** @@ -301,28 +263,31 @@ public class BluetoothEventManagerTest { */ @Test public void dispatchProfileConnectionStateChanged_notAssistantProfile_noUpdateFallbackDevice() { - ShadowBluetoothAdapter shadowBluetoothAdapter = - Shadow.extract(BluetoothAdapter.getDefaultAdapter()); - shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( - BluetoothStatusCodes.FEATURE_SUPPORTED); - shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( - BluetoothStatusCodes.FEATURE_SUPPORTED); - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); - LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class); - when(broadcast.isProfileReady()).thenReturn(true); - LocalBluetoothLeBroadcastAssistant assistant = - mock(LocalBluetoothLeBroadcastAssistant.class); - when(assistant.isProfileReady()).thenReturn(true); - LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class); - when(profileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast); - when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant); - when(mBtManager.getProfileManager()).thenReturn(profileManager); + setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */ + true, /* workProfile= */ false); mBluetoothEventManager.dispatchProfileConnectionStateChanged( mCachedBluetoothDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO); - verify(broadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); + verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); + } + + /** + * dispatchProfileConnectionStateChanged should not call {@link + * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when triggered for + * work profile. + */ + @Test + public void dispatchProfileConnectionStateChanged_workProfile_noUpdateFallbackDevice() { + setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */ + true, /* workProfile= */ true); + mBluetoothEventManager.dispatchProfileConnectionStateChanged( + mCachedBluetoothDevice, + BluetoothProfile.STATE_DISCONNECTED, + BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); + + verify(mBroadcast).updateFallbackActiveDeviceIfNeeded(); } /** @@ -332,28 +297,40 @@ public class BluetoothEventManagerTest { */ @Test public void dispatchProfileConnectionStateChanged_audioSharing_updateFallbackDevice() { + setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */ + true, /* workProfile= */ false); + mBluetoothEventManager.dispatchProfileConnectionStateChanged( + mCachedBluetoothDevice, + BluetoothProfile.STATE_DISCONNECTED, + BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); + + verify(mBroadcast).updateFallbackActiveDeviceIfNeeded(); + } + + private void setUpAudioSharing(boolean enableFlag, boolean enableFeature, + boolean enableProfile, boolean workProfile) { + if (enableFlag) { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + } else { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + } ShadowBluetoothAdapter shadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); - shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( - BluetoothStatusCodes.FEATURE_SUPPORTED); - shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( - BluetoothStatusCodes.FEATURE_SUPPORTED); - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); - LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class); - when(broadcast.isProfileReady()).thenReturn(true); + int code = enableFeature ? BluetoothStatusCodes.FEATURE_SUPPORTED + : BluetoothStatusCodes.FEATURE_NOT_SUPPORTED; + shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(code); + shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(code); + when(mBroadcast.isProfileReady()).thenReturn(enableProfile); LocalBluetoothLeBroadcastAssistant assistant = mock(LocalBluetoothLeBroadcastAssistant.class); - when(assistant.isProfileReady()).thenReturn(true); + when(assistant.isProfileReady()).thenReturn(enableProfile); LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class); - when(profileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast); + when(profileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast); when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant); when(mBtManager.getProfileManager()).thenReturn(profileManager); - mBluetoothEventManager.dispatchProfileConnectionStateChanged( - mCachedBluetoothDevice, - BluetoothProfile.STATE_DISCONNECTED, - BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); - - verify(broadcast).updateFallbackActiveDeviceIfNeeded(); + UserManager userManager = mock(UserManager.class); + when(mContext.getSystemService(UserManager.class)).thenReturn(userManager); + when(userManager.isManagedProfile()).thenReturn(workProfile); } @Test diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java index b180b69f1e07..fd14d1ff6786 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java @@ -39,6 +39,7 @@ import android.bluetooth.BluetoothStatusCodes; import android.content.Context; import android.os.Looper; import android.os.Parcel; +import android.os.UserManager; import android.platform.test.flag.junit.SetFlagsRule; import com.android.settingslib.flags.Flags; @@ -104,6 +105,8 @@ public class CsipDeviceManagerTest { private LocalBluetoothLeBroadcast mBroadcast; @Mock private LocalBluetoothLeBroadcastAssistant mAssistant; + @Mock + private UserManager mUserManager; private ShadowBluetoothAdapter mShadowBluetoothAdapter; private CachedBluetoothDevice mCachedDevice1; @@ -128,7 +131,7 @@ public class CsipDeviceManagerTest { public void setUp() { MockitoAnnotations.initMocks(this); - mContext = RuntimeEnvironment.application; + mContext = spy(RuntimeEnvironment.application); mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); mShadowBluetoothAdapter.setEnabled(true); mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( @@ -356,6 +359,37 @@ public class CsipDeviceManagerTest { } @Test + public void + addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_workProfile_doNothing() { + // Condition: The preferredDevice is main and there is another main device in top list + // Expected Result: return true and there is the preferredDevice in top list + CachedBluetoothDevice preferredDevice = mCachedDevice1; + mCachedDevice1.getMemberDevice().clear(); + mCachedDevices.clear(); + mCachedDevices.add(preferredDevice); + mCachedDevices.add(mCachedDevice2); + mCachedDevices.add(mCachedDevice3); + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mBroadcast.isEnabled(null)).thenReturn(true); + BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class); + when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata); + BluetoothLeBroadcastReceiveState state = Mockito.mock( + BluetoothLeBroadcastReceiveState.class); + when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L)); + when(mAssistant.getAllSources(mDevice2)).thenReturn(ImmutableList.of(state)); + when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager); + when(mUserManager.isManagedProfile()).thenReturn(true); + + assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) + .isTrue(); + assertThat(mCachedDevices.contains(preferredDevice)).isTrue(); + assertThat(mCachedDevices.contains(mCachedDevice2)).isFalse(); + assertThat(mCachedDevices.contains(mCachedDevice3)).isTrue(); + assertThat(preferredDevice.getMemberDevice()).contains(mCachedDevice2); + verify(mAssistant, never()).addSource(mDevice1, metadata, /* isGroupOp= */ false); + } + + @Test public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_syncSource() { // Condition: The preferredDevice is main and there is another main device in top list // Expected Result: return true and there is the preferredDevice in top list diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 456fedf912ff..408ed1e861c3 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -743,12 +743,6 @@ <uses-permission android:name="android.permission.READ_SAFETY_CENTER_STATUS" /> <uses-permission android:name="android.permission.MANAGE_SAFETY_CENTER" /> - <!-- Permissions required for CTS test - CtsVirtualDevicesTestCases --> - <uses-permission android:name="android.permission.CREATE_VIRTUAL_DEVICE" /> - <uses-permission android:name="android.permission.ADD_TRUSTED_DISPLAY" /> - <uses-permission android:name="android.permission.ADD_ALWAYS_UNLOCKED_DISPLAY" /> - - <!-- Permission required for CTS test - Notification test suite --> <uses-permission android:name="android.permission.REVOKE_POST_NOTIFICATIONS_WITHOUT_KILL" /> diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt index d9dcfdc7becb..9c6fd4bf71b4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt @@ -56,6 +56,7 @@ class DreamOverlayCallbackControllerTest : SysuiTestCase() { // Adding twice should not invoke twice reset(callback) + underTest.onStartDream() underTest.addCallback(callback) underTest.onWakeUp() verify(callback, times(1)).onWakeUp() @@ -68,6 +69,19 @@ class DreamOverlayCallbackControllerTest : SysuiTestCase() { } @Test + fun onWakeUp_multipleCalls() { + underTest.onStartDream() + assertThat(underTest.isDreaming).isEqualTo(true) + + underTest.addCallback(callback) + underTest.onWakeUp() + underTest.onWakeUp() + underTest.onWakeUp() + verify(callback, times(1)).onWakeUp() + assertThat(underTest.isDreaming).isEqualTo(false) + } + + @Test fun onStartDreamInvokesCallback() { underTest.addCallback(callback) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt index a3314e8900ce..f5d2d42902d8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt @@ -712,6 +712,9 @@ class DreamOverlayServiceTest : SysuiTestCase() { // Verify DreamOverlayContainerViewController is destroyed. verify(mDreamOverlayContainerViewController).destroy() + + // DreamOverlay callback receives onWakeUp. + verify(mDreamOverlayCallbackController).onWakeUp() } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt index c5ccf9e6a1d1..74d4178891b9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.policy.domain.interactor import android.app.AutomaticZenRule import android.app.Flags +import android.app.NotificationManager.INTERRUPTION_FILTER_NONE +import android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY import android.app.NotificationManager.Policy import android.platform.test.annotations.EnableFlags import android.provider.Settings @@ -25,6 +27,7 @@ import android.provider.Settings.Secure.ZEN_DURATION import android.provider.Settings.Secure.ZEN_DURATION_FOREVER import android.provider.Settings.Secure.ZEN_DURATION_PROMPT import android.service.notification.SystemZenRules +import android.service.notification.ZenPolicy import android.service.notification.ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -383,6 +386,120 @@ class ZenModeInteractorTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_MODES_UI) + fun activeModesBlockingEverything_hasModesWithFilterNone() = + testScope.runTest { + val blockingEverything by collectLastValue(underTest.activeModesBlockingEverything) + + zenModeRepository.addModes( + listOf( + TestModeBuilder() + .setName("Filter=None, Not active") + .setInterruptionFilter(INTERRUPTION_FILTER_NONE) + .setActive(false) + .build(), + TestModeBuilder() + .setName("Filter=Priority, Active") + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setActive(true) + .build(), + TestModeBuilder() + .setName("Filter=None, Active") + .setInterruptionFilter(INTERRUPTION_FILTER_NONE) + .setActive(true) + .build(), + TestModeBuilder() + .setName("Filter=None, Active Too") + .setInterruptionFilter(INTERRUPTION_FILTER_NONE) + .setActive(true) + .build(), + ) + ) + runCurrent() + + assertThat(blockingEverything!!.mainMode!!.name).isEqualTo("Filter=None, Active") + assertThat(blockingEverything!!.modeNames) + .containsExactly("Filter=None, Active", "Filter=None, Active Too") + .inOrder() + } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI) + fun activeModesBlockingMedia_hasModesWithPolicyBlockingMedia() = + testScope.runTest { + val blockingMedia by collectLastValue(underTest.activeModesBlockingMedia) + + zenModeRepository.addModes( + listOf( + TestModeBuilder() + .setName("Blocks media, Not active") + .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build()) + .setActive(false) + .build(), + TestModeBuilder() + .setName("Allows media, Active") + .setZenPolicy(ZenPolicy.Builder().allowMedia(true).build()) + .setActive(true) + .build(), + TestModeBuilder() + .setName("Blocks media, Active") + .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build()) + .setActive(true) + .build(), + TestModeBuilder() + .setName("Blocks media, Active Too") + .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build()) + .setActive(true) + .build(), + ) + ) + runCurrent() + + assertThat(blockingMedia!!.mainMode!!.name).isEqualTo("Blocks media, Active") + assertThat(blockingMedia!!.modeNames) + .containsExactly("Blocks media, Active", "Blocks media, Active Too") + .inOrder() + } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI) + fun activeModesBlockingAlarms_hasModesWithPolicyBlockingAlarms() = + testScope.runTest { + val blockingAlarms by collectLastValue(underTest.activeModesBlockingAlarms) + + zenModeRepository.addModes( + listOf( + TestModeBuilder() + .setName("Blocks alarms, Not active") + .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build()) + .setActive(false) + .build(), + TestModeBuilder() + .setName("Allows alarms, Active") + .setZenPolicy(ZenPolicy.Builder().allowAlarms(true).build()) + .setActive(true) + .build(), + TestModeBuilder() + .setName("Blocks alarms, Active") + .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build()) + .setActive(true) + .build(), + TestModeBuilder() + .setName("Blocks alarms, Active Too") + .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build()) + .setActive(true) + .build(), + ) + ) + runCurrent() + + assertThat(blockingAlarms!!.mainMode!!.name).isEqualTo("Blocks alarms, Active") + assertThat(blockingAlarms!!.modeNames) + .containsExactly("Blocks alarms, Active", "Blocks alarms, Active Too") + .inOrder() + } + + @Test @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API) fun modesHidingNotifications_onlyIncludesModesWithNotifListSuppression() = testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt new file mode 100644 index 000000000000..f80b36a10dc2 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel + +import android.app.Flags +import android.app.NotificationManager.INTERRUPTION_FILTER_NONE +import android.media.AudioManager +import android.platform.test.annotations.EnableFlags +import android.service.notification.ZenPolicy +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.logging.uiEventLogger +import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.settingslib.volume.shared.model.AudioStream +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository +import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor +import com.android.systemui.testKosmos +import com.android.systemui.volume.domain.interactor.audioVolumeInteractor +import com.android.systemui.volume.shared.volumePanelLogger +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class AudioStreamSliderViewModelTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val zenModeRepository = kosmos.fakeZenModeRepository + + private lateinit var mediaStream: AudioStreamSliderViewModel + private lateinit var alarmsStream: AudioStreamSliderViewModel + private lateinit var notificationStream: AudioStreamSliderViewModel + private lateinit var otherStream: AudioStreamSliderViewModel + + @Before + fun setUp() { + mediaStream = audioStreamSliderViewModel(AudioManager.STREAM_MUSIC) + alarmsStream = audioStreamSliderViewModel(AudioManager.STREAM_ALARM) + notificationStream = audioStreamSliderViewModel(AudioManager.STREAM_NOTIFICATION) + otherStream = audioStreamSliderViewModel(AudioManager.STREAM_VOICE_CALL) + } + + private fun audioStreamSliderViewModel(stream: Int): AudioStreamSliderViewModel { + return AudioStreamSliderViewModel( + AudioStreamSliderViewModel.FactoryAudioStreamWrapper(AudioStream(stream)), + testScope.backgroundScope, + context, + kosmos.audioVolumeInteractor, + kosmos.zenModeInteractor, + kosmos.uiEventLogger, + kosmos.volumePanelLogger, + ) + } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS) + fun slider_media_hasDisabledByModesText() = + testScope.runTest { + val mediaSlider by collectLastValue(mediaStream.slider) + + zenModeRepository.addMode( + TestModeBuilder() + .setName("Media is ok") + .setZenPolicy(ZenPolicy.Builder().allowAllSounds().build()) + .setActive(true) + .build() + ) + zenModeRepository.addMode( + TestModeBuilder() + .setName("No media plz") + .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build()) + .setActive(true) + .build() + ) + runCurrent() + + assertThat(mediaSlider!!.disabledMessage) + .isEqualTo("Unavailable because No media plz is on") + + zenModeRepository.clearModes() + runCurrent() + + assertThat(mediaSlider!!.disabledMessage).isEqualTo("Unavailable") + } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS) + fun slider_alarms_hasDisabledByModesText() = + testScope.runTest { + val alarmsSlider by collectLastValue(alarmsStream.slider) + + zenModeRepository.addMode( + TestModeBuilder() + .setName("Alarms are ok") + .setZenPolicy(ZenPolicy.Builder().allowAllSounds().build()) + .setActive(true) + .build() + ) + zenModeRepository.addMode( + TestModeBuilder() + .setName("Zzzzz") + .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build()) + .setActive(true) + .build() + ) + runCurrent() + + assertThat(alarmsSlider!!.disabledMessage).isEqualTo("Unavailable because Zzzzz is on") + + zenModeRepository.clearModes() + runCurrent() + + assertThat(alarmsSlider!!.disabledMessage).isEqualTo("Unavailable") + } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS) + fun slider_other_hasDisabledByModesText() = + testScope.runTest { + val otherSlider by collectLastValue(otherStream.slider) + + zenModeRepository.addMode( + TestModeBuilder() + .setName("Everything blocked") + .setInterruptionFilter(INTERRUPTION_FILTER_NONE) + .setActive(true) + .build() + ) + runCurrent() + + assertThat(otherSlider!!.disabledMessage) + .isEqualTo("Unavailable because Everything blocked is on") + + zenModeRepository.clearModes() + runCurrent() + + assertThat(otherSlider!!.disabledMessage).isEqualTo("Unavailable") + } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS) + fun slider_notification_hasSpecialDisabledText() = + testScope.runTest { + val notificationSlider by collectLastValue(notificationStream.slider) + runCurrent() + + assertThat(notificationSlider!!.disabledMessage) + .isEqualTo("Unavailable because ring is muted") + } +} diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 96a85d78e2b5..72250610d768 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1746,6 +1746,11 @@ <!-- A message shown when the media volume changing is disabled because of the don't disturb mode [CHAR_LIMIT=50]--> <string name="stream_media_unavailable">Unavailable because Do Not Disturb is on</string> + <!-- A message shown when a specific volume (e.g. Alarms, Media, etc) is disabled because an active mode is muting that audio stream altogether [CHAR_LIMIT=50]--> + <string name="stream_unavailable_by_modes">Unavailable because <xliff:g id="mode" example="Bedtime">%s</xliff:g> is on</string> + <!-- A message shown when a specific volume (e.g. Alarms, Media, etc) is disabled but we don't know which mode (or anything else) is responsible. [CHAR_LIMIT=50]--> + <string name="stream_unavailable_by_unknown">Unavailable</string> + <!-- Shown in the header of quick settings to indicate to the user that their phone ringer is on vibrate. [CHAR_LIMIT=NONE] --> <!-- Shown in the header of quick settings to indicate to the user that their phone ringer is on silent (muted). [CHAR_LIMIT=NONE] --> diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt index 373671d01395..0949ea4d7797 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.bouncer.domain.interactor +import android.util.Log import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository @@ -46,6 +47,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn /** Encapsulates business logic for interacting with the lock-screen alternate bouncer. */ @@ -137,6 +139,8 @@ constructor( flowOf(false) } } + .distinctUntilChanged() + .onEach { Log.d(TAG, "canShowAlternateBouncer changed to $it") } .stateIn( scope = scope, started = WhileSubscribed(), @@ -234,5 +238,7 @@ constructor( companion object { private const val MIN_VISIBILITY_DURATION_UNTIL_TOUCHES_DISMISS_ALTERNATE_BOUNCER_MS = 200L + + private const val TAG = "AlternateBouncerInteractor" } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayCallbackController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayCallbackController.kt index d5ff8f21abb2..2b617525fbcc 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayCallbackController.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayCallbackController.kt @@ -39,8 +39,10 @@ class DreamOverlayCallbackController @Inject constructor() : } fun onWakeUp() { - isDreaming = false - callbacks.forEach { it.onWakeUp() } + if (isDreaming) { + isDreaming = false + callbacks.forEach { it.onWakeUp() } + } } fun onStartDream() { diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index 83f86a718029..7a6ca0859a09 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -297,6 +297,8 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mStateController.setLowLightActive(false); mStateController.setEntryAnimationsFinished(false); + mDreamOverlayCallbackController.onWakeUp(); + if (mDreamOverlayContainerViewController != null) { mDreamOverlayContainerViewController.destroy(); mDreamOverlayContainerViewController = null; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java index 7b6a2cb62b14..560028cb5640 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java @@ -444,9 +444,11 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView if (onFinishedRunnable != null) { onFinishedRunnable.run(); } + if (mRunWithoutInterruptions) { + enableAppearDrawing(false); + } // We need to reset the View state, even if the animation was cancelled - enableAppearDrawing(false); onAppearAnimationFinished(isAppearing); if (mRunWithoutInterruptions) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 0474344ee390..7e5b45543e9e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -33,7 +33,6 @@ import static com.android.systemui.Flags.relockWithPowerButtonImmediately; import static com.android.systemui.Flags.statusBarSignalPolicyRefactor; import static com.android.systemui.charging.WirelessChargingAnimation.UNKNOWN_BATTERY_LEVEL; import static com.android.systemui.flags.Flags.SHORTCUT_LIST_SEARCH_LAYOUT; -import static com.android.systemui.statusbar.NotificationLockscreenUserManager.PERMISSION_SELF; import static com.android.systemui.statusbar.StatusBarState.SHADE; import android.annotation.Nullable; @@ -41,7 +40,6 @@ import android.app.ActivityOptions; import android.app.IWallpaperManager; import android.app.KeyguardManager; import android.app.Notification; -import android.app.NotificationManager; import android.app.PendingIntent; import android.app.StatusBarManager; import android.app.TaskInfo; @@ -275,11 +273,6 @@ import javax.inject.Provider; @SysUISingleton public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { - private static final String BANNER_ACTION_CANCEL = - "com.android.systemui.statusbar.banner_action_cancel"; - private static final String BANNER_ACTION_SETUP = - "com.android.systemui.statusbar.banner_action_setup"; - private static final int MSG_LAUNCH_TRANSITION_TIMEOUT = 1003; // 1020-1040 reserved for BaseStatusBar @@ -963,12 +956,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } } - IntentFilter internalFilter = new IntentFilter(); - internalFilter.addAction(BANNER_ACTION_CANCEL); - internalFilter.addAction(BANNER_ACTION_SETUP); - mContext.registerReceiver(mBannerActionBroadcastReceiver, internalFilter, PERMISSION_SELF, - null, Context.RECEIVER_EXPORTED_UNAUDITED); - if (mWallpaperSupported) { IWallpaperManager wallpaperManager = IWallpaperManager.Stub.asInterface( ServiceManager.getService(Context.WALLPAPER_SERVICE)); @@ -2948,29 +2935,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { return mDeviceInteractive; } - private final BroadcastReceiver mBannerActionBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (BANNER_ACTION_CANCEL.equals(action) || BANNER_ACTION_SETUP.equals(action)) { - NotificationManager noMan = (NotificationManager) - mContext.getSystemService(Context.NOTIFICATION_SERVICE); - noMan.cancel(com.android.internal.messages.nano.SystemMessageProto.SystemMessage. - NOTE_HIDDEN_NOTIFICATIONS); - - Settings.Secure.putInt(mContext.getContentResolver(), - Settings.Secure.SHOW_NOTE_ABOUT_NOTIFICATION_HIDING, 0); - if (BANNER_ACTION_SETUP.equals(action)) { - mShadeController.animateCollapseShadeForced(); - mContext.startActivity(new Intent(Settings.ACTION_APP_NOTIFICATION_REDACTION) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - - ); - } - } - } - }; - @Override public void handleExternalShadeWindowTouch(MotionEvent event) { getNotificationShadeWindowViewController().handleExternalTouch(event); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index 479ffb728eb2..17bd53869ee5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -530,6 +530,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb this::consumeKeyguardAuthenticatedBiometricsHandled ); } else { + // Collector that keeps the AlternateBouncerInteractor#canShowAlternateBouncer flow hot. mListenForCanShowAlternateBouncer = mJavaAdapter.alwaysCollectFlow( mAlternateBouncerInteractor.getCanShowAlternateBouncer(), this::consumeCanShowAlternateBouncer @@ -578,8 +579,17 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } private void consumeCanShowAlternateBouncer(boolean canShow) { - // do nothing, we only are registering for the flow to ensure that there's at least - // one subscriber that will update AlternateBouncerInteractor.canShowAlternateBouncer.value + // Hack: this is required to fix issues where + // KeyguardBouncerRepository#alternateBouncerVisible state is incorrectly set and then never + // reset. This is caused by usages of show()/forceShow() that only read this flow to set the + // alternate bouncer visible state, if there is a race condition between when that flow + // changes to false and when the read happens, the flow will be set to an incorrect value + // and not reset on time. + if (!canShow) { + Log.d(TAG, "canShowAlternateBouncer turned false, maybe try hiding the alternate " + + "bouncer if it is already visible"); + mAlternateBouncerInteractor.maybeHide(); + } } /** Register a callback, to be invoked by the Predictive Back system. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt index daba1099c49d..9839f9d76537 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt @@ -16,10 +16,12 @@ package com.android.systemui.statusbar.policy.domain.interactor +import android.app.NotificationManager.INTERRUPTION_FILTER_NONE import android.content.Context import android.provider.Settings import android.provider.Settings.Secure.ZEN_DURATION_FOREVER import android.provider.Settings.Secure.ZEN_DURATION_PROMPT +import android.service.notification.ZenPolicy.STATE_DISALLOW import android.service.notification.ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST import android.util.Log import androidx.concurrent.futures.await @@ -115,6 +117,26 @@ constructor( .flowOn(bgDispatcher) .distinctUntilChanged() + val activeModesBlockingEverything: Flow<ActiveZenModes> = getFilteredActiveModesFlow { mode -> + mode.interruptionFilter == INTERRUPTION_FILTER_NONE + } + + val activeModesBlockingMedia: Flow<ActiveZenModes> = getFilteredActiveModesFlow { mode -> + mode.policy.priorityCategoryMedia == STATE_DISALLOW + } + + val activeModesBlockingAlarms: Flow<ActiveZenModes> = getFilteredActiveModesFlow { mode -> + mode.policy.priorityCategoryAlarms == STATE_DISALLOW + } + + private fun getFilteredActiveModesFlow(predicate: (ZenMode) -> Boolean): Flow<ActiveZenModes> { + return modes + .map { modes -> modes.filter { mode -> predicate(mode) } } + .map { modes -> buildActiveZenModes(modes) } + .flowOn(bgDispatcher) + .distinctUntilChanged() + } + suspend fun getActiveModes() = buildActiveZenModes(zenModeRepository.getModes()) private suspend fun buildActiveZenModes(modes: List<ZenMode>): ActiveZenModes { diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt index ffb1f11c4970..2aa1ac99a400 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt @@ -18,6 +18,9 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel import android.content.Context import android.media.AudioManager +import android.media.AudioManager.STREAM_ALARM +import android.media.AudioManager.STREAM_MUSIC +import android.media.AudioManager.STREAM_NOTIFICATION import android.util.Log import com.android.internal.logging.UiEventLogger import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor @@ -25,7 +28,11 @@ import com.android.settingslib.volume.shared.model.AudioStream import com.android.settingslib.volume.shared.model.AudioStreamModel import com.android.settingslib.volume.shared.model.RingerMode import com.android.systemui.common.shared.model.Icon +import com.android.systemui.modes.shared.ModesUiIcons import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor +import com.android.systemui.statusbar.policy.domain.model.ActiveZenModes +import com.android.systemui.util.kotlin.combine import com.android.systemui.volume.panel.shared.VolumePanelLogger import com.android.systemui.volume.panel.ui.VolumePanelUiEvent import dagger.assisted.Assisted @@ -51,16 +58,14 @@ constructor( @Assisted private val coroutineScope: CoroutineScope, private val context: Context, private val audioVolumeInteractor: AudioVolumeInteractor, + private val zenModeInteractor: ZenModeInteractor, private val uiEventLogger: UiEventLogger, private val volumePanelLogger: VolumePanelLogger, ) : SliderViewModel { private val volumeChanges = MutableStateFlow<Int?>(null) private val streamsAffectedByRing = - setOf( - AudioManager.STREAM_RING, - AudioManager.STREAM_NOTIFICATION, - ) + setOf(AudioManager.STREAM_RING, AudioManager.STREAM_NOTIFICATION) private val audioStream = audioStreamWrapper.audioStream private val iconsByStream = mapOf( @@ -78,11 +83,6 @@ constructor( AudioStream(AudioManager.STREAM_NOTIFICATION) to R.string.stream_notification, AudioStream(AudioManager.STREAM_ALARM) to R.string.stream_alarm, ) - private val disabledTextByStream = - mapOf( - AudioStream(AudioManager.STREAM_NOTIFICATION) to - R.string.stream_notification_unavailable, - ) private val uiEventByStream = mapOf( AudioStream(AudioManager.STREAM_MUSIC) to @@ -98,15 +98,48 @@ constructor( ) override val slider: StateFlow<SliderState> = - combine( - audioVolumeInteractor.getAudioStream(audioStream), - audioVolumeInteractor.canChangeVolume(audioStream), - audioVolumeInteractor.ringerMode, - ) { model, isEnabled, ringerMode -> - volumePanelLogger.onVolumeUpdateReceived(audioStream, model.volume) - model.toState(isEnabled, ringerMode) - } - .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty) + if (ModesUiIcons.isEnabled) { + combine( + audioVolumeInteractor.getAudioStream(audioStream), + audioVolumeInteractor.canChangeVolume(audioStream), + audioVolumeInteractor.ringerMode, + zenModeInteractor.activeModesBlockingEverything, + zenModeInteractor.activeModesBlockingAlarms, + zenModeInteractor.activeModesBlockingMedia, + ) { + model, + isEnabled, + ringerMode, + modesBlockingEverything, + modesBlockingAlarms, + modesBlockingMedia -> + volumePanelLogger.onVolumeUpdateReceived(audioStream, model.volume) + model.toState( + isEnabled, + ringerMode, + getStreamDisabledMessage( + modesBlockingEverything, + modesBlockingAlarms, + modesBlockingMedia, + ), + ) + } + .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty) + } else { + combine( + audioVolumeInteractor.getAudioStream(audioStream), + audioVolumeInteractor.canChangeVolume(audioStream), + audioVolumeInteractor.ringerMode, + ) { model, isEnabled, ringerMode -> + volumePanelLogger.onVolumeUpdateReceived(audioStream, model.volume) + model.toState( + isEnabled, + ringerMode, + getStreamDisabledMessageWithoutModes(audioStream), + ) + } + .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty) + } init { volumeChanges @@ -139,6 +172,7 @@ constructor( private fun AudioStreamModel.toState( isEnabled: Boolean, ringerMode: RingerMode, + disabledMessage: String?, ): State { val label = labelsByStream[audioStream]?.let(context::getString) @@ -148,13 +182,7 @@ constructor( valueRange = volumeRange.first.toFloat()..volumeRange.last.toFloat(), icon = getIcon(ringerMode), label = label, - disabledMessage = - context.getString( - disabledTextByStream.getOrDefault( - audioStream, - R.string.stream_alarm_unavailable, - ) - ), + disabledMessage = disabledMessage, isEnabled = isEnabled, a11yStep = volumeRange.step, a11yClickDescription = @@ -191,6 +219,43 @@ constructor( ) } + private fun getStreamDisabledMessage( + blockingEverything: ActiveZenModes, + blockingAlarms: ActiveZenModes, + blockingMedia: ActiveZenModes, + ): String { + // TODO: b/372213356 - Figure out the correct messages for VOICE_CALL and RING. + // In fact, VOICE_CALL should not be affected by interruption filtering at all. + return if (audioStream.value == STREAM_NOTIFICATION) { + context.getString(R.string.stream_notification_unavailable) + } else { + val blockingModeName = + when { + blockingEverything.mainMode != null -> blockingEverything.mainMode.name + audioStream.value == STREAM_ALARM -> blockingAlarms.mainMode?.name + audioStream.value == STREAM_MUSIC -> blockingMedia.mainMode?.name + else -> null + } + + if (blockingModeName != null) { + context.getString(R.string.stream_unavailable_by_modes, blockingModeName) + } else { + // Should not actually be visible, but as a catch-all. + context.getString(R.string.stream_unavailable_by_unknown) + } + } + } + + private fun getStreamDisabledMessageWithoutModes(audioStream: AudioStream): String { + // TODO: b/372213356 - Figure out the correct messages for VOICE_CALL and RING. + // In fact, VOICE_CALL should not be affected by interruption filtering at all. + return if (audioStream.value == STREAM_NOTIFICATION) { + context.getString(R.string.stream_notification_unavailable) + } else { + context.getString(R.string.stream_alarm_unavailable) + } + } + private fun AudioStreamModel.getIcon(ringerMode: RingerMode): Icon { val iconRes = if (isAffectedByMute && isMuted) { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt index e6b52f0c52e0..55f0a28d0135 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel import android.content.applicationContext import com.android.internal.logging.uiEventLogger import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor import com.android.systemui.volume.domain.interactor.audioVolumeInteractor import com.android.systemui.volume.shared.volumePanelLogger import kotlinx.coroutines.CoroutineScope @@ -36,6 +37,7 @@ val Kosmos.audioStreamSliderViewModelFactory by coroutineScope, applicationContext, audioVolumeInteractor, + zenModeInteractor, uiEventLogger, volumePanelLogger, ) diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionDumpHelper.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionDumpHelper.java index 9fc413f6224b..a832545475bf 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionDumpHelper.java +++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionDumpHelper.java @@ -84,15 +84,17 @@ public final class AppFunctionDumpHelper { new FutureGlobalSearchSession(appSearchManager, Runnable::run)) { pw.println(); - FutureSearchResults futureSearchResults = - searchSession.search("", buildAppFunctionMetadataSearchSpec()).get(); - List<SearchResult> searchResultsList; - do { - searchResultsList = futureSearchResults.getNextPage().get(); - for (SearchResult searchResult : searchResultsList) { - dumpAppFunctionMetadata(pw, searchResult); - } - } while (!searchResultsList.isEmpty()); + try (FutureSearchResults futureSearchResults = + searchSession.search("", buildAppFunctionMetadataSearchSpec()).get(); ) { + List<SearchResult> searchResultsList; + do { + searchResultsList = futureSearchResults.getNextPage().get(); + for (SearchResult searchResult : searchResultsList) { + dumpAppFunctionMetadata(pw, searchResult); + } + } while (!searchResultsList.isEmpty()); + } + } catch (Exception e) { pw.println("Failed to dump AppFunction state: " + e); } diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java index 6d350e6b2a4a..c5fef191c52c 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java @@ -441,7 +441,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { targetUser, mServiceConfig.getExecuteAppFunctionCancellationTimeoutMillis(), cancellationSignal, - RunAppFunctionServiceCallback.create( + new RunAppFunctionServiceCallback( requestInternal, cancellationCallback, safeExecuteAppFunctionCallback), diff --git a/services/appfunctions/java/com/android/server/appfunctions/FutureSearchResults.java b/services/appfunctions/java/com/android/server/appfunctions/FutureSearchResults.java index 45cbdb4021e5..c38ff143178f 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/FutureSearchResults.java +++ b/services/appfunctions/java/com/android/server/appfunctions/FutureSearchResults.java @@ -24,11 +24,12 @@ import android.app.appsearch.SearchResults; import com.android.internal.infra.AndroidFuture; +import java.io.Closeable; import java.io.IOException; import java.util.List; /** A future API wrapper of {@link android.app.appsearch.SearchResults}. */ -public interface FutureSearchResults { +public interface FutureSearchResults extends Closeable { /** Converts a failed app search result codes into an exception. */ @NonNull @@ -52,4 +53,7 @@ public interface FutureSearchResults { * there are no more results. */ AndroidFuture<List<SearchResult>> getNextPage(); + + @Override + void close(); } diff --git a/services/appfunctions/java/com/android/server/appfunctions/FutureSearchResultsImpl.java b/services/appfunctions/java/com/android/server/appfunctions/FutureSearchResultsImpl.java index c3be342043ba..c8bc538f7226 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/FutureSearchResultsImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/FutureSearchResultsImpl.java @@ -54,4 +54,9 @@ public class FutureSearchResultsImpl implements FutureSearchResults { } }); } + + @Override + public void close() { + mSearchResults.close(); + } } diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java index bbf6c0beb163..96be76975e9d 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java +++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java @@ -420,26 +420,29 @@ public class MetadataSyncAdapter { Objects.requireNonNull(propertyPackageName); ArrayMap<String, ArraySet<String>> packageToFunctionIds = new ArrayMap<>(); - FutureSearchResults futureSearchResults = + try (FutureSearchResults futureSearchResults = searchSession .search( "", buildMetadataSearchSpec( schemaType, propertyFunctionId, propertyPackageName)) - .get(); - List<SearchResult> searchResultsList = futureSearchResults.getNextPage().get(); - // TODO(b/357551503): This could be expensive if we have more functions - while (!searchResultsList.isEmpty()) { - for (SearchResult searchResult : searchResultsList) { - String packageName = - searchResult.getGenericDocument().getPropertyString(propertyPackageName); - String functionId = - searchResult.getGenericDocument().getPropertyString(propertyFunctionId); - packageToFunctionIds - .computeIfAbsent(packageName, k -> new ArraySet<>()) - .add(functionId); + .get(); ) { + List<SearchResult> searchResultsList = futureSearchResults.getNextPage().get(); + // TODO(b/357551503): This could be expensive if we have more functions + while (!searchResultsList.isEmpty()) { + for (SearchResult searchResult : searchResultsList) { + String packageName = + searchResult + .getGenericDocument() + .getPropertyString(propertyPackageName); + String functionId = + searchResult.getGenericDocument().getPropertyString(propertyFunctionId); + packageToFunctionIds + .computeIfAbsent(packageName, k -> new ArraySet<>()) + .add(functionId); + } + searchResultsList = futureSearchResults.getNextPage().get(); } - searchResultsList = futureSearchResults.getNextPage().get(); } return packageToFunctionIds; } diff --git a/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java b/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java index 7820390dd544..129be65f3153 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java +++ b/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java @@ -27,17 +27,17 @@ import android.util.Slog; import com.android.server.appfunctions.RemoteServiceCaller.RunServiceCallCallback; import com.android.server.appfunctions.RemoteServiceCaller.ServiceUsageCompleteListener; - /** * A callback to forward a request to the {@link IAppFunctionService} and report back the result. */ public class RunAppFunctionServiceCallback implements RunServiceCallCallback<IAppFunctionService> { + private static final String TAG = RunAppFunctionServiceCallback.class.getSimpleName(); private final ExecuteAppFunctionAidlRequest mRequestInternal; private final SafeOneTimeExecuteAppFunctionCallback mSafeExecuteAppFunctionCallback; private final ICancellationCallback mCancellationCallback; - private RunAppFunctionServiceCallback( + public RunAppFunctionServiceCallback( ExecuteAppFunctionAidlRequest requestInternal, ICancellationCallback cancellationCallback, SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback) { @@ -46,21 +46,6 @@ public class RunAppFunctionServiceCallback implements RunServiceCallCallback<IAp this.mCancellationCallback = cancellationCallback; } - /** - * Creates a new instance of {@link RunAppFunctionServiceCallback}. - * - * @param requestInternal a request to send to the service. - * @param cancellationCallback a callback to forward cancellation signal to the service. - * @param safeExecuteAppFunctionCallback a callback to report back the result of the operation. - */ - public static RunAppFunctionServiceCallback create( - ExecuteAppFunctionAidlRequest requestInternal, - ICancellationCallback cancellationCallback, - SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback) { - return new RunAppFunctionServiceCallback( - requestInternal, cancellationCallback, safeExecuteAppFunctionCallback); - } - @Override public void onServiceConnected( @NonNull IAppFunctionService service, @@ -68,6 +53,7 @@ public class RunAppFunctionServiceCallback implements RunServiceCallCallback<IAp try { service.executeAppFunction( mRequestInternal.getClientRequest(), + mRequestInternal.getCallingPackage(), mCancellationCallback, new IExecuteAppFunctionCallback.Stub() { @Override @@ -88,7 +74,7 @@ public class RunAppFunctionServiceCallback implements RunServiceCallCallback<IAp @Override public void onFailedToConnect() { - Slog.e("AppFunctionManagerServiceImpl", "Failed to connect to service"); + Slog.e(TAG, "Failed to connect to service"); mSafeExecuteAppFunctionCallback.onResult( ExecuteAppFunctionResponse.newFailure( ExecuteAppFunctionResponse.RESULT_APP_UNKNOWN_ERROR, diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index 8d96ba93530b..c4e10360a7cb 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -150,7 +150,7 @@ import javax.xml.datatype.DatatypeConfigurationException; * <screenBrightnessDefault>0.65</screenBrightnessDefault> * <powerThrottlingConfig> * <brightnessLowestCapAllowed>0.1</brightnessLowestCapAllowed> - * <customAnimationRateSec>0.004</customAnimationRateSec> + * <customAnimationRate>0.004</customAnimationRate> * <pollingWindowMaxMillis>30000</pollingWindowMaxMillis> * <pollingWindowMinMillis>10000</pollingWindowMinMillis> * <powerThrottlingMap> @@ -2193,11 +2193,11 @@ public class DisplayDeviceConfig { return; } float lowestBrightnessCap = powerThrottlingCfg.getBrightnessLowestCapAllowed().floatValue(); - float customAnimationRateSec = powerThrottlingCfg.getCustomAnimationRateSec().floatValue(); + float customAnimationRate = powerThrottlingCfg.getCustomAnimationRate().floatValue(); int pollingWindowMaxMillis = powerThrottlingCfg.getPollingWindowMaxMillis().intValue(); int pollingWindowMinMillis = powerThrottlingCfg.getPollingWindowMinMillis().intValue(); mPowerThrottlingConfigData = new PowerThrottlingConfigData(lowestBrightnessCap, - customAnimationRateSec, + customAnimationRate, pollingWindowMaxMillis, pollingWindowMinMillis); } @@ -3012,16 +3012,16 @@ public class DisplayDeviceConfig { /** Lowest brightness cap allowed for this device. */ public final float brightnessLowestCapAllowed; /** Time take to animate brightness in seconds. */ - public final float customAnimationRateSec; + public final float customAnimationRate; /** Time window for maximum polling power in milliseconds. */ public final int pollingWindowMaxMillis; /** Time window for minimum polling power in milliseconds. */ public final int pollingWindowMinMillis; public PowerThrottlingConfigData(float brightnessLowestCapAllowed, - float customAnimationRateSec, int pollingWindowMaxMillis, + float customAnimationRate, int pollingWindowMaxMillis, int pollingWindowMinMillis) { this.brightnessLowestCapAllowed = brightnessLowestCapAllowed; - this.customAnimationRateSec = customAnimationRateSec; + this.customAnimationRate = customAnimationRate; this.pollingWindowMaxMillis = pollingWindowMaxMillis; this.pollingWindowMinMillis = pollingWindowMinMillis; } @@ -3031,7 +3031,7 @@ public class DisplayDeviceConfig { return "PowerThrottlingConfigData{" + "brightnessLowestCapAllowed: " + brightnessLowestCapAllowed - + ", customAnimationRateSec: " + customAnimationRateSec + + ", customAnimationRate: " + customAnimationRate + ", pollingWindowMaxMillis: " + pollingWindowMaxMillis + ", pollingWindowMinMillis: " + pollingWindowMinMillis + "} "; diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessPowerClamper.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessPowerClamper.java index 85e81f989845..1a18b004a3e2 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessPowerClamper.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessPowerClamper.java @@ -85,7 +85,7 @@ class BrightnessPowerClamper extends private String mDataId = null; private float mCurrentBrightness = PowerManager.BRIGHTNESS_INVALID; private float mCustomAnimationRateSec = DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET; - private float mCustomAnimationRateSecDeviceConfig = + private float mCustomAnimationRateDeviceConfig = DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET; private final BiFunction<String, String, ThrottlingLevel> mDataPointMapper = (key, value) -> { try { @@ -117,7 +117,7 @@ class BrightnessPowerClamper extends }; mPowerThrottlingConfigData = powerData.getPowerThrottlingConfigData(); if (mPowerThrottlingConfigData != null) { - mCustomAnimationRateSecDeviceConfig = mPowerThrottlingConfigData.customAnimationRateSec; + mCustomAnimationRateDeviceConfig = mPowerThrottlingConfigData.customAnimationRate; } mThermalLevelListener = new ThermalLevelListener(handler); mPmicMonitor = @@ -228,10 +228,6 @@ class BrightnessPowerClamper extends } mPowerThrottlingConfigData = data.getPowerThrottlingConfigData(); - if (mPowerThrottlingConfigData == null) { - Slog.d(TAG, - "Power throttling data is missing for configuration data."); - } } private void recalculateBrightnessCap() { @@ -282,13 +278,13 @@ class BrightnessPowerClamper extends mIsActive = isActive; Slog.i(TAG, "Power clamper changing current brightness cap mBrightnessCap: " + mBrightnessCap + " to target brightness cap:" + targetBrightnessCap - + " for current screen brightness: " + mCurrentBrightness); - mBrightnessCap = targetBrightnessCap; - Slog.i(TAG, "Power clamper changed state: thermalStatus:" + mCurrentThermalLevel + + " for current screen brightness: " + mCurrentBrightness + "\n" + + "Power clamper changed state: thermalStatus:" + mCurrentThermalLevel + " mCurrentThermalLevelChanged:" + mCurrentThermalLevelChanged + " mCurrentAvgPowerConsumed:" + mCurrentAvgPowerConsumed - + " mCustomAnimationRateSec:" + mCustomAnimationRateSecDeviceConfig); - mCustomAnimationRateSec = mCustomAnimationRateSecDeviceConfig; + + " mCustomAnimationRateSec:" + mCustomAnimationRateDeviceConfig); + mBrightnessCap = targetBrightnessCap; + mCustomAnimationRateSec = mCustomAnimationRateDeviceConfig; mChangeListener.onChanged(); } else { mCustomAnimationRateSec = DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET; @@ -344,7 +340,7 @@ class BrightnessPowerClamper extends + mPowerThrottlingConfigData.pollingWindowMinMillis + " msec."); return; } - mCustomAnimationRateSecDeviceConfig = mPowerThrottlingConfigData.customAnimationRateSec; + mCustomAnimationRateDeviceConfig = mPowerThrottlingConfigData.customAnimationRate; mThermalLevelListener.start(); } diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 4665a72b0b06..b228bb9a89c3 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -5144,10 +5144,28 @@ public class ComputerEngine implements Computer { } updateOwnerPackageName = installSource.mUpdateOwnerPackageName; + + if (DEBUG_INSTALL) { + Log.d(TAG, "ComputerEngine getInstallSourceInfo updateOwnerPackageName = " + + updateOwnerPackageName + ", callingUid = " + callingUid + ", packageName = " + + packageName + ", userId = " + userId); + } + if (updateOwnerPackageName != null) { final PackageStateInternal ps = mSettings.getPackage(updateOwnerPackageName); final boolean isCallerSystemOrUpdateOwner = callingUid == Process.SYSTEM_UID || isCallerSameApp(updateOwnerPackageName, callingUid); + + if (DEBUG_INSTALL) { + Log.d(TAG, "ComputerEngine getInstallSourceInfo ps = " + + ps + ", isCallerSystemOrUpdateOwner =" + isCallerSystemOrUpdateOwner + + ", isCallerSameApp = " + + isCallerSameApp(updateOwnerPackageName, callingUid) + ", filter = " + + shouldFilterApplicationIncludingUninstalled(ps, callingUid, userId) + + ", FromManagedUserOrProfile = " + + isCallerFromManagedUserOrProfile(userId)); + } + // Except for package visibility filtering, we also hide update owner if the installer // is in the managed user or profile. As we don't enforce the update ownership for the // managed user and profile, knowing there's an update owner is meaningless in that @@ -5159,6 +5177,11 @@ public class ComputerEngine implements Computer { } } + if (DEBUG_INSTALL) { + Log.d(TAG, "ComputerEngine getInstallSourceInfo updateOwnerPackageName = " + + updateOwnerPackageName); + } + if (installSource.mIsInitiatingPackageUninstalled) { // We can't check visibility in the usual way, since the initiating package is no // longer present. So we apply simpler rules to whether to expose the info: diff --git a/services/core/java/com/android/server/stats/bootstrap/StatsBootstrapAtomService.java b/services/core/java/com/android/server/stats/bootstrap/StatsBootstrapAtomService.java index 0d420a535415..dcb47a7b60b6 100644 --- a/services/core/java/com/android/server/stats/bootstrap/StatsBootstrapAtomService.java +++ b/services/core/java/com/android/server/stats/bootstrap/StatsBootstrapAtomService.java @@ -62,6 +62,9 @@ public class StatsBootstrapAtomService extends IStatsBootstrapAtomService.Stub { case StatsBootstrapAtomValue.bytesValue: builder.writeByteArray(value.getBytesValue()); break; + case StatsBootstrapAtomValue.stringArrayValue: + builder.writeStringArray(value.getStringArrayValue()); + break; default: Slog.e(TAG, "Unexpected value type " + value.getTag() + " when logging atom " + atom.atomId); diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java index 35ec5adf54b0..0580d4a5a4a3 100644 --- a/services/core/java/com/android/server/wm/ActivityStartController.java +++ b/services/core/java/com/android/server/wm/ActivityStartController.java @@ -43,7 +43,6 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Binder; -import android.os.Bundle; import android.os.IBinder; import android.os.Trace; import android.os.UserHandle; @@ -550,14 +549,14 @@ public class ActivityStartController { * Starts an activity in the TaskFragment. * @param taskFragment TaskFragment {@link TaskFragment} to start the activity in. * @param activityIntent intent to start the activity. - * @param activityOptions ActivityOptions to start the activity with. + * @param activityOptions SafeActivityOptions to start the activity with. * @param resultTo the caller activity * @param callingUid the caller uid * @param callingPid the caller pid * @return the start result. */ int startActivityInTaskFragment(@NonNull TaskFragment taskFragment, - @NonNull Intent activityIntent, @Nullable Bundle activityOptions, + @NonNull Intent activityIntent, @Nullable SafeActivityOptions activityOptions, @Nullable IBinder resultTo, int callingUid, int callingPid, @Nullable IBinder errorCallbackToken) { final ActivityRecord caller = diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 2229807f5db1..82c7a9350eca 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -1523,8 +1523,10 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub final IBinder callerActivityToken = operation.getActivityToken(); final Intent activityIntent = operation.getActivityIntent(); final Bundle activityOptions = operation.getBundle(); + final SafeActivityOptions safeOptions = + SafeActivityOptions.fromBundle(activityOptions, caller.mPid, caller.mUid); final int result = waitAsyncStart(() -> mService.getActivityStartController() - .startActivityInTaskFragment(taskFragment, activityIntent, activityOptions, + .startActivityInTaskFragment(taskFragment, activityIntent, safeOptions, callerActivityToken, caller.mUid, caller.mPid, errorCallbackToken)); if (!isStartResultSuccessful(result)) { diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd index 776de2e52061..20c69ac93f63 100644 --- a/services/core/xsd/display-device-config/display-device-config.xsd +++ b/services/core/xsd/display-device-config/display-device-config.xsd @@ -464,7 +464,7 @@ <xs:annotation name="nonnull"/> <xs:annotation name="final"/> </xs:element> - <xs:element name="customAnimationRateSec" type="nonNegativeDecimal" minOccurs="0" maxOccurs="1"> + <xs:element name="customAnimationRate" type="nonNegativeDecimal" minOccurs="0" maxOccurs="1"> <xs:annotation name="nonnull"/> <xs:annotation name="final"/> </xs:element> diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt index 110a5a20da6a..a8f18b3d2eee 100644 --- a/services/core/xsd/display-device-config/schema/current.txt +++ b/services/core/xsd/display-device-config/schema/current.txt @@ -345,12 +345,12 @@ package com.android.server.display.config { public class PowerThrottlingConfig { ctor public PowerThrottlingConfig(); method @NonNull public final java.math.BigDecimal getBrightnessLowestCapAllowed(); - method @NonNull public final java.math.BigDecimal getCustomAnimationRateSec(); + method @NonNull public final java.math.BigDecimal getCustomAnimationRate(); method @NonNull public final java.math.BigInteger getPollingWindowMaxMillis(); method @NonNull public final java.math.BigInteger getPollingWindowMinMillis(); method public final java.util.List<com.android.server.display.config.PowerThrottlingMap> getPowerThrottlingMap(); method public final void setBrightnessLowestCapAllowed(@NonNull java.math.BigDecimal); - method public final void setCustomAnimationRateSec(@NonNull java.math.BigDecimal); + method public final void setCustomAnimationRate(@NonNull java.math.BigDecimal); method public final void setPollingWindowMaxMillis(@NonNull java.math.BigInteger); method public final void setPollingWindowMinMillis(@NonNull java.math.BigInteger); } diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt index 5758da858423..96fb4535b992 100644 --- a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt +++ b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt @@ -391,6 +391,10 @@ class MetadataSyncAdapterTest { return AndroidFuture.completedFuture(mutableListOf()) } } + + override fun close() { + Log.d("FakeRuntimeMetadataSearchSession", "Closing session") + } } return AndroidFuture.completedFuture(futureSearchResults) } diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java index 3976ea4fc86e..2220f439f6c8 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java @@ -265,7 +265,7 @@ public final class DisplayDeviceConfigTest { mDisplayDeviceConfig.getPowerThrottlingConfigData(); assertNotNull(powerThrottlingConfigData); assertEquals(0.1f, powerThrottlingConfigData.brightnessLowestCapAllowed, SMALL_DELTA); - assertEquals(15f, powerThrottlingConfigData.customAnimationRateSec, SMALL_DELTA); + assertEquals(15f, powerThrottlingConfigData.customAnimationRate, SMALL_DELTA); assertEquals(20000, powerThrottlingConfigData.pollingWindowMaxMillis); assertEquals(10000, powerThrottlingConfigData.pollingWindowMinMillis); } @@ -1299,7 +1299,7 @@ public final class DisplayDeviceConfigTest { private String getPowerThrottlingConfig() { return "<powerThrottlingConfig >\n" + "<brightnessLowestCapAllowed>0.1</brightnessLowestCapAllowed>\n" - + "<customAnimationRateSec>15</customAnimationRateSec>\n" + + "<customAnimationRate>15</customAnimationRate>\n" + "<pollingWindowMaxMillis>20000</pollingWindowMaxMillis>\n" + "<pollingWindowMinMillis>10000</pollingWindowMinMillis>\n" + "<powerThrottlingMap>\n" diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml index 2724149d859f..c645c0852f1b 100644 --- a/services/tests/servicestests/AndroidManifest.xml +++ b/services/tests/servicestests/AndroidManifest.xml @@ -113,6 +113,7 @@ <uses-permission android:name="android.permission.MANAGE_ROLE_HOLDERS" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.CAMERA" /> + <uses-permission android:name="android.permission.CREATE_VIRTUAL_DEVICE" /> <queries> <package android:name="com.android.servicestests.apps.suspendtestapp" /> diff --git a/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java index c970a3e34d12..840e5c58078b 100644 --- a/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java +++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java @@ -65,7 +65,6 @@ public class AppOpsActiveWatcherTest { VirtualDeviceRule.withAdditionalPermissions( Manifest.permission.GRANT_RUNTIME_PERMISSIONS, Manifest.permission.REVOKE_RUNTIME_PERMISSIONS, - Manifest.permission.CREATE_VIRTUAL_DEVICE, Manifest.permission.GET_APP_OPS_STATS ); private static final long NOTIFICATION_TIMEOUT_MILLIS = 5000; diff --git a/services/tests/servicestests/src/com/android/server/appop/AppOpsDeviceAwareServiceTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsDeviceAwareServiceTest.java index 7f2327aa4f24..e3eca6d5fd83 100644 --- a/services/tests/servicestests/src/com/android/server/appop/AppOpsDeviceAwareServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsDeviceAwareServiceTest.java @@ -58,7 +58,6 @@ public class AppOpsDeviceAwareServiceTest { VirtualDeviceRule.withAdditionalPermissions( Manifest.permission.GRANT_RUNTIME_PERMISSIONS, Manifest.permission.REVOKE_RUNTIME_PERMISSIONS, - Manifest.permission.CREATE_VIRTUAL_DEVICE, Manifest.permission.GET_APP_OPS_STATS); private static final String ATTRIBUTION_TAG_1 = "attributionTag1"; diff --git a/services/tests/servicestests/src/com/android/server/appop/AppOpsNotedWatcherTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsNotedWatcherTest.java index 1abd4eb6157f..b0846f62628c 100644 --- a/services/tests/servicestests/src/com/android/server/appop/AppOpsNotedWatcherTest.java +++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsNotedWatcherTest.java @@ -22,16 +22,14 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; -import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; import android.app.AppOpsManager; import android.app.AppOpsManager.OnOpNotedListener; import android.companion.virtual.VirtualDeviceManager; -import android.companion.virtual.VirtualDeviceParams; import android.content.AttributionSource; import android.content.Context; import android.os.Process; -import android.virtualdevice.cts.common.FakeAssociationRule; +import android.virtualdevice.cts.common.VirtualDeviceRule; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; @@ -42,8 +40,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InOrder; -import java.util.concurrent.atomic.AtomicInteger; - /** * Tests watching noted ops. */ @@ -51,7 +47,7 @@ import java.util.concurrent.atomic.AtomicInteger; @RunWith(AndroidJUnit4.class) public class AppOpsNotedWatcherTest { @Rule - public FakeAssociationRule mFakeAssociationRule = new FakeAssociationRule(); + public VirtualDeviceRule mVirtualDeviceRule = VirtualDeviceRule.createDefault(); private static final long NOTIFICATION_TIMEOUT_MILLIS = 5000; @Test @@ -119,19 +115,12 @@ public class AppOpsNotedWatcherTest { public void testWatchNotedOpsForExternalDevice() { final AppOpsManager.OnOpNotedListener listener = mock( AppOpsManager.OnOpNotedListener.class); - final VirtualDeviceManager virtualDeviceManager = getContext().getSystemService( - VirtualDeviceManager.class); - AtomicInteger virtualDeviceId = new AtomicInteger(); - runWithShellPermissionIdentity(() -> { - final VirtualDeviceManager.VirtualDevice virtualDevice = - virtualDeviceManager.createVirtualDevice( - mFakeAssociationRule.getAssociationInfo().getId(), - new VirtualDeviceParams.Builder().setName("virtual_device").build()); - virtualDeviceId.set(virtualDevice.getDeviceId()); - }); + final VirtualDeviceManager.VirtualDevice virtualDevice = + mVirtualDeviceRule.createManagedVirtualDevice(); + final int virtualDeviceId = virtualDevice.getDeviceId(); AttributionSource attributionSource = new AttributionSource(Process.myUid(), getContext().getOpPackageName(), getContext().getAttributionTag(), - virtualDeviceId.get()); + virtualDeviceId); final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class); appOpsManager.startWatchingNoted(new int[]{AppOpsManager.OP_FINE_LOCATION, @@ -142,7 +131,7 @@ public class AppOpsNotedWatcherTest { verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS) .times(1)).onOpNoted(eq(AppOpsManager.OPSTR_FINE_LOCATION), eq(Process.myUid()), eq(getContext().getOpPackageName()), - eq(getContext().getAttributionTag()), eq(virtualDeviceId.get()), + eq(getContext().getAttributionTag()), eq(virtualDeviceId), eq(AppOpsManager.OP_FLAG_SELF), eq(AppOpsManager.MODE_ALLOWED)); appOpsManager.finishOp(getContext().getAttributionSource().getToken(), diff --git a/services/tests/servicestests/src/com/android/server/appop/AppOpsStartedWatcherTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsStartedWatcherTest.java index 8a6ba4d484f7..d46fb90f40d6 100644 --- a/services/tests/servicestests/src/com/android/server/appop/AppOpsStartedWatcherTest.java +++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsStartedWatcherTest.java @@ -16,8 +16,6 @@ package com.android.server.appop; -import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; - import static org.mockito.Mockito.eq; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -28,11 +26,10 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import android.app.AppOpsManager; import android.app.AppOpsManager.OnOpStartedListener; import android.companion.virtual.VirtualDeviceManager; -import android.companion.virtual.VirtualDeviceParams; import android.content.AttributionSource; import android.content.Context; import android.os.Process; -import android.virtualdevice.cts.common.FakeAssociationRule; +import android.virtualdevice.cts.common.VirtualDeviceRule; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; @@ -43,15 +40,13 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InOrder; -import java.util.concurrent.atomic.AtomicInteger; - /** Tests watching started ops. */ @SmallTest @RunWith(AndroidJUnit4.class) public class AppOpsStartedWatcherTest { @Rule - public FakeAssociationRule mFakeAssociationRule = new FakeAssociationRule(); + public VirtualDeviceRule mVirtualDeviceRule = VirtualDeviceRule.createDefault(); private static final long NOTIFICATION_TIMEOUT_MILLIS = 5000; @Test @@ -124,20 +119,13 @@ public class AppOpsStartedWatcherTest { @Test public void testWatchStartedOpsForExternalDevice() { - final VirtualDeviceManager virtualDeviceManager = getContext().getSystemService( - VirtualDeviceManager.class); - AtomicInteger virtualDeviceId = new AtomicInteger(); - runWithShellPermissionIdentity(() -> { - final VirtualDeviceManager.VirtualDevice virtualDevice = - virtualDeviceManager.createVirtualDevice( - mFakeAssociationRule.getAssociationInfo().getId(), - new VirtualDeviceParams.Builder().setName("virtual_device").build()); - virtualDeviceId.set(virtualDevice.getDeviceId()); - }); + final VirtualDeviceManager.VirtualDevice virtualDevice = + mVirtualDeviceRule.createManagedVirtualDevice(); + final int virtualDeviceId = virtualDevice.getDeviceId(); final OnOpStartedListener listener = mock(OnOpStartedListener.class); AttributionSource attributionSource = new AttributionSource(Process.myUid(), getContext().getOpPackageName(), getContext().getAttributionTag(), - virtualDeviceId.get()); + virtualDeviceId); final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class); appOpsManager.startWatchingStarted(new int[]{AppOpsManager.OP_FINE_LOCATION, @@ -150,7 +138,7 @@ public class AppOpsStartedWatcherTest { verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS) .times(1)).onOpStarted(eq(AppOpsManager.OP_FINE_LOCATION), eq(Process.myUid()), eq(getContext().getOpPackageName()), - eq(getContext().getAttributionTag()), eq(virtualDeviceId.get()), + eq(getContext().getAttributionTag()), eq(virtualDeviceId), eq(AppOpsManager.OP_FLAG_SELF), eq(AppOpsManager.MODE_ALLOWED), eq(OnOpStartedListener.START_TYPE_STARTED), eq(AppOpsManager.ATTRIBUTION_FLAGS_NONE), diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index 62f5edce4e36..dad45b3048b9 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -50,7 +50,6 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.testng.Assert.assertThrows; -import android.Manifest; import android.app.WindowConfiguration; import android.app.admin.DevicePolicyManager; import android.companion.AssociationInfo; @@ -113,10 +112,11 @@ import android.view.Display; import android.view.DisplayInfo; import android.view.KeyEvent; import android.view.WindowManager; +import android.virtualdevice.cts.common.VirtualDeviceRule; import androidx.test.platform.app.InstrumentationRegistry; -import com.android.compatibility.common.util.AdoptShellPermissionsRule; +import com.android.compatibility.common.util.SystemUtil; import com.android.internal.app.BlockedAppStreamingActivity; import com.android.internal.os.BackgroundThread; import com.android.server.LocalServices; @@ -224,9 +224,7 @@ public class VirtualDeviceManagerServiceTest { public SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Rule - public AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule( - InstrumentationRegistry.getInstrumentation().getUiAutomation(), - Manifest.permission.CREATE_VIRTUAL_DEVICE); + public VirtualDeviceRule mVirtualDeviceRule = VirtualDeviceRule.createDefault(); private Context mContext; private InputManagerMockHelper mInputManagerMockHelper; @@ -1069,64 +1067,65 @@ public class VirtualDeviceManagerServiceTest { @Test public void createVirtualDpad_noPermission_failsSecurityException() { addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); - try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) { - assertThrows(SecurityException.class, - () -> mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER)); - } + // Shell doesn't have CREATE_VIRTUAL_DEVICE permission. + SystemUtil.runWithShellPermissionIdentity(() -> + assertThrows(SecurityException.class, + () -> mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER))); } @Test public void createVirtualKeyboard_noPermission_failsSecurityException() { addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); - try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) { - assertThrows(SecurityException.class, - () -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER)); - } + // Shell doesn't have CREATE_VIRTUAL_DEVICE permission. + SystemUtil.runWithShellPermissionIdentity(() -> + assertThrows(SecurityException.class, + () -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER))); } @Test public void createVirtualMouse_noPermission_failsSecurityException() { addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); - try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) { - assertThrows(SecurityException.class, - () -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER)); - } + // Shell doesn't have CREATE_VIRTUAL_DEVICE permission. + SystemUtil.runWithShellPermissionIdentity(() -> + assertThrows(SecurityException.class, + () -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER))); } @Test public void createVirtualTouchscreen_noPermission_failsSecurityException() { addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); - try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) { - assertThrows(SecurityException.class, - () -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER)); - } + // Shell doesn't have CREATE_VIRTUAL_DEVICE permission. + SystemUtil.runWithShellPermissionIdentity(() -> + assertThrows(SecurityException.class, + () -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER))); } @Test public void createVirtualNavigationTouchpad_noPermission_failsSecurityException() { addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); - try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) { - assertThrows(SecurityException.class, - () -> mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG, - BINDER)); - } + // Shell doesn't have CREATE_VIRTUAL_DEVICE permission. + SystemUtil.runWithShellPermissionIdentity(() -> + assertThrows(SecurityException.class, + () -> mDeviceImpl.createVirtualNavigationTouchpad( + NAVIGATION_TOUCHPAD_CONFIG, + BINDER))); } @Test public void onAudioSessionStarting_noPermission_failsSecurityException() { addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); - try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) { - assertThrows(SecurityException.class, - () -> mDeviceImpl.onAudioSessionStarting( - DISPLAY_ID_1, mRoutingCallback, mConfigChangedCallback)); - } + // Shell doesn't have CREATE_VIRTUAL_DEVICE permission. + SystemUtil.runWithShellPermissionIdentity(() -> + assertThrows(SecurityException.class, + () -> mDeviceImpl.onAudioSessionStarting( + DISPLAY_ID_1, mRoutingCallback, mConfigChangedCallback))); } @Test public void onAudioSessionEnded_noPermission_failsSecurityException() { - try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) { - assertThrows(SecurityException.class, () -> mDeviceImpl.onAudioSessionEnded()); - } + // Shell doesn't have CREATE_VIRTUAL_DEVICE permission. + SystemUtil.runWithShellPermissionIdentity(() -> + assertThrows(SecurityException.class, () -> mDeviceImpl.onAudioSessionEnded())); } @Test @@ -2002,18 +2001,4 @@ public class VirtualDeviceManagerServiceTest { /* timeApprovedMs= */0, /* lastTimeConnectedMs= */0, /* systemDataSyncFlags= */ -1, /* deviceIcon= */ null); } - - /** Helper class to drop permissions temporarily and restore them at the end of a test. */ - static final class DropShellPermissionsTemporarily implements AutoCloseable { - DropShellPermissionsTemporarily() { - InstrumentationRegistry.getInstrumentation().getUiAutomation() - .dropShellPermissionIdentity(); - } - - @Override - public void close() { - InstrumentationRegistry.getInstrumentation().getUiAutomation() - .adoptShellPermissionIdentity(); - } - } } diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java index 425bb158f997..7e22d74c64e1 100644 --- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java @@ -1256,7 +1256,8 @@ public class MediaProjectionManagerServiceTest { Manifest.permission.BYPASS_ROLE_QUALIFICATION); roleManager.setBypassingRoleQualification(true); - roleManager.addRoleHolderAsUser(role, packageName, /* flags = */ 0, user, + roleManager.addRoleHolderAsUser(role, packageName, + /* flags= */ RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP, user, mContext.getMainExecutor(), success -> { if (success) { latch.countDown(); @@ -1271,9 +1272,9 @@ public class MediaProjectionManagerServiceTest { } catch (InterruptedException e) { throw new RuntimeException(e); } finally { - roleManager.removeRoleHolderAsUser(role, packageName, 0, user, - mContext.getMainExecutor(), (aBool) -> { - }); + roleManager.removeRoleHolderAsUser(role, packageName, + /* flags= */ RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP, user, + mContext.getMainExecutor(), (aBool) -> {}); roleManager.setBypassingRoleQualification(false); instrumentation.getUiAutomation() .dropShellPermissionIdentity(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java index f56825faab73..42e31de295d6 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -16,6 +16,7 @@ package com.android.server.wm; +import static android.Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS; import static android.app.ActivityManager.START_CANCELED; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; @@ -28,6 +29,8 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; +import static android.content.pm.PackageManager.PERMISSION_DENIED; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED; import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED; import static android.view.InsetsSource.FLAG_FORCE_CONSUMING; @@ -37,6 +40,7 @@ import static android.window.DisplayAreaOrganizer.FEATURE_VENDOR_FIRST; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.never; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; @@ -61,6 +65,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.quality.Strictness.LENIENT; import android.annotation.NonNull; import android.app.ActivityManager; @@ -69,6 +74,7 @@ import android.app.ActivityOptions; import android.app.ActivityTaskManager.RootTaskInfo; import android.app.IRequestFinishCallback; import android.app.PictureInPictureParams; +import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ParceledListSlice; import android.content.res.Configuration; @@ -87,6 +93,7 @@ import android.view.WindowInsets; import android.window.ITaskFragmentOrganizer; import android.window.ITaskOrganizer; import android.window.IWindowContainerTransactionCallback; +import android.window.RemoteTransition; import android.window.StartingWindowInfo; import android.window.StartingWindowRemovalInfo; import android.window.TaskAppearedInfo; @@ -102,6 +109,7 @@ import com.android.window.flags.Flags; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.MockitoSession; import java.util.ArrayList; import java.util.HashSet; @@ -638,6 +646,66 @@ public class WindowOrganizerTests extends WindowTestsBase { } @Test + public void testStartActivityInTaskFragment_checkCallerPermission() { + final ActivityStartController activityStartController = + mWm.mAtmService.getActivityStartController(); + spyOn(activityStartController); + final ArgumentCaptor<SafeActivityOptions> activityOptionsCaptor = + ArgumentCaptor.forClass(SafeActivityOptions.class); + + final int uid = Binder.getCallingUid(); + final Task rootTask = new TaskBuilder(mSupervisor).setCreateActivity(true) + .setWindowingMode(WINDOWING_MODE_FULLSCREEN).build(); + final WindowContainerTransaction t = new WindowContainerTransaction(); + final TaskFragmentOrganizer organizer = + createTaskFragmentOrganizer(t, true /* isSystemOrganizer */); + final IBinder token = new Binder(); + final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(rootTask) + .setFragmentToken(token) + .setOrganizer(organizer) + .createActivityCount(1) + .build(); + mWm.mAtmService.mWindowOrganizerController.mLaunchTaskFragments.put(token, taskFragment); + final ActivityRecord ownerActivity = taskFragment.getTopMostActivity(); + + // Start Activity in TaskFragment with remote transition. + final RemoteTransition transition = mock(RemoteTransition.class); + final ActivityOptions options = ActivityOptions.makeRemoteTransition(transition); + final Intent intent = new Intent(); + t.startActivityInTaskFragment(token, ownerActivity.token, intent, options.toBundle()); + mWm.mAtmService.mWindowOrganizerController.applyTaskFragmentTransactionLocked( + t, TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN, + false /* shouldApplyIndependently */, null /* remoteTransition */); + + // Get the ActivityOptions. + verify(activityStartController).startActivityInTaskFragment( + eq(taskFragment), eq(intent), activityOptionsCaptor.capture(), + eq(ownerActivity.token), eq(uid), anyInt(), any()); + final SafeActivityOptions safeActivityOptions = activityOptionsCaptor.getValue(); + + final MockitoSession session = + mockitoSession().strictness(LENIENT).spyStatic(ActivityTaskManagerService.class) + .startMocking(); + try { + // Without the CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS permission, start activity with + // remote transition is not allowed. + doReturn(PERMISSION_DENIED).when(() -> ActivityTaskManagerService.checkPermission( + eq(CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS), anyInt(), eq(uid))); + assertThrows(SecurityException.class, + () -> safeActivityOptions.getOptions(mWm.mAtmService.mTaskSupervisor)); + + // With the CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS permission, start activity with + // remote transition is allowed. + doReturn(PERMISSION_GRANTED).when(() -> ActivityTaskManagerService.checkPermission( + eq(CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS), anyInt(), eq(uid))); + safeActivityOptions.getOptions(mWm.mAtmService.mTaskSupervisor); + } finally { + session.finishMocking(); + } + } + + @Test public void testTaskFragmentChangeHidden_throwsWhenNotSystemOrganizer() { // Non-system organizers are not allow to update the hidden state. testTaskFragmentChangesWithoutSystemOrganizerThrowException( diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt index 634b6eedd7e6..8d27c1d1dfd1 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt @@ -33,9 +33,9 @@ class LetterboxAppHelper @JvmOverloads constructor( instr: Instrumentation, - launcherName: String = ActivityOptions.NonResizeablePortraitActivity.LABEL, + launcherName: String = ActivityOptions.NonResizeableFixedAspectRatioPortraitActivity.LABEL, component: ComponentNameMatcher = - ActivityOptions.NonResizeablePortraitActivity.COMPONENT.toFlickerComponent() + ActivityOptions.NonResizeableFixedAspectRatioPortraitActivity.COMPONENT.toFlickerComponent() ) : StandardAppHelper(instr, launcherName, component) { private val gestureHelper: GestureHelper = GestureHelper(instrumentation) diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml index f891606f0066..f2e34257ef01 100644 --- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml +++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml @@ -115,6 +115,19 @@ <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> + <activity android:name=".NonResizeableFixedAspectRatioPortraitActivity" + android:theme="@style/CutoutNever" + android:resizeableActivity="false" + android:screenOrientation="portrait" + android:minAspectRatio="1.77" + android:taskAffinity="com.android.server.wm.flicker.testapp.NonResizeableFixedAspectRatioPortraitActivity" + android:label="NonResizeableFixedAspectRatioPortraitActivity" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> <activity android:name=".StartMediaProjectionActivity" android:theme="@style/CutoutNever" android:resizeableActivity="false" @@ -143,6 +156,7 @@ <activity android:name=".LaunchTransparentActivity" android:resizeableActivity="false" android:screenOrientation="portrait" + android:minAspectRatio="1.77" android:theme="@style/OptOutEdgeToEdge" android:taskAffinity="com.android.server.wm.flicker.testapp.LaunchTransparentActivity" android:label="LaunchTransparentActivity" diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java index e4de2c574553..73625da9dfa5 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java @@ -85,6 +85,12 @@ public class ActivityOptions { FLICKER_APP_PACKAGE + ".NonResizeablePortraitActivity"); } + public static class NonResizeableFixedAspectRatioPortraitActivity { + public static final String LABEL = "NonResizeableFixedAspectRatioPortraitActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".NonResizeableFixedAspectRatioPortraitActivity"); + } + public static class StartMediaProjectionActivity { public static final String LABEL = "StartMediaProjectionActivity"; public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/NonResizeableFixedAspectRatioPortraitActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/NonResizeableFixedAspectRatioPortraitActivity.java new file mode 100644 index 000000000000..be38c259d00d --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/NonResizeableFixedAspectRatioPortraitActivity.java @@ -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 com.android.server.wm.flicker.testapp; + +import android.app.Activity; +import android.os.Bundle; + +public class NonResizeableFixedAspectRatioPortraitActivity extends Activity { + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + setContentView(R.layout.activity_non_resizeable); + } +} |