Merge "Add null check on device posture change" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index fbe4905..d582cb7 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -24,6 +24,7 @@
"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 @@
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 65e4f27..a131ea7 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -4950,7 +4950,7 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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/api/system-current.txt b/core/api/system-current.txt
index 8edfc21..7781f88 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -18559,6 +18559,7 @@
method @Deprecated public abstract void setUserAgent(int);
method public abstract void setVideoOverlayForEmbeddedEncryptedVideoEnabled(boolean);
field public static final long ENABLE_SIMPLIFIED_DARK_MODE = 214741472L; // 0xcccb1e0L
+ field @FlaggedApi("android.webkit.user_agent_reduction") public static final long ENABLE_USER_AGENT_REDUCTION = 371034303L; // 0x161d88bfL
}
public class WebStorage {
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index 6ab39b0..832c88a 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -120,7 +120,7 @@
/**
* 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 @@
/**
* 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 @@
* <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 7a68a65..ceca850 100644
--- a/core/java/android/app/appfunctions/AppFunctionService.java
+++ b/core/java/android/app/appfunctions/AppFunctionService.java
@@ -29,11 +29,9 @@
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 @@
*/
void perform(
@NonNull ExecuteAppFunctionRequest request,
+ @NonNull String callingPackage,
@NonNull CancellationSignal cancellationSignal,
@NonNull Consumer<ExecuteAppFunctionResponse> callback);
}
@@ -92,6 +91,7 @@
@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 @@
try {
onExecuteFunction.perform(
request,
+ callingPackage,
buildCancellationSignal(cancellationCallback),
safeCallback::onResult);
} catch (Exception ex) {
@@ -128,12 +129,11 @@
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 @@
return mBinder;
}
-
/**
* Called by the system to execute a specific app function.
*
@@ -161,7 +160,6 @@
*
* @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 @@
* @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 291f33c..bf935d2 100644
--- a/core/java/android/app/appfunctions/IAppFunctionService.aidl
+++ b/core/java/android/app/appfunctions/IAppFunctionService.aidl
@@ -34,11 +34,13 @@
* 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 508077e..1af2437 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 @@
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 @@
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 0000000..bf0aeac
--- /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/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java
index a69a371..acb48f3 100644
--- a/core/java/android/hardware/camera2/CameraMetadata.java
+++ b/core/java/android/hardware/camera2/CameraMetadata.java
@@ -2270,7 +2270,17 @@
* {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration} are ignored. The
* application has control over the various
* android.flash.* fields.</p>
+ * <p>If the device supports manual flash strength control, i.e.,
+ * if {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel} and
+ * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel} are greater than 1, then
+ * the auto-exposure (AE) precapture metering sequence should be
+ * triggered for the configured flash mode and strength to avoid
+ * the image being incorrectly exposed at different
+ * {@link CaptureRequest#FLASH_STRENGTH_LEVEL android.flash.strengthLevel}.</p>
*
+ * @see CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL
+ * @see CaptureRequest#FLASH_STRENGTH_LEVEL
+ * @see CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL
* @see CaptureRequest#SENSOR_EXPOSURE_TIME
* @see CaptureRequest#SENSOR_FRAME_DURATION
* @see CaptureRequest#SENSOR_SENSITIVITY
diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java
index 3f5ae91..a193ee1 100644
--- a/core/java/android/hardware/camera2/CaptureRequest.java
+++ b/core/java/android/hardware/camera2/CaptureRequest.java
@@ -1358,6 +1358,13 @@
* camera device auto-exposure routine for the overridden
* fields for a given capture will be available in its
* CaptureResult.</p>
+ * <p>When {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is AE_MODE_ON and if the device
+ * supports manual flash strength control, i.e.,
+ * if {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel} and
+ * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel} are greater than 1, then
+ * the auto-exposure (AE) precapture metering sequence should be
+ * triggered to avoid the image being incorrectly exposed at
+ * different {@link CaptureRequest#FLASH_STRENGTH_LEVEL android.flash.strengthLevel}.</p>
* <p><b>Possible values:</b></p>
* <ul>
* <li>{@link #CONTROL_AE_MODE_OFF OFF}</li>
@@ -1373,9 +1380,13 @@
* <p>This key is available on all devices.</p>
*
* @see CameraCharacteristics#CONTROL_AE_AVAILABLE_MODES
+ * @see CaptureRequest#CONTROL_AE_MODE
* @see CaptureRequest#CONTROL_MODE
* @see CameraCharacteristics#FLASH_INFO_AVAILABLE
* @see CaptureRequest#FLASH_MODE
+ * @see CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL
+ * @see CaptureRequest#FLASH_STRENGTH_LEVEL
+ * @see CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL
* @see CaptureRequest#SENSOR_EXPOSURE_TIME
* @see CaptureRequest#SENSOR_FRAME_DURATION
* @see CaptureRequest#SENSOR_SENSITIVITY
diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java
index a18a634..e5ca46a 100644
--- a/core/java/android/hardware/camera2/CaptureResult.java
+++ b/core/java/android/hardware/camera2/CaptureResult.java
@@ -759,6 +759,13 @@
* camera device auto-exposure routine for the overridden
* fields for a given capture will be available in its
* CaptureResult.</p>
+ * <p>When {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is AE_MODE_ON and if the device
+ * supports manual flash strength control, i.e.,
+ * if {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel} and
+ * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel} are greater than 1, then
+ * the auto-exposure (AE) precapture metering sequence should be
+ * triggered to avoid the image being incorrectly exposed at
+ * different {@link CaptureRequest#FLASH_STRENGTH_LEVEL android.flash.strengthLevel}.</p>
* <p><b>Possible values:</b></p>
* <ul>
* <li>{@link #CONTROL_AE_MODE_OFF OFF}</li>
@@ -774,9 +781,13 @@
* <p>This key is available on all devices.</p>
*
* @see CameraCharacteristics#CONTROL_AE_AVAILABLE_MODES
+ * @see CaptureRequest#CONTROL_AE_MODE
* @see CaptureRequest#CONTROL_MODE
* @see CameraCharacteristics#FLASH_INFO_AVAILABLE
* @see CaptureRequest#FLASH_MODE
+ * @see CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL
+ * @see CaptureRequest#FLASH_STRENGTH_LEVEL
+ * @see CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL
* @see CaptureRequest#SENSOR_EXPOSURE_TIME
* @see CaptureRequest#SENSOR_FRAME_DURATION
* @see CaptureRequest#SENSOR_SENSITIVITY
diff --git a/core/java/android/os/OWNERS b/core/java/android/os/OWNERS
index 7d3076d..a1b75034 100644
--- a/core/java/android/os/OWNERS
+++ b/core/java/android/os/OWNERS
@@ -115,3 +115,10 @@
# 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 a90dfa4..b59bc06 100644
--- a/core/java/android/os/StatsBootstrapAtomValue.aidl
+++ b/core/java/android/os/StatsBootstrapAtomValue.aidl
@@ -26,4 +26,5 @@
float floatValue;
String stringValue;
byte[] bytesValue;
+ String[] stringArrayValue;
}
\ No newline at end of file
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index feeb339..271970b 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -255,3 +255,14 @@
description: "This fixed read-only flag is used to enable replacing permission BODY_SENSORS (and BODY_SENSORS_BACKGROUND) with granular health permission READ_HEART_RATE (and READ_HEALTH_DATA_IN_BACKGROUND)"
bug: "364638912"
}
+
+flag {
+ name: "delay_uid_state_changes_from_capability_updates"
+ is_fixed_read_only: true
+ namespace: "permissions"
+ description: "If proc state is decreasing over the restriction threshold and capability is changed, delay if no new capabilities are added"
+ bug: "308573169"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java
index a43906f..dfac244 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 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 fe6aafb..c5ca059 100644
--- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
@@ -860,6 +860,37 @@
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 @@
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 @@
}
/**
- * 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 @@
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 @@
if (isBitSet(nonDefaultFields, fieldIndex++)) {
parcel.writeLong(mLeashedParentNodeId);
}
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ parcel.writeInt(mChecked);
+ }
if (DEBUG) {
fieldIndex--;
@@ -4771,6 +4885,7 @@
mLeashedChild = other.mLeashedChild;
mLeashedParent = other.mLeashedParent;
mLeashedParentNodeId = other.mLeashedParentNodeId;
+ mChecked = other.mChecked;
}
private void initCopyInfos(AccessibilityNodeInfo other) {
@@ -4960,6 +5075,9 @@
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 69cbb9b..8ffae84 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 {
+ 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 @@
metadata {
purpose: PURPOSE_BUGFIX
}
-}
+ }
diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java
index 7366b9a..ab5969b 100644
--- a/core/java/android/webkit/WebSettings.java
+++ b/core/java/android/webkit/WebSettings.java
@@ -16,10 +16,12 @@
package android.webkit;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
import android.compat.annotation.EnabledSince;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
@@ -151,6 +153,24 @@
public static final long ENABLE_SIMPLIFIED_DARK_MODE = 214741472L;
/**
+ * Enable User-Agent Reduction for webview.
+ * The OS, CPU, and Build information in the default User-Agent will be
+ * reduced to the static "Linux; Android 10; K" string.
+ * Minor/build/patch version information in the default User-Agent is
+ * reduced to "0.0.0". The rest of the default User-Agent remains unchanged.
+ *
+ * See https://developers.google.com/privacy-sandbox/protections/user-agent
+ * for details related to User-Agent Reduction.
+ *
+ * @hide
+ */
+ @ChangeId
+ @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @FlaggedApi(android.webkit.Flags.FLAG_USER_AGENT_REDUCTION)
+ @SystemApi
+ public static final long ENABLE_USER_AGENT_REDUCTION = 371034303L;
+
+ /**
* Default cache usage mode. If the navigation type doesn't impose any
* specific behavior, use cached resources when they are available
* and not expired, otherwise load resources from the network.
diff --git a/core/java/android/webkit/flags.aconfig b/core/java/android/webkit/flags.aconfig
index b21a490..d1013a9 100644
--- a/core/java/android/webkit/flags.aconfig
+++ b/core/java/android/webkit/flags.aconfig
@@ -18,3 +18,11 @@
bug: "310653407"
is_fixed_read_only: true
}
+
+flag {
+ name: "user_agent_reduction"
+ is_exported: true
+ namespace: "webview"
+ description: "New feature reduce user-agent for webview"
+ bug: "371034303"
+}
diff --git a/core/java/android/window/OWNERS b/core/java/android/window/OWNERS
index 2c61df9..77c99b9 100644
--- a/core/java/android/window/OWNERS
+++ b/core/java/android/window/OWNERS
@@ -1,3 +1,5 @@
set noparent
include /services/core/java/com/android/server/wm/OWNERS
+
+per-file DesktopModeFlags.java = file:/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS
diff --git a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
index 39aadfb..8faaf95 100644
--- a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
+++ b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
@@ -53,19 +53,18 @@
* @hide
*/
public class AconfigFlags {
+ private static final boolean DEBUG = false;
private static final String LOG_TAG = "AconfigFlags";
-
- public enum Permission {
- READ_WRITE,
- READ_ONLY
- }
+ private static final String OVERRIDE_PREFIX = "device_config_overrides/";
+ private static final String STAGED_PREFIX = "staged/";
private final ArrayMap<String, Boolean> mFlagValues = new ArrayMap<>();
- private final ArrayMap<String, Permission> mFlagPermissions = new ArrayMap<>();
public AconfigFlags() {
if (!Flags.manifestFlagging()) {
- Slog.v(LOG_TAG, "Feature disabled, skipped all loading");
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "Feature disabled, skipped all loading");
+ }
return;
}
final var defaultFlagProtoFiles =
@@ -130,19 +129,17 @@
if (!"false".equalsIgnoreCase(value) && !"true".equalsIgnoreCase(value)) {
continue;
}
- final var overridePrefix = "device_config_overrides/";
- final var stagedPrefix = "staged/";
String separator = "/";
String prefix = "default";
int priority = 0;
- if (name.startsWith(overridePrefix)) {
- prefix = overridePrefix;
- name = name.substring(overridePrefix.length());
+ if (name.startsWith(OVERRIDE_PREFIX)) {
+ prefix = OVERRIDE_PREFIX;
+ name = name.substring(OVERRIDE_PREFIX.length());
separator = ":";
priority = 20;
- } else if (name.startsWith(stagedPrefix)) {
- prefix = stagedPrefix;
- name = name.substring(stagedPrefix.length());
+ } else if (name.startsWith(STAGED_PREFIX)) {
+ prefix = STAGED_PREFIX;
+ name = name.substring(STAGED_PREFIX.length());
separator = "*";
priority = 10;
}
@@ -155,12 +152,19 @@
if (!mFlagValues.containsKey(flagPackageAndName)) {
continue;
}
- Slog.d(LOG_TAG, "Found " + prefix
- + " Aconfig flag value for " + flagPackageAndName + " = " + value);
+ if (DEBUG) {
+ Slog.d(LOG_TAG, "Found " + prefix
+ + " Aconfig flag value in settings for " + flagPackageAndName
+ + " = " + value);
+ }
final Integer currentPriority = flagPriority.get(flagPackageAndName);
if (currentPriority != null && currentPriority >= priority) {
- Slog.i(LOG_TAG, "Skipping " + prefix + " flag " + flagPackageAndName
- + " because of the existing one with priority " + currentPriority);
+ if (DEBUG) {
+ Slog.d(LOG_TAG, "Skipping " + prefix + " flag "
+ + flagPackageAndName
+ + " in settings because of existing one with priority "
+ + currentPriority);
+ }
continue;
}
flagPriority.put(flagPackageAndName, priority);
@@ -185,15 +189,7 @@
for (parsed_flag flag : parsedFlags.parsedFlag) {
String flagPackageAndName = flag.package_ + "." + flag.name;
boolean flagValue = (flag.state == Aconfig.ENABLED);
- Slog.v(LOG_TAG, "Read Aconfig default flag value "
- + flagPackageAndName + " = " + flagValue);
mFlagValues.put(flagPackageAndName, flagValue);
-
- Permission permission = flag.permission == Aconfig.READ_ONLY
- ? Permission.READ_ONLY
- : Permission.READ_WRITE;
-
- mFlagPermissions.put(flagPackageAndName, permission);
}
}
@@ -203,24 +199,15 @@
* @return the current value of the given Aconfig flag, or null if there is no such flag
*/
@Nullable
- public Boolean getFlagValue(@NonNull String flagPackageAndName) {
+ private Boolean getFlagValue(@NonNull String flagPackageAndName) {
Boolean value = mFlagValues.get(flagPackageAndName);
- Slog.d(LOG_TAG, "Aconfig flag value for " + flagPackageAndName + " = " + value);
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "Aconfig flag value for " + flagPackageAndName + " = " + value);
+ }
return value;
}
/**
- * Get the flag permission, or null if the flag doesn't exist.
- * @param flagPackageAndName Full flag name formatted as 'package.flag'
- * @return the current permission of the given Aconfig flag, or null if there is no such flag
- */
- @Nullable
- public Permission getFlagPermission(@NonNull String flagPackageAndName) {
- Permission permission = mFlagPermissions.get(flagPackageAndName);
- return permission;
- }
-
- /**
* Check if the element in {@code parser} should be skipped because of the feature flag.
* @param parser XML parser object currently parsing an element
* @return true if the element is disabled because of its feature flag
@@ -247,7 +234,7 @@
}
// Skip if flag==false && attr=="flag" OR flag==true && attr=="!flag" (negated)
if (flagValue == negated) {
- Slog.v(LOG_TAG, "Skipping element " + parser.getName()
+ Slog.i(LOG_TAG, "Skipping element " + parser.getName()
+ " behind feature flag " + featureFlag + " = " + flagValue);
return true;
}
diff --git a/core/java/com/android/internal/widget/NotificationProgressBar.java b/core/java/com/android/internal/widget/NotificationProgressBar.java
new file mode 100644
index 0000000..12e1dd9
--- /dev/null
+++ b/core/java/com/android/internal/widget/NotificationProgressBar.java
@@ -0,0 +1,222 @@
+/*
+ * 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.internal.widget;
+
+import android.app.Notification.ProgressStyle;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ProgressBar;
+import android.widget.RemoteViews;
+
+import androidx.annotation.ColorInt;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.widget.NotificationProgressDrawable.Part;
+import com.android.internal.widget.NotificationProgressDrawable.Point;
+import com.android.internal.widget.NotificationProgressDrawable.Segment;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * NotificationProgressBar extends the capabilities of ProgressBar by adding functionalities to
+ * represent Notification ProgressStyle progress, such as for ridesharing and navigation.
+ */
+@RemoteViews.RemoteView
+public class NotificationProgressBar extends ProgressBar {
+ public NotificationProgressBar(Context context) {
+ this(context, null);
+ }
+
+ public NotificationProgressBar(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.progressBarStyle);
+ }
+
+ public NotificationProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public NotificationProgressBar(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ /**
+ * Processes the ProgressStyle data and convert to list of {@code
+ * NotificationProgressDrawable.Part}.
+ */
+ @VisibleForTesting
+ public static List<Part> processAndConvertToDrawableParts(
+ List<ProgressStyle.Segment> segments,
+ List<ProgressStyle.Point> points,
+ int progress,
+ boolean isStyledByProgress
+ ) {
+ if (segments.isEmpty()) {
+ throw new IllegalArgumentException("List of segments shouldn't be empty");
+ }
+
+ for (ProgressStyle.Segment segment : segments) {
+ final int length = segment.getLength();
+ if (length <= 0) {
+ throw new IllegalArgumentException("Invalid segment length : " + length);
+ }
+ }
+
+ final int progressMax = segments.stream().mapToInt(ProgressStyle.Segment::getLength).sum();
+
+ if (progress < 0 || progress > progressMax) {
+ throw new IllegalArgumentException("Invalid progress : " + progress);
+ }
+ for (ProgressStyle.Point point : points) {
+ final int pos = point.getPosition();
+ if (pos <= 0 || pos >= progressMax) {
+ throw new IllegalArgumentException("Invalid Point position : " + pos);
+ }
+ }
+
+ final Map<Integer, ProgressStyle.Segment> startToSegmentMap = generateStartToSegmentMap(
+ segments);
+ final Map<Integer, ProgressStyle.Point> positionToPointMap = generatePositionToPointMap(
+ points);
+ final SortedSet<Integer> sortedPos = generateSortedPositionSet(startToSegmentMap,
+ positionToPointMap, progress, isStyledByProgress);
+
+ final Map<Integer, ProgressStyle.Segment> startToSplitSegmentMap =
+ splitSegmentsByPointsAndProgress(
+ startToSegmentMap, sortedPos, progressMax);
+
+ return convertToDrawableParts(startToSplitSegmentMap, positionToPointMap, sortedPos,
+ progress, progressMax,
+ isStyledByProgress);
+ }
+
+
+ // Any segment with a point on it gets split by the point.
+ // If isStyledByProgress is true, also split the segment with the progress value in its range.
+ private static Map<Integer, ProgressStyle.Segment> splitSegmentsByPointsAndProgress(
+ Map<Integer, ProgressStyle.Segment> startToSegmentMap,
+ SortedSet<Integer> sortedPos,
+ int progressMax) {
+ int prevSegStart = 0;
+ for (Integer pos : sortedPos) {
+ if (pos == 0 || pos == progressMax) continue;
+ if (startToSegmentMap.containsKey(pos)) {
+ prevSegStart = pos;
+ continue;
+ }
+
+ final ProgressStyle.Segment prevSeg = startToSegmentMap.get(prevSegStart);
+ final ProgressStyle.Segment leftSeg = new ProgressStyle.Segment(
+ pos - prevSegStart).setColor(
+ prevSeg.getColor());
+ final ProgressStyle.Segment rightSeg = new ProgressStyle.Segment(
+ prevSegStart + prevSeg.getLength() - pos).setColor(prevSeg.getColor());
+
+ startToSegmentMap.put(prevSegStart, leftSeg);
+ startToSegmentMap.put(pos, rightSeg);
+
+ prevSegStart = pos;
+ }
+
+ return startToSegmentMap;
+ }
+
+ private static List<Part> convertToDrawableParts(
+ Map<Integer, ProgressStyle.Segment> startToSegmentMap,
+ Map<Integer, ProgressStyle.Point> positionToPointMap,
+ SortedSet<Integer> sortedPos,
+ int progress,
+ int progressMax,
+ boolean isStyledByProgress
+ ) {
+ List<Part> parts = new ArrayList<>();
+ boolean styleRemainingParts = false;
+ for (Integer pos : sortedPos) {
+ if (positionToPointMap.containsKey(pos)) {
+ final ProgressStyle.Point point = positionToPointMap.get(pos);
+ final int color = maybeGetFadedColor(point.getColor(), styleRemainingParts);
+ parts.add(new Point(null, color, styleRemainingParts));
+ }
+ // We want the Point at the current progress to be filled (not faded), but a Segment
+ // starting at this progress to be faded.
+ if (isStyledByProgress && !styleRemainingParts && pos == progress) {
+ styleRemainingParts = true;
+ }
+ if (startToSegmentMap.containsKey(pos)) {
+ final ProgressStyle.Segment seg = startToSegmentMap.get(pos);
+ final int color = maybeGetFadedColor(seg.getColor(), styleRemainingParts);
+ parts.add(new Segment(
+ (float) seg.getLength() / progressMax, color, styleRemainingParts));
+ }
+ }
+
+ return parts;
+ }
+
+ @ColorInt
+ private static int maybeGetFadedColor(@ColorInt int color, boolean fade) {
+ if (!fade) return color;
+
+ return NotificationProgressDrawable.getFadedColor(color);
+ }
+
+ private static Map<Integer, ProgressStyle.Segment> generateStartToSegmentMap(
+ List<ProgressStyle.Segment> segments) {
+ final Map<Integer, ProgressStyle.Segment> startToSegmentMap = new HashMap<>();
+
+ int currentStart = 0; // Initial start position is 0
+
+ for (ProgressStyle.Segment segment : segments) {
+ // Use the current start position as the key, and the segment as the value
+ startToSegmentMap.put(currentStart, segment);
+
+ // Update the start position for the next segment
+ currentStart += segment.getLength();
+ }
+
+ return startToSegmentMap;
+ }
+
+ private static Map<Integer, ProgressStyle.Point> generatePositionToPointMap(
+ List<ProgressStyle.Point> points) {
+ final Map<Integer, ProgressStyle.Point> positionToPointMap = new HashMap<>();
+
+ for (ProgressStyle.Point point : points) {
+ positionToPointMap.put(point.getPosition(), point);
+ }
+
+ return positionToPointMap;
+ }
+
+ private static SortedSet<Integer> generateSortedPositionSet(
+ Map<Integer, ProgressStyle.Segment> startToSegmentMap,
+ Map<Integer, ProgressStyle.Point> positionToPointMap, int progress,
+ boolean isStyledByProgress) {
+ final SortedSet<Integer> sortedPos = new TreeSet<>(startToSegmentMap.keySet());
+ sortedPos.addAll(positionToPointMap.keySet());
+ if (isStyledByProgress) {
+ sortedPos.add(progress);
+ }
+
+ return sortedPos;
+ }
+}
diff --git a/core/java/com/android/internal/widget/NotificationProgressDrawable.java b/core/java/com/android/internal/widget/NotificationProgressDrawable.java
index 4d88546..89ef875 100644
--- a/core/java/com/android/internal/widget/NotificationProgressDrawable.java
+++ b/core/java/com/android/internal/widget/NotificationProgressDrawable.java
@@ -45,6 +45,7 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Objects;
/**
* This is used by NotificationProgressBar for displaying a custom background. It composes of
@@ -126,7 +127,7 @@
* @see #setStroke(int, int, float, float)
*/
public void setStrokeDefaultColor(@ColorInt int color) {
- mState.mStrokeColor = color;
+ mState.setStrokeColor(color);
}
/**
@@ -138,7 +139,7 @@
* @see #mutate()
*/
public void setPointRectDefaultColor(@ColorInt int color) {
- mState.mPointRectColor = color;
+ mState.setPointRectColor(color);
}
private void setStrokeInternal(int width, float dashWidth, float dashGap) {
@@ -194,7 +195,7 @@
mStrokePaint.setColor(segment.mColor != Color.TRANSPARENT ? segment.mColor
: mState.mStrokeColor);
mDashedStrokePaint.setColor(segment.mColor != Color.TRANSPARENT ? segment.mColor
- : mState.mStrokeColor);
+ : mState.mFadedStrokeColor);
// Leave space for the rounded line cap which extends beyond start/end.
final float capWidth = mStrokePaint.getStrokeWidth() / 2F;
@@ -220,7 +221,8 @@
mPointRectF.inset(inset, inset);
mFillPaint.setColor(point.mColor != Color.TRANSPARENT ? point.mColor
- : mState.mPointRectColor);
+ : (point.mFaded ? mState.mFadedPointRectColor
+ : mState.mPointRectColor));
canvas.drawRoundRect(mPointRectF, cornerRadius, cornerRadius, mFillPaint);
}
@@ -424,8 +426,9 @@
state.mPointRectCornerRadius = a.getDimension(
R.styleable.NotificationProgressDrawablePoints_cornerRadius,
state.mPointRectCornerRadius);
- state.mPointRectColor = a.getColor(R.styleable.NotificationProgressDrawablePoints_color,
+ final int color = a.getColor(R.styleable.NotificationProgressDrawablePoints_color,
state.mPointRectColor);
+ setPointRectDefaultColor(color);
}
static int resolveDensity(@Nullable Resources r, int parentDensity) {
@@ -478,7 +481,6 @@
* {@link Point} with zero length.
*/
public interface Part {
-
}
/**
@@ -521,6 +523,24 @@
return "Segment(fraction=" + this.mFraction + ", color=" + this.mColor + ", dashed="
+ this.mDashed + ')';
}
+
+ // Needed for unit tests
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) return true;
+
+ if (other == null || getClass() != other.getClass()) return false;
+
+ Segment that = (Segment) other;
+ if (Float.compare(this.mFraction, that.mFraction) != 0) return false;
+ if (this.mColor != that.mColor) return false;
+ return this.mDashed == that.mDashed;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mFraction, mColor, mDashed);
+ }
}
/**
@@ -532,14 +552,21 @@
@Nullable
private final Drawable mIcon;
@ColorInt private final int mColor;
+ private final boolean mFaded;
public Point(@Nullable Drawable icon) {
- this(icon, Color.TRANSPARENT);
+ this(icon, Color.TRANSPARENT, false);
}
public Point(@Nullable Drawable icon, @ColorInt int color) {
+ this(icon, color, false);
+
+ }
+
+ public Point(@Nullable Drawable icon, @ColorInt int color, boolean faded) {
mIcon = icon;
mColor = color;
+ mFaded = faded;
}
@Nullable
@@ -547,9 +574,37 @@
return this.mIcon;
}
+ public int getColor() {
+ return this.mColor;
+ }
+
+ public boolean getFaded() {
+ return this.mFaded;
+ }
+
@Override
public String toString() {
- return "Point(icon=" + this.mIcon + ", color=" + this.mColor + ')';
+ return "Point(icon=" + this.mIcon + ", color=" + this.mColor + ", faded=" + this.mFaded
+ + ")";
+ }
+
+ // Needed for unit tests.
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) return true;
+
+ if (other == null || getClass() != other.getClass()) return false;
+
+ Point that = (Point) other;
+
+ if (!Objects.equals(this.mIcon, that.mIcon)) return false;
+ if (this.mColor != that.mColor) return false;
+ return this.mFaded == that.mFaded;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mIcon, mColor, mFaded);
}
}
@@ -576,12 +631,14 @@
float mSegPointGap = 0.0f;
int mStrokeWidth = 0;
int mStrokeColor;
+ int mFadedStrokeColor;
float mStrokeDashWidth = 0.0f;
float mStrokeDashGap = 0.0f;
float mPointRadius;
float mPointRectInset;
float mPointRectCornerRadius;
int mPointRectColor;
+ int mFadedPointRectColor;
int[] mThemeAttrs;
int[] mThemeAttrsSegments;
@@ -595,6 +652,7 @@
State(@NonNull State orig, @Nullable Resources res) {
mChangingConfigurations = orig.mChangingConfigurations;
mStrokeColor = orig.mStrokeColor;
+ mFadedStrokeColor = orig.mFadedStrokeColor;
mStrokeWidth = orig.mStrokeWidth;
mStrokeDashWidth = orig.mStrokeDashWidth;
mStrokeDashGap = orig.mStrokeDashGap;
@@ -602,6 +660,7 @@
mPointRectInset = orig.mPointRectInset;
mPointRectCornerRadius = orig.mPointRectCornerRadius;
mPointRectColor = orig.mPointRectColor;
+ mFadedPointRectColor = orig.mFadedPointRectColor;
mThemeAttrs = orig.mThemeAttrs;
mThemeAttrsSegments = orig.mThemeAttrsSegments;
@@ -683,10 +742,30 @@
public void setStroke(int width, int color, float dashWidth, float dashGap) {
mStrokeWidth = width;
- mStrokeColor = color;
mStrokeDashWidth = dashWidth;
mStrokeDashGap = dashGap;
+
+ setStrokeColor(color);
}
+
+ public void setStrokeColor(int color) {
+ mStrokeColor = color;
+ mFadedStrokeColor = getFadedColor(color);
+ }
+
+ public void setPointRectColor(int color) {
+ mPointRectColor = color;
+ mFadedPointRectColor = getFadedColor(color);
+ }
+ }
+
+ /**
+ * Get a color with an opacity that's 50% of the input color.
+ */
+ @ColorInt
+ static int getFadedColor(@ColorInt int color) {
+ return Color.argb(Color.alpha(color) / 2, Color.red(color), Color.green(color),
+ Color.blue(color));
}
@Override
diff --git a/core/jni/android_view_WindowManagerGlobal.cpp b/core/jni/android_view_WindowManagerGlobal.cpp
index abc621d..4202de3 100644
--- a/core/jni/android_view_WindowManagerGlobal.cpp
+++ b/core/jni/android_view_WindowManagerGlobal.cpp
@@ -69,8 +69,8 @@
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 @@
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 88b3e1c..27417c0 100644
--- a/core/jni/platform/host/HostRuntime.cpp
+++ b/core/jni/platform/host/HostRuntime.cpp
@@ -115,9 +115,9 @@
#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 a28b2f6..51e79e7 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 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.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 @@
}
+ @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 6e563ff..da202b6 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 @@
// 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/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java
new file mode 100644
index 0000000..6419c1e0
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java
@@ -0,0 +1,302 @@
+/*
+ * 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.internal.widget;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Notification.ProgressStyle;
+import android.graphics.Color;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.internal.widget.NotificationProgressDrawable.Part;
+import com.android.internal.widget.NotificationProgressDrawable.Point;
+import com.android.internal.widget.NotificationProgressDrawable.Segment;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class NotificationProgressBarTest {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void processAndConvertToDrawableParts_segmentsIsEmpty() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ int progress = 50;
+ boolean isStyledByProgress = true;
+
+ NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
+ isStyledByProgress);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void processAndConvertToDrawableParts_segmentLengthIsNegative() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(-50));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ int progress = 50;
+ boolean isStyledByProgress = true;
+
+ NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
+ isStyledByProgress);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void processAndConvertToDrawableParts_segmentLengthIsZero() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(0));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ int progress = 50;
+ boolean isStyledByProgress = true;
+
+ NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
+ isStyledByProgress);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void processAndConvertToDrawableParts_progressIsNegative() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(100));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ int progress = -50;
+ boolean isStyledByProgress = true;
+
+ NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
+ isStyledByProgress);
+ }
+
+ @Test
+ public void processAndConvertToDrawableParts_progressIsZero() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(100).setColor(Color.RED));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ int progress = 0;
+ boolean isStyledByProgress = true;
+
+ List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
+ segments, points, progress, isStyledByProgress);
+
+ int fadedRed = 0x7FFF0000;
+ List<Part> expected = new ArrayList<>(List.of(new Segment(1f, fadedRed, true)));
+
+ assertThat(parts).isEqualTo(expected);
+ }
+
+ @Test
+ public void processAndConvertToDrawableParts_progressAtMax() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(100).setColor(Color.RED));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ int progress = 100;
+ boolean isStyledByProgress = true;
+
+ List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
+ segments, points, progress, isStyledByProgress);
+
+ List<Part> expected = new ArrayList<>(List.of(new Segment(1f, Color.RED)));
+
+ assertThat(parts).isEqualTo(expected);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void processAndConvertToDrawableParts_progressAboveMax() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(100));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ int progress = 150;
+ boolean isStyledByProgress = true;
+
+ NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
+ isStyledByProgress);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void processAndConvertToDrawableParts_pointPositionIsNegative() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(100));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ points.add(new ProgressStyle.Point(-50).setColor(Color.RED));
+ int progress = 50;
+ boolean isStyledByProgress = true;
+
+ NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
+ isStyledByProgress);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void processAndConvertToDrawableParts_pointPositionIsZero() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(100));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ points.add(new ProgressStyle.Point(0).setColor(Color.RED));
+ int progress = 50;
+ boolean isStyledByProgress = true;
+
+ NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
+ isStyledByProgress);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void processAndConvertToDrawableParts_pointPositionAtMax() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(100));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ points.add(new ProgressStyle.Point(100).setColor(Color.RED));
+ int progress = 50;
+ boolean isStyledByProgress = true;
+
+ NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
+ isStyledByProgress);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void processAndConvertToDrawableParts_pointPositionAboveMax() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(100));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ points.add(new ProgressStyle.Point(150).setColor(Color.RED));
+ int progress = 50;
+ boolean isStyledByProgress = true;
+
+ NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
+ isStyledByProgress);
+ }
+
+ @Test
+ public void processAndConvertToDrawableParts_multipleSegmentsWithoutPoints() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(50).setColor(Color.RED));
+ segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ int progress = 60;
+ boolean isStyledByProgress = true;
+
+ List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
+ segments, points, progress, isStyledByProgress);
+
+ // Colors with 50% opacity
+ int fadedGreen = 0x7F00FF00;
+
+ List<Part> expected = new ArrayList<>(List.of(
+ new Segment(0.50f, Color.RED),
+ new Segment(0.10f, Color.GREEN),
+ new Segment(0.40f, fadedGreen, true)));
+
+ assertThat(parts).isEqualTo(expected);
+ }
+
+ @Test
+ public void processAndConvertToDrawableParts_singleSegmentWithPoints() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ points.add(new ProgressStyle.Point(15).setColor(Color.RED));
+ points.add(new ProgressStyle.Point(25).setColor(Color.BLUE));
+ points.add(new ProgressStyle.Point(60).setColor(Color.BLUE));
+ points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW));
+ int progress = 60;
+ boolean isStyledByProgress = true;
+
+ // Colors with 50% opacity
+ int fadedBlue = 0x7F0000FF;
+ int fadedYellow = 0x7FFFFF00;
+
+ List<Part> expected = new ArrayList<>(List.of(
+ new Segment(0.15f, Color.BLUE),
+ new Point(null, Color.RED),
+ new Segment(0.10f, Color.BLUE),
+ new Point(null, Color.BLUE),
+ new Segment(0.35f, Color.BLUE),
+ new Point(null, Color.BLUE),
+ new Segment(0.15f, fadedBlue, true),
+ new Point(null, fadedYellow, true),
+ new Segment(0.25f, fadedBlue, true)));
+
+ List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
+ segments, points, progress, isStyledByProgress);
+
+ assertThat(parts).isEqualTo(expected);
+ }
+
+ @Test
+ public void processAndConvertToDrawableParts_multipleSegmentsWithPoints() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(50).setColor(Color.RED));
+ segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ points.add(new ProgressStyle.Point(15).setColor(Color.RED));
+ points.add(new ProgressStyle.Point(25).setColor(Color.BLUE));
+ points.add(new ProgressStyle.Point(60).setColor(Color.BLUE));
+ points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW));
+ int progress = 60;
+ boolean isStyledByProgress = true;
+
+ List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
+ segments, points, progress, isStyledByProgress);
+
+ // Colors with 50% opacity
+ int fadedGreen = 0x7F00FF00;
+ int fadedBlue = 0x7F0000FF;
+ int fadedYellow = 0x7FFFFF00;
+
+ List<Part> expected = new ArrayList<>(List.of(
+ new Segment(0.15f, Color.RED),
+ new Point(null, Color.RED),
+ new Segment(0.10f, Color.RED),
+ new Point(null, Color.BLUE),
+ new Segment(0.25f, Color.RED),
+ new Segment(0.10f, Color.GREEN),
+ new Point(null, Color.BLUE),
+ new Segment(0.15f, fadedGreen, true),
+ new Point(null, fadedYellow, true),
+ new Segment(0.25f, fadedGreen, true)));
+
+ assertThat(parts).isEqualTo(expected);
+ }
+
+ @Test
+ public void processAndConvertToDrawableParts_multipleSegmentsWithPoints_notStyledByProgress() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(50).setColor(Color.RED));
+ segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ points.add(new ProgressStyle.Point(15).setColor(Color.RED));
+ points.add(new ProgressStyle.Point(25).setColor(Color.BLUE));
+ points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW));
+ int progress = 60;
+ boolean isStyledByProgress = false;
+
+ List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
+ segments, points, progress, isStyledByProgress);
+
+ List<Part> expected = new ArrayList<>(List.of(
+ new Segment(0.15f, Color.RED),
+ new Point(null, Color.RED),
+ new Segment(0.10f, Color.RED),
+ new Point(null, Color.BLUE),
+ new Segment(0.25f, Color.RED),
+ new Segment(0.25f, Color.GREEN),
+ new Point(null, Color.YELLOW),
+ new Segment(0.25f, Color.GREEN)));
+
+ assertThat(parts).isEqualTo(expected);
+ }
+}
diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml
index 3b739c3..1260796 100644
--- a/libs/WindowManager/Shell/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/AndroidManifest.xml
@@ -24,6 +24,7 @@
<uses-permission android:name="android.permission.WAKEUP_SURFACE_FLINGER" />
<uses-permission android:name="android.permission.READ_FRAME_BUFFER" />
<uses-permission android:name="android.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE" />
+ <uses-permission android:name="android.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION" />
<application>
<activity
diff --git a/libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_dismiss_button_background.xml b/libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_confirm_button_background.xml
similarity index 100%
rename from libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_dismiss_button_background.xml
rename to libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_confirm_button_background.xml
diff --git a/libs/WindowManager/Shell/res/layout/open_by_default_settings_dialog.xml b/libs/WindowManager/Shell/res/layout/open_by_default_settings_dialog.xml
index 8ff382b..b5bceda 100644
--- a/libs/WindowManager/Shell/res/layout/open_by_default_settings_dialog.xml
+++ b/libs/WindowManager/Shell/res/layout/open_by_default_settings_dialog.xml
@@ -111,7 +111,7 @@
</RadioGroup>
<Button
- android:id="@+id/open_by_default_settings_dialog_dismiss_button"
+ android:id="@+id/open_by_default_settings_dialog_confirm_button"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:text="@string/open_by_default_dialog_dismiss_button_text"
@@ -122,7 +122,7 @@
android:textSize="14sp"
android:textFontWeight="500"
android:textColor="?androidprv:attr/materialColorOnPrimary"
- android:background="@drawable/open_by_default_settings_dialog_dismiss_button_background"/>
+ android:background="@drawable/open_by_default_settings_dialog_confirm_button_background"/>
</LinearLayout>
</ScrollView>
</FrameLayout>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
index 71bcb59..65132fe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
@@ -22,7 +22,13 @@
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.PackageManager
+import android.content.pm.verify.domain.DomainVerificationManager
+import android.content.pm.verify.domain.DomainVerificationUserState
import android.net.Uri
+import com.android.internal.protolog.ProtoLog
+import com.android.wm.shell.protolog.ShellProtoLogGroup
+
+private const val TAG = "AppToWebUtils"
private val GenericBrowserIntent = Intent()
.setAction(Intent.ACTION_VIEW)
@@ -58,4 +64,25 @@
val component = intent.resolveActivity(packageManager) ?: return null
intent.setComponent(component)
return intent
-}
\ No newline at end of file
+}
+
+/**
+ * Returns the [DomainVerificationUserState] of the user associated with the given
+ * [DomainVerificationManager] and the given package.
+ */
+fun getDomainVerificationUserState(
+ manager: DomainVerificationManager,
+ packageName: String
+): DomainVerificationUserState? {
+ try {
+ return manager.getDomainVerificationUserState(packageName)
+ } catch (e: PackageManager.NameNotFoundException) {
+ ProtoLog.w(
+ ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+ "%s: Failed to get domain verification user state: %s",
+ TAG,
+ e.message!!
+ )
+ return null
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt
index 4926cbd..a727b54 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt
@@ -19,6 +19,7 @@
import android.app.ActivityManager.RunningTaskInfo
import android.app.TaskInfo
import android.content.Context
+import android.content.pm.verify.domain.DomainVerificationManager
import android.graphics.Bitmap
import android.graphics.PixelFormat
import android.view.LayoutInflater
@@ -30,6 +31,7 @@
import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
import android.view.WindowlessWindowManager
import android.widget.ImageView
+import android.widget.RadioButton
import android.widget.TextView
import android.window.TaskConstants
import com.android.wm.shell.R
@@ -58,8 +60,17 @@
private lateinit var appIconView: ImageView
private lateinit var appNameView: TextView
+ private lateinit var openInAppButton: RadioButton
+ private lateinit var openInBrowserButton: RadioButton
+
+ private val domainVerificationManager =
+ context.getSystemService(DomainVerificationManager::class.java)!!
+ private val packageName = taskInfo.baseActivity?.packageName!!
+
+
init {
createDialog()
+ initializeRadioButtons()
bindAppInfo(appIconBitmap, appName)
}
@@ -111,9 +122,30 @@
closeMenu()
}
+ dialog.setConfirmButtonClickListener {
+ setDefaultLinkHandlingSetting()
+ closeMenu()
+ }
+
listener.onDialogCreated()
}
+ private fun initializeRadioButtons() {
+ openInAppButton = dialog.requireViewById(R.id.open_in_app_button)
+ openInBrowserButton = dialog.requireViewById(R.id.open_in_browser_button)
+
+ val userState =
+ getDomainVerificationUserState(domainVerificationManager, packageName) ?: return
+ val openInApp = userState.isLinkHandlingAllowed
+ openInAppButton.isChecked = openInApp
+ openInBrowserButton.isChecked = !openInApp
+ }
+
+ private fun setDefaultLinkHandlingSetting() {
+ domainVerificationManager.setDomainVerificationLinkHandlingAllowed(
+ packageName, openInAppButton.isChecked)
+ }
+
private fun closeMenu() {
dialogContainer?.releaseView()
dialogContainer = null
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialogView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialogView.kt
index d03a38e..1b914f4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialogView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialogView.kt
@@ -36,9 +36,6 @@
private lateinit var backgroundDim: Drawable
fun setDismissOnClickListener(callback: (View) -> Unit) {
- val dismissButton = dialogContainer.requireViewById<Button>(
- R.id.open_by_default_settings_dialog_dismiss_button)
- dismissButton.setOnClickListener(callback)
// Clicks on the background dim should also dismiss the dialog.
setOnClickListener(callback)
// We add a no-op on-click listener to the dialog container so that clicks on it won't
@@ -46,6 +43,13 @@
dialogContainer.setOnClickListener { }
}
+ fun setConfirmButtonClickListener(callback: (View) -> Unit) {
+ val dismissButton = dialogContainer.requireViewById<Button>(
+ R.id.open_by_default_settings_dialog_confirm_button
+ )
+ dismissButton.setOnClickListener(callback)
+ }
+
override fun onFinishInflate() {
super.onFinishInflate()
dialogContainer = requireViewById(R.id.open_by_default_dialog_container)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 75adef4..52262e6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -264,7 +264,8 @@
Optional<DesktopTasksLimiter> desktopTasksLimiter,
AppHandleEducationController appHandleEducationController,
WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
- Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler) {
+ Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler,
+ FocusTransitionObserver focusTransitionObserver) {
if (DesktopModeStatus.canEnterDesktopMode(context)) {
return new DesktopModeWindowDecorViewModel(
context,
@@ -291,7 +292,8 @@
desktopTasksLimiter,
appHandleEducationController,
windowDecorCaptionHandleRepository,
- desktopActivityOrientationHandler);
+ desktopActivityOrientationHandler,
+ focusTransitionObserver);
}
return new CaptionWindowDecorViewModel(
context,
@@ -305,7 +307,8 @@
displayController,
rootTaskDisplayAreaOrganizer,
syncQueue,
- transitions);
+ transitions,
+ focusTransitionObserver);
}
@WMSingleton
@@ -695,10 +698,16 @@
static Optional<DesktopFullImmersiveTransitionHandler> provideDesktopImmersiveHandler(
Context context,
Transitions transitions,
- @DynamicOverride DesktopRepository desktopRepository) {
+ @DynamicOverride DesktopRepository desktopRepository,
+ DisplayController displayController,
+ ShellTaskOrganizer shellTaskOrganizer) {
if (DesktopModeStatus.canEnterDesktopMode(context)) {
return Optional.of(
- new DesktopFullImmersiveTransitionHandler(transitions, desktopRepository));
+ new DesktopFullImmersiveTransitionHandler(
+ transitions,
+ desktopRepository,
+ displayController,
+ shellTaskOrganizer));
}
return Optional.empty();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt
index f749aa1..679179a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt
@@ -27,8 +27,12 @@
import android.window.TransitionRequestInfo
import android.window.WindowContainerTransaction
import androidx.core.animation.addListener
+import com.android.internal.annotations.VisibleForTesting
import com.android.internal.protolog.ProtoLog
-import com.android.wm.shell.protolog.ShellProtoLogGroup
+import com.android.window.flags.Flags
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.TransitionHandler
import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
@@ -41,16 +45,29 @@
class DesktopFullImmersiveTransitionHandler(
private val transitions: Transitions,
private val desktopRepository: DesktopRepository,
+ private val displayController: DisplayController,
+ private val shellTaskOrganizer: ShellTaskOrganizer,
private val transactionSupplier: () -> SurfaceControl.Transaction,
) : TransitionHandler {
constructor(
transitions: Transitions,
desktopRepository: DesktopRepository,
- ) : this(transitions, desktopRepository, { SurfaceControl.Transaction() })
+ displayController: DisplayController,
+ shellTaskOrganizer: ShellTaskOrganizer,
+ ) : this(
+ transitions,
+ desktopRepository,
+ displayController,
+ shellTaskOrganizer,
+ { SurfaceControl.Transaction() }
+ )
private var state: TransitionState? = null
+ @VisibleForTesting
+ val pendingExternalExitTransitions = mutableSetOf<ExternalPendingExit>()
+
/** Whether there is an immersive transition that hasn't completed yet. */
private val inProgress: Boolean
get() = state != null
@@ -61,15 +78,15 @@
var onTaskResizeAnimationListener: OnTaskResizeAnimationListener? = null
/** Starts a transition to enter full immersive state inside the desktop. */
- fun enterImmersive(taskInfo: RunningTaskInfo, wct: WindowContainerTransaction) {
+ fun moveTaskToImmersive(taskInfo: RunningTaskInfo) {
if (inProgress) {
- ProtoLog.v(
- ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
- "FullImmersive: cannot start entry because transition already in progress."
- )
+ logV("Cannot start entry because transition already in progress.")
return
}
-
+ val wct = WindowContainerTransaction().apply {
+ setBounds(taskInfo.token, Rect())
+ }
+ logV("Moving task ${taskInfo.taskId} into immersive mode")
val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ this)
state = TransitionState(
transition = transition,
@@ -79,15 +96,18 @@
)
}
- fun exitImmersive(taskInfo: RunningTaskInfo, wct: WindowContainerTransaction) {
+ fun moveTaskToNonImmersive(taskInfo: RunningTaskInfo) {
if (inProgress) {
- ProtoLog.v(
- ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
- "$TAG: cannot start exit because transition already in progress."
- )
+ logV("Cannot start exit because transition already in progress.")
return
}
+ val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
+ val destinationBounds = calculateMaximizeBounds(displayLayout, taskInfo)
+ val wct = WindowContainerTransaction().apply {
+ setBounds(taskInfo.token, destinationBounds)
+ }
+ logV("Moving task ${taskInfo.taskId} out of immersive mode")
val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ this)
state = TransitionState(
transition = transition,
@@ -97,6 +117,82 @@
)
}
+ /**
+ * Bring the immersive app of the given [displayId] out of immersive mode, if applicable.
+ *
+ * @param transition that will apply this transaction
+ * @param wct that will apply these changes
+ * @param displayId of the display that should exit immersive mode
+ */
+ fun exitImmersiveIfApplicable(
+ transition: IBinder,
+ wct: WindowContainerTransaction,
+ displayId: Int
+ ) {
+ if (!Flags.enableFullyImmersiveInDesktop()) return
+ exitImmersiveIfApplicable(wct, displayId)?.invoke(transition)
+ }
+
+ /**
+ * Bring the immersive app of the given [displayId] out of immersive mode, if applicable.
+ *
+ * @param wct that will apply these changes
+ * @param displayId of the display that should exit immersive mode
+ * @return a function to apply once the transition that will apply these changes is started
+ */
+ fun exitImmersiveIfApplicable(
+ wct: WindowContainerTransaction,
+ displayId: Int
+ ): ((IBinder) -> Unit)? {
+ if (!Flags.enableFullyImmersiveInDesktop()) return null
+ val displayLayout = displayController.getDisplayLayout(displayId) ?: return null
+ val immersiveTask = desktopRepository.getTaskInFullImmersiveState(displayId) ?: return null
+ val taskInfo = shellTaskOrganizer.getRunningTaskInfo(immersiveTask) ?: return null
+ logV("Appending immersive exit for task: $immersiveTask in display: $displayId")
+ wct.setBounds(taskInfo.token, calculateMaximizeBounds(displayLayout, taskInfo))
+ return { transition -> addPendingImmersiveExit(immersiveTask, displayId, transition) }
+ }
+
+ /**
+ * Bring the given [taskInfo] out of immersive mode, if applicable.
+ *
+ * @param wct that will apply these changes
+ * @param taskInfo of the task that should exit immersive mode
+ * @return a function to apply once the transition that will apply these changes is started
+ */
+ fun exitImmersiveIfApplicable(
+ wct: WindowContainerTransaction,
+ taskInfo: RunningTaskInfo
+ ): ((IBinder) -> Unit)? {
+ if (!Flags.enableFullyImmersiveInDesktop()) return null
+ if (desktopRepository.isTaskInFullImmersiveState(taskInfo.taskId)) {
+ // A full immersive task is being minimized, make sure the immersive state is broken
+ // (i.e. resize back to max bounds).
+ displayController.getDisplayLayout(taskInfo.displayId)?.let { displayLayout ->
+ wct.setBounds(taskInfo.token, calculateMaximizeBounds(displayLayout, taskInfo))
+ logV("Appending immersive exit for task: ${taskInfo.taskId}")
+ return { transition ->
+ addPendingImmersiveExit(
+ taskId = taskInfo.taskId,
+ displayId = taskInfo.displayId,
+ transition = transition
+ )
+ }
+ }
+ }
+ return null
+ }
+
+ private fun addPendingImmersiveExit(taskId: Int, displayId: Int, transition: IBinder) {
+ pendingExternalExitTransitions.add(
+ ExternalPendingExit(
+ taskId = taskId,
+ displayId = displayId,
+ transition = transition
+ )
+ )
+ }
+
override fun startAnimation(
transition: IBinder,
info: TransitionInfo,
@@ -190,15 +286,31 @@
* Called when any transition in the system is ready to play. This is needed to update the
* repository state before window decorations are drawn (which happens immediately after
* |onTransitionReady|, before this transition actually animates) because drawing decorations
- * depends in whether the task is in full immersive state or not.
+ * depends on whether the task is in full immersive state or not.
*/
- fun onTransitionReady(transition: IBinder) {
+ fun onTransitionReady(transition: IBinder, info: TransitionInfo) {
+ // Check if this is a pending external exit transition.
+ val pendingExit = pendingExternalExitTransitions
+ .firstOrNull { pendingExit -> pendingExit.transition == transition }
+ if (pendingExit != null) {
+ pendingExternalExitTransitions.remove(pendingExit)
+ if (info.hasTaskChange(taskId = pendingExit.taskId)) {
+ if (desktopRepository.isTaskInFullImmersiveState(pendingExit.taskId)) {
+ logV("Pending external exit for task ${pendingExit.taskId} verified")
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = pendingExit.displayId,
+ taskId = pendingExit.taskId,
+ immersive = false
+ )
+ }
+ }
+ return
+ }
+
+ // Check if this is a direct immersive enter/exit transition.
val state = this.state ?: return
- // TODO: b/369443668 - this assumes invoking the exit transition is the only way to exit
- // immersive, which isn't realistic. The app could crash, the user could dismiss it from
- // overview, etc. This (or its caller) should search all transitions to look for any
- // immersive task exiting that state to keep the repository properly updated.
if (transition == state.transition) {
+ logV("Direct move for task ${state.taskId} in ${state.direction} direction verified")
when (state.direction) {
Direction.ENTER -> {
desktopRepository.setTaskInFullImmersiveState(
@@ -225,6 +337,9 @@
private fun requireState(): TransitionState =
state ?: error("Expected non-null transition state")
+ private fun TransitionInfo.hasTaskChange(taskId: Int): Boolean =
+ changes.any { c -> c.taskInfo?.taskId == taskId }
+
/** The state of the currently running transition. */
private data class TransitionState(
val transition: IBinder,
@@ -233,12 +348,28 @@
val direction: Direction
)
+ /**
+ * Tracks state of a transition involving an immersive exit that is external to this class' own
+ * transitions. This usually means transitions that exit immersive mode as a side-effect and
+ * not the primary action (for example, minimizing the immersive task or launching a new task
+ * on top of the immersive task).
+ */
+ data class ExternalPendingExit(
+ val taskId: Int,
+ val displayId: Int,
+ val transition: IBinder,
+ )
+
private enum class Direction {
ENTER, EXIT
}
+ private fun logV(msg: String, vararg arguments: Any?) {
+ ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+ }
+
private companion object {
- private const val TAG = "FullImmersiveHandler"
+ private const val TAG = "DesktopImmersive"
private const val FULL_IMMERSIVE_ANIM_DURATION_MS = 336L
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
index bd61722..6d47922 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
@@ -123,6 +123,29 @@
}
/**
+ * Calculates the maximized bounds of a task given in the given [DisplayLayout], taking
+ * resizability into consideration.
+ */
+fun calculateMaximizeBounds(
+ displayLayout: DisplayLayout,
+ taskInfo: RunningTaskInfo,
+): Rect {
+ val stableBounds = Rect()
+ displayLayout.getStableBounds(stableBounds)
+ if (taskInfo.isResizeable) {
+ // if resizable then expand to entire stable bounds (full display minus insets)
+ return Rect(stableBounds)
+ } else {
+ // if non-resizable then calculate max bounds according to aspect ratio
+ val activityAspectRatio = calculateAspectRatio(taskInfo)
+ val newSize = maximizeSizeGivenAspectRatio(taskInfo,
+ Size(stableBounds.width(), stableBounds.height()), activityAspectRatio)
+ return centerInArea(
+ newSize, stableBounds, stableBounds.left, stableBounds.top)
+ }
+}
+
+/**
* Calculates the largest size that can fit in a given area while maintaining a specific aspect
* ratio.
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
index c175133..5ac4ef5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
@@ -328,6 +328,10 @@
return desktopTaskDataSequence().any { taskId == it.fullImmersiveTaskId }
}
+ /** Returns the task that is currently in immersive mode in this display, or null. */
+ fun getTaskInFullImmersiveState(displayId: Int): Int? =
+ desktopTaskDataByDisplayId.getOrCreate(displayId).fullImmersiveTaskId
+
private fun notifyVisibleTaskListeners(displayId: Int, visibleTasksCount: Int) {
visibleTasksListeners.forEach { (listener, executor) ->
executor.execute { listener.onTasksVisibilityChanged(displayId, visibleTasksCount) }
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 75c795b..92535f3 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
@@ -91,6 +91,7 @@
import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.shared.annotations.ExternalThread
import com.android.wm.shell.shared.annotations.ShellMainThread
+import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.DESKTOP_DENSITY_OVERRIDE
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.useDesktopOverrideDensity
@@ -190,6 +191,7 @@
private var recentsAnimationRunning = false
private lateinit var splitScreenController: SplitScreenController
+ lateinit var freeformTaskTransitionStarter: FreeformTaskTransitionStarter
// Launch cookie used to identify a drag and drop transition to fullscreen after it has begun.
// Used to prevent handleRequest from moving the new fullscreen task to freeform.
private var dragAndDropFullscreenCookie: Binder? = null
@@ -354,6 +356,8 @@
// TODO(342378842): Instead of using default display, support multiple displays
val taskToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask(
DEFAULT_DISPLAY, wct, taskId)
+ val runOnTransit = immersiveTransitionHandler
+ .exitImmersiveIfApplicable(wct, DEFAULT_DISPLAY)
wct.startTask(
taskId,
ActivityOptions.makeBasic().apply {
@@ -363,6 +367,7 @@
// TODO(343149901): Add DPI changes for task launch
val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource)
addPendingMinimizeTransition(transition, taskToMinimize)
+ runOnTransit?.invoke(transition)
return true
}
@@ -379,6 +384,7 @@
}
logV("moveRunningTaskToDesktop taskId=%d", task.taskId)
exitSplitIfApplicable(wct, task)
+ val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(wct, task.displayId)
// Bring other apps to front first
val taskToMinimize =
bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId)
@@ -386,6 +392,7 @@
val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource)
addPendingMinimizeTransition(transition, taskToMinimize)
+ runOnTransit?.invoke(transition)
}
/**
@@ -422,8 +429,13 @@
val taskToMinimize =
bringDesktopAppsToFrontBeforeShowingNewTask(taskInfo.displayId, wct, taskInfo.taskId)
addMoveToDesktopChanges(wct, taskInfo)
+ val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(
+ wct, taskInfo.displayId)
val transition = dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct)
- transition?.let { addPendingMinimizeTransition(it, taskToMinimize) }
+ transition?.let {
+ addPendingMinimizeTransition(it, taskToMinimize)
+ runOnTransit?.invoke(transition)
+ }
}
/**
@@ -453,20 +465,36 @@
removeWallpaperActivity(wct)
}
taskRepository.addClosingTask(displayId, taskId)
+ taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
+ doesAnyTaskRequireTaskbarRounding(
+ displayId,
+ taskId
+ )
+ )
}
- /**
- * Perform clean up of the desktop wallpaper activity if the minimized window task is the last
- * active task.
- *
- * @param wct transaction to modify if the last active task is minimized
- * @param taskId task id of the window that's being minimized
- */
- fun onDesktopWindowMinimize(wct: WindowContainerTransaction, taskId: Int) {
+ fun minimizeTask(taskInfo: RunningTaskInfo) {
+ val taskId = taskInfo.taskId
+ val displayId = taskInfo.displayId
+ val wct = WindowContainerTransaction()
if (taskRepository.isOnlyVisibleNonClosingTask(taskId)) {
+ // Perform clean up of the desktop wallpaper activity if the minimized window task is
+ // the last active task.
removeWallpaperActivity(wct)
}
- // Do not call taskRepository.minimizeTask because it will be called by DekstopTasksLimiter.
+ // Notify immersive handler as it might need to exit immersive state.
+ val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(wct, taskInfo)
+
+ wct.reorder(taskInfo.token, false)
+ val transition = freeformTaskTransitionStarter.startMinimizedModeTransition(wct)
+ desktopTasksLimiter.ifPresent {
+ it.addPendingMinimizeChange(
+ transition = transition,
+ displayId = displayId,
+ taskId = taskId
+ )
+ }
+ runOnTransit?.invoke(transition)
}
/** Move a task with given `taskId` to fullscreen */
@@ -552,6 +580,8 @@
// TODO: b/342378842 - Instead of using default display, support multiple displays
val taskToMinimize: RunningTaskInfo? =
addAndGetMinimizeChangesIfNeeded(DEFAULT_DISPLAY, wct, taskId)
+ val runOnTransit = immersiveTransitionHandler
+ .exitImmersiveIfApplicable(wct, DEFAULT_DISPLAY)
wct.startTask(
taskId,
ActivityOptions.makeBasic().apply {
@@ -560,6 +590,7 @@
)
val transition = transitions.startTransition(TRANSIT_OPEN, wct, null /* handler */)
addPendingMinimizeTransition(transition, taskToMinimize)
+ runOnTransit?.invoke(transition)
}
/** Move a task to the front */
@@ -567,11 +598,14 @@
logV("moveTaskToFront taskId=%s", taskInfo.taskId)
val wct = WindowContainerTransaction()
wct.reorder(taskInfo.token, true /* onTop */, true /* includingParents */)
+ val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(
+ wct, taskInfo.displayId)
val taskToMinimize =
addAndGetMinimizeChangesIfNeeded(taskInfo.displayId, wct, taskInfo.taskId)
val transition = transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */)
addPendingMinimizeTransition(transition, taskToMinimize)
+ runOnTransit?.invoke(transition)
}
/**
@@ -643,22 +677,12 @@
private fun moveDesktopTaskToFullImmersive(taskInfo: RunningTaskInfo) {
check(taskInfo.isFreeform) { "Task must already be in freeform" }
- val wct = WindowContainerTransaction().apply {
- setBounds(taskInfo.token, Rect())
- }
- immersiveTransitionHandler.enterImmersive(taskInfo, wct)
+ immersiveTransitionHandler.moveTaskToImmersive(taskInfo)
}
private fun exitDesktopTaskFromFullImmersive(taskInfo: RunningTaskInfo) {
check(taskInfo.isFreeform) { "Task must already be in freeform" }
- val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
- val stableBounds = Rect().apply { displayLayout.getStableBounds(this) }
- val destinationBounds = getMaximizeBounds(taskInfo, stableBounds)
-
- val wct = WindowContainerTransaction().apply {
- setBounds(taskInfo.token, destinationBounds)
- }
- immersiveTransitionHandler.exitImmersive(taskInfo, wct)
+ immersiveTransitionHandler.moveTaskToNonImmersive(taskInfo)
}
/**
@@ -697,7 +721,7 @@
// and toggle to the stable bounds.
taskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, currentTaskBounds)
- destinationBounds.set(getMaximizeBounds(taskInfo, stableBounds))
+ destinationBounds.set(calculateMaximizeBounds(displayLayout, taskInfo))
}
@@ -1285,8 +1309,10 @@
if (useDesktopOverrideDensity()) {
wct.setDensityDpi(task.token, DESKTOP_DENSITY_OVERRIDE)
}
- // Desktop Mode is showing and we're launching a new Task - we might need to minimize
- // a Task.
+ // Desktop Mode is showing and we're launching a new Task:
+ // 1) Exit immersive if needed.
+ immersiveTransitionHandler.exitImmersiveIfApplicable(transition, wct, task.displayId)
+ // 2) minimize a Task if needed.
val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task.taskId)
if (taskToMinimize != null) {
addPendingMinimizeTransition(transition, taskToMinimize)
@@ -1316,6 +1342,9 @@
val taskToMinimize =
addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task.taskId)
addPendingMinimizeTransition(transition, taskToMinimize)
+ immersiveTransitionHandler.exitImmersiveIfApplicable(
+ transition, wct, task.displayId
+ )
}
}
return null
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 a4bc2fe..0b1bb8f 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.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 @@
/**
* 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 @@
shellInit: ShellInit
) : Transitions.TransitionObserver {
+ private var transitionToCloseWallpaper: IBinder? = null
+
init {
if (DesktopModeStatus.canEnterDesktopMode(context)) {
shellInit.addInitCallback(::onInit, this)
@@ -70,6 +73,7 @@
handleBackNavigation(info)
removeTaskIfNeeded(info)
}
+ removeWallpaperOnLastTaskClosingIfNeeded(transition, info)
}
private fun removeTaskIfNeeded(info: TransitionInfo) {
@@ -81,13 +85,9 @@
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 @@
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 @@
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 @@
// 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/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
index 4106a10..771573d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
@@ -89,7 +89,7 @@
// TODO(b/367268953): Remove when DesktopTaskListener is introduced and the repository
// is updated from there **before** the |mWindowDecorViewModel| methods are invoked.
// Otherwise window decoration relayout won't run with the immersive state up to date.
- mImmersiveTransitionHandler.ifPresent(h -> h.onTransitionReady(transition));
+ mImmersiveTransitionHandler.ifPresent(h -> h.onTransitionReady(transition, info));
}
// Update focus state first to ensure the correct state can be queried from listeners.
// TODO(371503964): Remove this once the unified task repository is ready.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index c540ede..be4fd7c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -58,9 +58,11 @@
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.shared.FocusTransitionListener;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.FocusTransitionObserver;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.windowdecor.extension.TaskInfoKt;
@@ -68,7 +70,7 @@
* View model for the window decoration with a caption and shadows. Works with
* {@link CaptionWindowDecoration}.
*/
-public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
+public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusTransitionListener {
private static final String TAG = "CaptionWindowDecorViewModel";
private final ShellTaskOrganizer mTaskOrganizer;
@@ -85,6 +87,7 @@
private final Region mExclusionRegion = Region.obtain();
private final InputManager mInputManager;
private TaskOperations mTaskOperations;
+ private FocusTransitionObserver mFocusTransitionObserver;
/**
* Whether to pilfer the next motion event to send cancellations to the windows below.
@@ -121,7 +124,8 @@
DisplayController displayController,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
SyncTransactionQueue syncQueue,
- Transitions transitions) {
+ Transitions transitions,
+ FocusTransitionObserver focusTransitionObserver) {
mContext = context;
mMainExecutor = shellExecutor;
mMainHandler = mainHandler;
@@ -133,6 +137,7 @@
mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
mSyncQueue = syncQueue;
mTransitions = transitions;
+ mFocusTransitionObserver = focusTransitionObserver;
if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
mTaskOperations = new TaskOperations(null, mContext, mSyncQueue);
}
@@ -148,6 +153,16 @@
} catch (RemoteException e) {
Log.e(TAG, "Failed to register window manager callbacks", e);
}
+ mFocusTransitionObserver.setLocalFocusTransitionListener(this, mMainExecutor);
+ }
+
+ @Override
+ public void onFocusedTaskChanged(int taskId, boolean isFocusedOnDisplay,
+ boolean isFocusedGlobally) {
+ final WindowDecoration decor = mWindowDecorByTaskId.get(taskId);
+ if (decor != null) {
+ decor.relayout(decor.mTaskInfo, isFocusedGlobally);
+ }
}
@Override
@@ -180,7 +195,7 @@
return;
}
- decoration.relayout(taskInfo);
+ decoration.relayout(taskInfo, decoration.mHasGlobalFocus);
}
@Override
@@ -217,7 +232,8 @@
createWindowDecoration(taskInfo, taskSurface, startT, finishT);
} else {
decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */,
- false /* setTaskCropAndPosition */);
+ false /* setTaskCropAndPosition */,
+ mFocusTransitionObserver.hasGlobalFocus(taskInfo));
}
}
@@ -230,7 +246,8 @@
if (decoration == null) return;
decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */,
- false /* setTaskCropAndPosition */);
+ false /* setTaskCropAndPosition */,
+ mFocusTransitionObserver.hasGlobalFocus(taskInfo));
}
@Override
@@ -308,7 +325,8 @@
windowDecoration.setDragPositioningCallback(taskPositioner);
windowDecoration.setTaskDragResizer(taskPositioner);
windowDecoration.relayout(taskInfo, startT, finishT,
- false /* applyStartTransactionOnDraw */, false /* setTaskCropAndPosition */);
+ false /* applyStartTransactionOnDraw */, false /* setTaskCropAndPosition */,
+ mFocusTransitionObserver.hasGlobalFocus(taskInfo));
}
private class CaptionTouchEventListener implements
@@ -359,7 +377,7 @@
}
if (e.getAction() == MotionEvent.ACTION_DOWN) {
final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
- if (!taskInfo.isFocused) {
+ if (!mFocusTransitionObserver.hasGlobalFocus(taskInfo)) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
wct.reorder(mTaskToken, true /* onTop */, true /* includingParents */);
mSyncQueue.queue(wct);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 576c911..509cb85 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -174,7 +174,7 @@
}
@Override
- void relayout(RunningTaskInfo taskInfo) {
+ void relayout(RunningTaskInfo taskInfo, boolean hasGlobalFocus) {
final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
// The crop and position of the task should only be set when a task is fluid resizing. In
// all other cases, it is expected that the transition handler positions and crops the task
@@ -185,7 +185,7 @@
// synced with the buffer transaction (that draws the View). Both will be shown on screen
// at the same, whereas applying them independently causes flickering. See b/270202228.
relayout(taskInfo, t, t, true /* applyStartTransactionOnDraw */,
- shouldSetTaskPositionAndCrop);
+ shouldSetTaskPositionAndCrop, hasGlobalFocus);
}
@VisibleForTesting
@@ -196,12 +196,13 @@
boolean setTaskCropAndPosition,
boolean isStatusBarVisible,
boolean isKeyguardVisibleAndOccluded,
- InsetsState displayInsetsState) {
+ InsetsState displayInsetsState,
+ boolean hasGlobalFocus) {
relayoutParams.reset();
relayoutParams.mRunningTaskInfo = taskInfo;
relayoutParams.mLayoutResId = R.layout.caption_window_decor;
relayoutParams.mCaptionHeightId = getCaptionHeightIdStatic(taskInfo.getWindowingMode());
- relayoutParams.mShadowRadiusId = taskInfo.isFocused
+ relayoutParams.mShadowRadiusId = hasGlobalFocus
? R.dimen.freeform_decor_shadow_focused_thickness
: R.dimen.freeform_decor_shadow_unfocused_thickness;
relayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
@@ -233,7 +234,8 @@
@SuppressLint("MissingPermission")
void relayout(RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
- boolean applyStartTransactionOnDraw, boolean setTaskCropAndPosition) {
+ boolean applyStartTransactionOnDraw, boolean setTaskCropAndPosition,
+ boolean hasGlobalFocus) {
final boolean isFreeform =
taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM;
final boolean isDragResizeable = ENABLE_WINDOWING_SCALED_RESIZING.isTrue()
@@ -245,7 +247,7 @@
updateRelayoutParams(mRelayoutParams, taskInfo, applyStartTransactionOnDraw,
setTaskCropAndPosition, mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded,
- mDisplayController.getInsetsState(taskInfo.displayId));
+ mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus);
relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
// After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index e55bc67..9e089b2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -56,7 +56,6 @@
import android.graphics.Region;
import android.hardware.input.InputManager;
import android.os.Handler;
-import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.UserHandle;
@@ -112,6 +111,7 @@
import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
import com.android.wm.shell.desktopmode.education.AppHandleEducationController;
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.shared.FocusTransitionListener;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
@@ -124,6 +124,7 @@
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.FocusTransitionObserver;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.ExclusionRegionListener;
import com.android.wm.shell.windowdecor.extension.InsetsStateKt;
@@ -133,20 +134,21 @@
import kotlin.Pair;
import kotlin.Unit;
+import kotlinx.coroutines.ExperimentalCoroutinesApi;
+
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
-import kotlinx.coroutines.ExperimentalCoroutinesApi;
-
/**
* View model for the window decoration with a caption and shadows. Works with
* {@link DesktopModeWindowDecoration}.
*/
-public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
+public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
+ FocusTransitionListener {
private static final String TAG = "DesktopModeWindowDecorViewModel";
private final DesktopModeWindowDecoration.Factory mDesktopModeWindowDecorFactory;
@@ -216,6 +218,7 @@
}
};
private final TaskPositionerFactory mTaskPositionerFactory;
+ private final FocusTransitionObserver mFocusTransitionObserver;
public DesktopModeWindowDecorViewModel(
Context context,
@@ -242,7 +245,8 @@
Optional<DesktopTasksLimiter> desktopTasksLimiter,
AppHandleEducationController appHandleEducationController,
WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
- Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler) {
+ Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
+ FocusTransitionObserver focusTransitionObserver) {
this(
context,
shellExecutor,
@@ -274,7 +278,8 @@
appHandleEducationController,
windowDecorCaptionHandleRepository,
activityOrientationChangeHandler,
- new TaskPositionerFactory());
+ new TaskPositionerFactory(),
+ focusTransitionObserver);
}
@VisibleForTesting
@@ -309,7 +314,8 @@
AppHandleEducationController appHandleEducationController,
WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
- TaskPositionerFactory taskPositionerFactory) {
+ TaskPositionerFactory taskPositionerFactory,
+ FocusTransitionObserver focusTransitionObserver) {
mContext = context;
mMainExecutor = shellExecutor;
mMainHandler = mainHandler;
@@ -369,6 +375,7 @@
}
};
mTaskPositionerFactory = taskPositionerFactory;
+ mFocusTransitionObserver = focusTransitionObserver;
shellInit.addInitCallback(this::onInit, this);
}
@@ -402,11 +409,22 @@
return Unit.INSTANCE;
});
}
+ mFocusTransitionObserver.setLocalFocusTransitionListener(this, mMainExecutor);
+ }
+
+ @Override
+ public void onFocusedTaskChanged(int taskId, boolean isFocusedOnDisplay,
+ boolean isFocusedGlobally) {
+ final WindowDecoration decor = mWindowDecorByTaskId.get(taskId);
+ if (decor != null) {
+ decor.relayout(decor.mTaskInfo, isFocusedGlobally);
+ }
}
@Override
public void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter) {
mTaskOperations = new TaskOperations(transitionStarter, mContext, mSyncQueue);
+ mDesktopTasksController.setFreeformTaskTransitionStarter(transitionStarter);
}
@Override
@@ -447,7 +465,7 @@
removeTaskFromEventReceiver(oldTaskInfo.displayId);
incrementEventReceiverTasks(taskInfo.displayId);
}
- decoration.relayout(taskInfo);
+ decoration.relayout(taskInfo, decoration.mHasGlobalFocus);
mActivityOrientationChangeHandler.ifPresent(handler ->
handler.handleActivityOrientationChange(oldTaskInfo, taskInfo));
}
@@ -486,7 +504,8 @@
createWindowDecoration(taskInfo, taskSurface, startT, finishT);
} else {
decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */,
- false /* shouldSetTaskPositionAndCrop */);
+ false /* shouldSetTaskPositionAndCrop */,
+ mFocusTransitionObserver.hasGlobalFocus(taskInfo));
}
}
@@ -499,7 +518,8 @@
if (decoration == null) return;
decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */,
- false /* shouldSetTaskPositionAndCrop */);
+ false /* shouldSetTaskPositionAndCrop */,
+ mFocusTransitionObserver.hasGlobalFocus(taskInfo));
}
@Override
@@ -774,11 +794,7 @@
onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button");
}
} else if (id == R.id.minimize_window) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- mDesktopTasksController.onDesktopWindowMinimize(wct, mTaskId);
- final IBinder transition = mTaskOperations.minimizeTask(mTaskToken, wct);
- mDesktopTasksLimiter.ifPresent(limiter ->
- limiter.addPendingMinimizeChange(transition, mDisplayId, mTaskId));
+ mDesktopTasksController.minimizeTask(decoration.mTaskInfo);
}
}
@@ -895,7 +911,7 @@
}
private void moveTaskToFront(RunningTaskInfo taskInfo) {
- if (!taskInfo.isFocused) {
+ if (!mFocusTransitionObserver.hasGlobalFocus(taskInfo)) {
mDesktopTasksController.moveTaskToFront(taskInfo);
}
}
@@ -1516,7 +1532,8 @@
windowDecoration.setExclusionRegionListener(mExclusionRegionListener);
windowDecoration.setDragPositioningCallback(taskPositioner);
windowDecoration.relayout(taskInfo, startT, finishT,
- false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */);
+ false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */,
+ mFocusTransitionObserver.hasGlobalFocus(taskInfo));
if (!Flags.enableHandleInputFix()) {
incrementEventReceiverTasks(taskInfo.displayId);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index a78fb9b..2c621b1f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -352,7 +352,7 @@
}
@Override
- void relayout(ActivityManager.RunningTaskInfo taskInfo) {
+ void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean hasGlobalFocus) {
final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
// The crop and position of the task should only be set when a task is fluid resizing. In
// all other cases, it is expected that the transition handler positions and crops the task
@@ -365,7 +365,8 @@
// the View). Both will be shown on screen at the same, whereas applying them independently
// causes flickering. See b/270202228.
final boolean applyTransactionOnDraw = taskInfo.isFreeform();
- relayout(taskInfo, t, t, applyTransactionOnDraw, shouldSetTaskPositionAndCrop);
+ relayout(taskInfo, t, t, applyTransactionOnDraw, shouldSetTaskPositionAndCrop,
+ hasGlobalFocus);
if (!applyTransactionOnDraw) {
t.apply();
}
@@ -373,18 +374,19 @@
void relayout(ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
- boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) {
+ boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop,
+ boolean hasGlobalFocus) {
Trace.beginSection("DesktopModeWindowDecoration#relayout");
if (taskInfo.isFreeform()) {
// The Task is in Freeform mode -> show its header in sync since it's an integral part
// of the window itself - a delayed header might cause bad UX.
relayoutInSync(taskInfo, startT, finishT, applyStartTransactionOnDraw,
- shouldSetTaskPositionAndCrop);
+ shouldSetTaskPositionAndCrop, hasGlobalFocus);
} else {
// The Task is outside Freeform mode -> allow the handle view to be delayed since the
// handle is just a small addition to the window.
relayoutWithDelayedViewHost(taskInfo, startT, finishT, applyStartTransactionOnDraw,
- shouldSetTaskPositionAndCrop);
+ shouldSetTaskPositionAndCrop, hasGlobalFocus);
}
Trace.endSection();
}
@@ -392,11 +394,12 @@
/** Run the whole relayout phase immediately without delay. */
private void relayoutInSync(ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
- boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) {
+ boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop,
+ boolean hasGlobalFocus) {
// Clear the current ViewHost runnable as we will update the ViewHost here
clearCurrentViewHostRunnable();
updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT, applyStartTransactionOnDraw,
- shouldSetTaskPositionAndCrop);
+ shouldSetTaskPositionAndCrop, hasGlobalFocus);
if (mResult.mRootView != null) {
updateViewHost(mRelayoutParams, startT, mResult);
}
@@ -418,7 +421,8 @@
*/
private void relayoutWithDelayedViewHost(ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
- boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) {
+ boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop,
+ boolean hasGlobalFocus) {
if (applyStartTransactionOnDraw) {
throw new IllegalArgumentException(
"We cannot both sync viewhost ondraw and delay viewhost creation.");
@@ -426,7 +430,8 @@
// Clear the current ViewHost runnable as we will update the ViewHost here
clearCurrentViewHostRunnable();
updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT,
- false /* applyStartTransactionOnDraw */, shouldSetTaskPositionAndCrop);
+ false /* applyStartTransactionOnDraw */, shouldSetTaskPositionAndCrop,
+ hasGlobalFocus);
if (mResult.mRootView == null) {
// This means something blocks the window decor from showing, e.g. the task is hidden.
// Nothing is set up in this case including the decoration surface.
@@ -440,7 +445,8 @@
@SuppressLint("MissingPermission")
private void updateRelayoutParamsAndSurfaces(ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
- boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) {
+ boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop,
+ boolean hasGlobalFocus) {
Trace.beginSection("DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces");
if (Flags.enableDesktopWindowingAppToWeb()) {
@@ -459,7 +465,8 @@
.isTaskInFullImmersiveState(taskInfo.taskId);
updateRelayoutParams(mRelayoutParams, mContext, taskInfo, applyStartTransactionOnDraw,
shouldSetTaskPositionAndCrop, mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded,
- inFullImmersive, mDisplayController.getInsetsState(taskInfo.displayId));
+ inFullImmersive, mDisplayController.getInsetsState(taskInfo.displayId),
+ hasGlobalFocus);
final WindowDecorLinearLayout oldRootView = mResult.mRootView;
final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
@@ -507,12 +514,13 @@
));
} else {
mWindowDecorViewHolder.bindData(new AppHeaderViewHolder.HeaderData(
- mTaskInfo, TaskInfoKt.getRequestingImmersive(mTaskInfo), inFullImmersive
+ mTaskInfo, TaskInfoKt.getRequestingImmersive(mTaskInfo), inFullImmersive,
+ hasGlobalFocus
));
}
Trace.endSection();
- if (!mTaskInfo.isFocused) {
+ if (!hasGlobalFocus) {
closeHandleMenu();
closeManageWindowsMenu();
closeMaximizeMenu();
@@ -780,7 +788,8 @@
boolean isStatusBarVisible,
boolean isKeyguardVisibleAndOccluded,
boolean inFullImmersiveMode,
- @NonNull InsetsState displayInsetsState) {
+ @NonNull InsetsState displayInsetsState,
+ boolean hasGlobalFocus) {
final int captionLayoutId = getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode());
final boolean isAppHeader =
captionLayoutId == R.layout.desktop_mode_app_header;
@@ -790,6 +799,7 @@
relayoutParams.mLayoutResId = captionLayoutId;
relayoutParams.mCaptionHeightId = getCaptionHeightIdStatic(taskInfo.getWindowingMode());
relayoutParams.mCaptionWidthId = getCaptionWidthId(relayoutParams.mLayoutResId);
+ relayoutParams.mHasGlobalFocus = hasGlobalFocus;
final boolean showCaption;
if (Flags.enableFullyImmersiveInDesktop()) {
@@ -812,7 +822,7 @@
|| (isStatusBarVisible && !isKeyguardVisibleAndOccluded);
}
relayoutParams.mIsCaptionVisible = showCaption;
-
+ relayoutParams.mIsInsetSource = isAppHeader && !inFullImmersiveMode;
if (isAppHeader) {
if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) {
// If the app is requesting to customize the caption bar, allow input to fall
@@ -837,7 +847,6 @@
WindowInsets.Type.systemBars() & ~WindowInsets.Type.captionBar(),
false /* ignoreVisibility */);
relayoutParams.mCaptionTopPadding = systemBarInsets.top;
- relayoutParams.mIsInsetSource = false;
}
// Report occluding elements as bounding rects to the insets system so that apps can
// draw in the empty space in the center:
@@ -865,8 +874,8 @@
relayoutParams.mInputFeatures
|= WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL;
}
- if (DesktopModeStatus.useWindowShadow(/* isFocusedWindow= */ taskInfo.isFocused)) {
- relayoutParams.mShadowRadiusId = taskInfo.isFocused
+ if (DesktopModeStatus.useWindowShadow(/* isFocusedWindow= */ hasGlobalFocus)) {
+ relayoutParams.mShadowRadiusId = hasGlobalFocus
? R.dimen.freeform_decor_shadow_focused_thickness
: R.dimen.freeform_decor_shadow_unfocused_thickness;
}
@@ -1408,7 +1417,7 @@
}
boolean isFocused() {
- return mTaskInfo.isFocused;
+ return mHasGlobalFocus;
}
/**
@@ -1592,7 +1601,7 @@
private static int getCaptionHeightIdStatic(@WindowingMode int windowingMode) {
return windowingMode == WINDOWING_MODE_FULLSCREEN
- ? R.dimen.desktop_mode_fullscreen_decor_caption_height
+ ? com.android.internal.R.dimen.status_bar_height_default
: R.dimen.desktop_mode_freeform_decor_caption_height;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
index f4c7fe3..ccf329c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
@@ -93,7 +93,7 @@
mWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds());
mRepositionStartPoint.set(x, y);
mDragStartListener.onDragStart(mWindowDecoration.mTaskInfo.taskId);
- if (mCtrlType != CTRL_TYPE_UNDEFINED && !mWindowDecoration.mTaskInfo.isFocused) {
+ if (mCtrlType != CTRL_TYPE_UNDEFINED && !mWindowDecoration.mHasGlobalFocus) {
WindowContainerTransaction wct = new WindowContainerTransaction();
wct.reorder(mWindowDecoration.mTaskInfo.token, true /* onTop */,
true /* includingParents */);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
index a1f76d2..ff3b455 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
@@ -106,7 +106,7 @@
// Capture CUJ for re-sizing window in DW mode.
mInteractionJankMonitor.begin(mDesktopWindowDecoration.mTaskSurface,
mDesktopWindowDecoration.mContext, mHandler, CUJ_DESKTOP_MODE_RESIZE_WINDOW);
- if (!mDesktopWindowDecoration.mTaskInfo.isFocused) {
+ if (!mDesktopWindowDecoration.mHasGlobalFocus) {
WindowContainerTransaction wct = new WindowContainerTransaction();
wct.reorder(mDesktopWindowDecoration.mTaskInfo.token, true /* onTop */,
true /* includingParents */);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index f8aed41..ce5cfd0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -125,7 +125,7 @@
}
mDisplayController.removeDisplayWindowListener(this);
- relayout(mTaskInfo);
+ relayout(mTaskInfo, mHasGlobalFocus);
}
};
@@ -146,6 +146,7 @@
boolean mIsStatusBarVisible;
boolean mIsKeyguardVisibleAndOccluded;
+ boolean mHasGlobalFocus;
/** The most recent set of insets applied to this window decoration. */
private WindowDecorationInsets mWindowDecorationInsets;
@@ -199,8 +200,9 @@
*
* @param taskInfo The previous {@link RunningTaskInfo} passed into {@link #relayout} or the
* constructor.
+ * @param hasGlobalFocus Whether the task is focused
*/
- abstract void relayout(RunningTaskInfo taskInfo);
+ abstract void relayout(RunningTaskInfo taskInfo, boolean hasGlobalFocus);
/**
* Used by the {@link DragPositioningCallback} associated with the implementing class to
@@ -225,6 +227,7 @@
if (params.mRunningTaskInfo != null) {
mTaskInfo = params.mRunningTaskInfo;
}
+ mHasGlobalFocus = params.mHasGlobalFocus;
final int oldLayoutResId = mLayoutResId;
mLayoutResId = params.mLayoutResId;
@@ -246,7 +249,7 @@
final Rect taskBounds = mTaskInfo.getConfiguration().windowConfiguration.getBounds();
outResult.mWidth = taskBounds.width();
outResult.mHeight = taskBounds.height();
- outResult.mRootView.setTaskFocusState(mTaskInfo.isFocused);
+ outResult.mRootView.setTaskFocusState(mHasGlobalFocus);
final Resources resources = mDecorWindowContext.getResources();
outResult.mCaptionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId)
+ params.mCaptionTopPadding;
@@ -391,11 +394,11 @@
final WindowDecorationInsets newInsets = new WindowDecorationInsets(
mTaskInfo.token, mOwner, captionInsetsRect, boundingRects,
- params.mInsetSourceFlags);
+ params.mInsetSourceFlags, params.mIsInsetSource);
if (!newInsets.equals(mWindowDecorationInsets)) {
// Add or update this caption as an insets source.
mWindowDecorationInsets = newInsets;
- mWindowDecorationInsets.addOrUpdate(wct);
+ mWindowDecorationInsets.update(wct);
}
}
@@ -512,7 +515,7 @@
mIsKeyguardVisibleAndOccluded = visible && occluded;
final boolean changed = prevVisAndOccluded != mIsKeyguardVisibleAndOccluded;
if (changed) {
- relayout(mTaskInfo);
+ relayout(mTaskInfo, mHasGlobalFocus);
}
}
@@ -522,7 +525,7 @@
final boolean changed = prevStatusBarVisibility != mIsStatusBarVisible;
if (changed) {
- relayout(mTaskInfo);
+ relayout(mTaskInfo, mHasGlobalFocus);
}
}
@@ -710,10 +713,11 @@
final int captionHeight = loadDimensionPixelSize(mContext.getResources(), captionHeightId);
final Rect captionInsets = new Rect(0, 0, 0, captionHeight);
final WindowDecorationInsets newInsets = new WindowDecorationInsets(mTaskInfo.token,
- mOwner, captionInsets, null /* boundingRets */, 0 /* flags */);
+ mOwner, captionInsets, null /* boundingRets */, 0 /* flags */,
+ true /* shouldAddCaptionInset */);
if (!newInsets.equals(mWindowDecorationInsets)) {
mWindowDecorationInsets = newInsets;
- mWindowDecorationInsets.addOrUpdate(wct);
+ mWindowDecorationInsets.update(wct);
}
}
@@ -737,6 +741,7 @@
boolean mApplyStartTransactionOnDraw;
boolean mSetTaskPositionAndCrop;
+ boolean mHasGlobalFocus;
void reset() {
mLayoutResId = Resources.ID_NULL;
@@ -756,6 +761,7 @@
mApplyStartTransactionOnDraw = false;
mSetTaskPositionAndCrop = false;
mWindowDecorConfig = null;
+ mHasGlobalFocus = false;
}
boolean hasInputFeatureSpy() {
@@ -814,21 +820,26 @@
private final Rect mFrame;
private final Rect[] mBoundingRects;
private final @InsetsSource.Flags int mFlags;
+ private final boolean mShouldAddCaptionInset;
private WindowDecorationInsets(WindowContainerToken token, Binder owner, Rect frame,
- Rect[] boundingRects, @InsetsSource.Flags int flags) {
+ Rect[] boundingRects, @InsetsSource.Flags int flags,
+ boolean shouldAddCaptionInset) {
mToken = token;
mOwner = owner;
mFrame = frame;
mBoundingRects = boundingRects;
mFlags = flags;
+ mShouldAddCaptionInset = shouldAddCaptionInset;
}
- void addOrUpdate(WindowContainerTransaction wct) {
- wct.addInsetsSource(mToken, mOwner, INDEX, captionBar(), mFrame, mBoundingRects,
- mFlags);
- wct.addInsetsSource(mToken, mOwner, INDEX, mandatorySystemGestures(), mFrame,
- mBoundingRects, 0 /* flags */);
+ void update(WindowContainerTransaction wct) {
+ if (mShouldAddCaptionInset) {
+ wct.addInsetsSource(mToken, mOwner, INDEX, captionBar(), mFrame, mBoundingRects,
+ mFlags);
+ wct.addInsetsSource(mToken, mOwner, INDEX, mandatorySystemGestures(), mFrame,
+ mBoundingRects, 0 /* flags */);
+ }
}
void remove(WindowContainerTransaction wct) {
@@ -843,7 +854,8 @@
return Objects.equals(mToken, that.mToken) && Objects.equals(mOwner,
that.mOwner) && Objects.equals(mFrame, that.mFrame)
&& Objects.deepEquals(mBoundingRects, that.mBoundingRects)
- && mFlags == that.mFlags;
+ && mFlags == that.mFlags
+ && mShouldAddCaptionInset == that.mShouldAddCaptionInset;
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
index c2af1d4..cf03b3f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -81,6 +81,7 @@
val taskInfo: RunningTaskInfo,
val isRequestingImmersive: Boolean,
val inFullImmersiveState: Boolean,
+ val hasGlobalFocus: Boolean
) : Data()
private val decorThemeUtil = DecorThemeUtil(context)
@@ -159,24 +160,27 @@
}
override fun bindData(data: HeaderData) {
- bindData(data.taskInfo, data.isRequestingImmersive, data.inFullImmersiveState)
+ bindData(data.taskInfo, data.isRequestingImmersive, data.inFullImmersiveState,
+ data.hasGlobalFocus)
}
private fun bindData(
taskInfo: RunningTaskInfo,
isRequestingImmersive: Boolean,
inFullImmersiveState: Boolean,
+ hasGlobalFocus: Boolean
) {
if (DesktopModeFlags.ENABLE_THEMED_APP_HEADERS.isTrue()) {
- bindDataWithThemedHeaders(taskInfo, isRequestingImmersive, inFullImmersiveState)
+ bindDataWithThemedHeaders(taskInfo, isRequestingImmersive, inFullImmersiveState,
+ hasGlobalFocus)
} else {
- bindDataLegacy(taskInfo)
+ bindDataLegacy(taskInfo, hasGlobalFocus)
}
}
- private fun bindDataLegacy(taskInfo: RunningTaskInfo) {
- captionView.setBackgroundColor(getCaptionBackgroundColor(taskInfo))
- val color = getAppNameAndButtonColor(taskInfo)
+ private fun bindDataLegacy(taskInfo: RunningTaskInfo, hasGlobalFocus: Boolean) {
+ captionView.setBackgroundColor(getCaptionBackgroundColor(taskInfo, hasGlobalFocus))
+ val color = getAppNameAndButtonColor(taskInfo, hasGlobalFocus)
val alpha = Color.alpha(color)
closeWindowButton.imageTintList = ColorStateList.valueOf(color)
maximizeWindowButton.imageTintList = ColorStateList.valueOf(color)
@@ -210,9 +214,10 @@
private fun bindDataWithThemedHeaders(
taskInfo: RunningTaskInfo,
requestingImmersive: Boolean,
- inFullImmersiveState: Boolean
+ inFullImmersiveState: Boolean,
+ hasGlobalFocus: Boolean
) {
- val header = fillHeaderInfo(taskInfo)
+ val header = fillHeaderInfo(taskInfo, hasGlobalFocus)
val headerStyle = getHeaderStyle(header)
// Caption Background
@@ -455,7 +460,7 @@
}
}
- private fun fillHeaderInfo(taskInfo: RunningTaskInfo): Header {
+ private fun fillHeaderInfo(taskInfo: RunningTaskInfo, hasGlobalFocus: Boolean): Header {
return Header(
type = if (taskInfo.isTransparentCaptionBarAppearance) {
Header.Type.CUSTOM
@@ -463,7 +468,7 @@
Header.Type.DEFAULT
},
appTheme = decorThemeUtil.getAppTheme(taskInfo),
- isFocused = taskInfo.isFocused,
+ isFocused = hasGlobalFocus,
isAppearanceCaptionLight = taskInfo.isLightCaptionBarAppearance
)
}
@@ -544,19 +549,19 @@
}
@ColorInt
- private fun getCaptionBackgroundColor(taskInfo: RunningTaskInfo): Int {
+ private fun getCaptionBackgroundColor(taskInfo: RunningTaskInfo, hasGlobalFocus: Boolean): Int {
if (taskInfo.isTransparentCaptionBarAppearance) {
return Color.TRANSPARENT
}
val materialColorAttr: Int =
if (isDarkMode()) {
- if (!taskInfo.isFocused) {
+ if (!hasGlobalFocus) {
materialColorSurfaceContainerHigh
} else {
materialColorSurfaceDim
}
} else {
- if (!taskInfo.isFocused) {
+ if (!hasGlobalFocus) {
materialColorSurfaceContainerLow
} else {
materialColorSecondaryContainer
@@ -569,7 +574,7 @@
}
@ColorInt
- private fun getAppNameAndButtonColor(taskInfo: RunningTaskInfo): Int {
+ private fun getAppNameAndButtonColor(taskInfo: RunningTaskInfo, hasGlobalFocus: Boolean): Int {
val materialColorAttr = when {
taskInfo.isTransparentCaptionBarAppearance &&
taskInfo.isLightCaptionBarAppearance -> materialColorOnSecondaryContainer
@@ -579,8 +584,8 @@
else -> materialColorOnSecondaryContainer
}
val appDetailsOpacity = when {
- isDarkMode() && !taskInfo.isFocused -> DARK_THEME_UNFOCUSED_OPACITY
- !isDarkMode() && !taskInfo.isFocused -> LIGHT_THEME_UNFOCUSED_OPACITY
+ isDarkMode() && !hasGlobalFocus -> DARK_THEME_UNFOCUSED_OPACITY
+ !isDarkMode() && !hasGlobalFocus -> LIGHT_THEME_UNFOCUSED_OPACITY
else -> FOCUSED_OPACITY
}
context.withStyledAttributes(null, intArrayOf(materialColorAttr), 0, 0) {
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 2980d51..e176f47 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 @@
@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 2484f67..9b8c949 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.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 @@
@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/DesktopFullImmersiveTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt
index cae6095..2e9effb4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt
@@ -15,23 +15,39 @@
*/
package com.android.wm.shell.desktopmode
+import android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS
+import android.os.Binder
import android.os.IBinder
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
import android.testing.AndroidTestingRunner
+import android.view.Display.DEFAULT_DISPLAY
import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TransitionFlags
+import android.view.WindowManager.TransitionType
+import android.window.TransitionInfo
+import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
import androidx.test.filters.SmallTest
+import com.android.window.flags.Flags
+import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayLayout
import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.Transitions
import com.google.common.truth.Truth.assertThat
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.mock
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
@@ -40,14 +56,18 @@
/**
* Tests for [DesktopFullImmersiveTransitionHandler].
*
- * Usage: atest WMShellUnitTests:DesktopFullImmersiveTransitionHandler
+ * Usage: atest WMShellUnitTests:DesktopFullImmersiveTransitionHandlerTest
*/
@SmallTest
@RunWith(AndroidTestingRunner::class)
class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() {
+ @JvmField @Rule val setFlagsRule = SetFlagsRule()
+
@Mock private lateinit var mockTransitions: Transitions
private lateinit var desktopRepository: DesktopRepository
+ @Mock private lateinit var mockDisplayController: DisplayController
+ @Mock private lateinit var mockShellTaskOrganizer: ShellTaskOrganizer
private val transactionSupplier = { SurfaceControl.Transaction() }
private lateinit var immersiveHandler: DesktopFullImmersiveTransitionHandler
@@ -57,19 +77,22 @@
desktopRepository = DesktopRepository(
context, ShellInit(TestShellExecutor()), mock(), mock()
)
+ whenever(mockDisplayController.getDisplayLayout(DEFAULT_DISPLAY))
+ .thenReturn(DisplayLayout())
immersiveHandler = DesktopFullImmersiveTransitionHandler(
transitions = mockTransitions,
desktopRepository = desktopRepository,
- transactionSupplier = transactionSupplier
+ displayController = mockDisplayController,
+ shellTaskOrganizer = mockShellTaskOrganizer,
+ transactionSupplier = transactionSupplier,
)
}
@Test
fun enterImmersive_transitionReady_updatesRepository() {
val task = createFreeformTask()
- val wct = WindowContainerTransaction()
val mockBinder = mock(IBinder::class.java)
- whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
.thenReturn(mockBinder)
desktopRepository.setTaskInFullImmersiveState(
displayId = task.displayId,
@@ -77,8 +100,8 @@
immersive = false
)
- immersiveHandler.enterImmersive(task, wct)
- immersiveHandler.onTransitionReady(mockBinder)
+ immersiveHandler.moveTaskToImmersive(task)
+ immersiveHandler.onTransitionReady(mockBinder, createTransitionInfo())
assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isTrue()
}
@@ -86,9 +109,8 @@
@Test
fun exitImmersive_transitionReady_updatesRepository() {
val task = createFreeformTask()
- val wct = WindowContainerTransaction()
val mockBinder = mock(IBinder::class.java)
- whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
.thenReturn(mockBinder)
desktopRepository.setTaskInFullImmersiveState(
displayId = task.displayId,
@@ -96,8 +118,8 @@
immersive = true
)
- immersiveHandler.exitImmersive(task, wct)
- immersiveHandler.onTransitionReady(mockBinder)
+ immersiveHandler.moveTaskToNonImmersive(task)
+ immersiveHandler.onTransitionReady(mockBinder, createTransitionInfo())
assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isFalse()
}
@@ -105,28 +127,251 @@
@Test
fun enterImmersive_inProgress_ignores() {
val task = createFreeformTask()
- val wct = WindowContainerTransaction()
val mockBinder = mock(IBinder::class.java)
- whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
.thenReturn(mockBinder)
- immersiveHandler.enterImmersive(task, wct)
- immersiveHandler.enterImmersive(task, wct)
+ immersiveHandler.moveTaskToImmersive(task)
+ immersiveHandler.moveTaskToImmersive(task)
- verify(mockTransitions, times(1)).startTransition(TRANSIT_CHANGE, wct, immersiveHandler)
+ verify(mockTransitions, times(1))
+ .startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler))
}
@Test
fun exitImmersive_inProgress_ignores() {
val task = createFreeformTask()
- val wct = WindowContainerTransaction()
val mockBinder = mock(IBinder::class.java)
- whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
.thenReturn(mockBinder)
- immersiveHandler.exitImmersive(task, wct)
- immersiveHandler.exitImmersive(task, wct)
+ immersiveHandler.moveTaskToNonImmersive(task)
+ immersiveHandler.moveTaskToNonImmersive(task)
- verify(mockTransitions, times(1)).startTransition(TRANSIT_CHANGE, wct, immersiveHandler)
+ verify(mockTransitions, times(1))
+ .startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler))
}
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_inImmersive_addsPendingExit() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = true
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+
+ assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+ exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
+ && exit.taskId == task.taskId
+ }).isTrue()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_notInImmersive_doesNotAddPendingExit() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = false
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+
+ assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+ exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
+ && exit.taskId == task.taskId
+ }).isFalse()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_byDisplay_inImmersive_changesTaskBounds() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = true
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+
+ assertThat(wct.hasBoundsChange(task.token)).isTrue()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_byDisplay_notInImmersive_doesNotChangeTaskBounds() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = false
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+
+ assertThat(wct.hasBoundsChange(task.token)).isFalse()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_byTask_inImmersive_changesTaskBounds() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = true
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(wct = wct, taskInfo = task)
+
+ assertThat(wct.hasBoundsChange(task.token)).isTrue()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_byTask_notInImmersive_doesNotChangeTaskBounds() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = false
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(wct, task.taskId)
+
+ assertThat(wct.hasBoundsChange(task.token)).isFalse()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_byTask_inImmersive_addsPendingExitOnRun() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = true
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(wct, task.taskId)?.invoke(transition)
+
+ assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+ exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
+ && exit.taskId == task.taskId
+ }).isFalse()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_byTask_notInImmersive_doesNotAddPendingExitOnRun() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = false
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(wct, task.taskId)?.invoke(transition)
+
+ assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+ exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
+ && exit.taskId == task.taskId
+ }).isFalse()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun onTransitionReady_pendingExit_removesPendingExit() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = true
+ )
+ immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+
+ immersiveHandler.onTransitionReady(
+ transition = transition,
+ info = createTransitionInfo(
+ changes = listOf(
+ TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task }
+ )
+ )
+ )
+
+ assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+ exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
+ && exit.taskId == task.taskId
+ }).isFalse()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun onTransitionReady_pendingExit_updatesRepository() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = true
+ )
+ immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+
+ immersiveHandler.onTransitionReady(
+ transition = transition,
+ info = createTransitionInfo(
+ changes = listOf(
+ TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task }
+ )
+ )
+ )
+
+ assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isFalse()
+ }
+
+ private fun createTransitionInfo(
+ @TransitionType type: Int = TRANSIT_CHANGE,
+ @TransitionFlags flags: Int = 0,
+ changes: List<TransitionInfo.Change> = emptyList()
+ ): TransitionInfo = TransitionInfo(type, flags).apply {
+ changes.forEach { change -> addChange(change) }
+ }
+
+ private fun WindowContainerTransaction.hasBoundsChange(token: WindowContainerToken): Boolean =
+ this.changes.any { change ->
+ change.key == token.asBinder()
+ && (change.value.windowSetMask and WINDOW_CONFIG_BOUNDS) != 0
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
index 1308114..e20f0ec 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
@@ -957,6 +957,15 @@
assertThat(repo.getActiveTasks(displayId = DEFAULT_DISPLAY)).isEmpty()
}
+ @Test
+ fun getTaskInFullImmersiveState_byDisplay() {
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true)
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID + 1, taskId = 2, immersive = true)
+
+ assertThat(repo.getTaskInFullImmersiveState(DEFAULT_DESKTOP_ID)).isEqualTo(1)
+ assertThat(repo.getTaskInFullImmersiveState(DEFAULT_DESKTOP_ID + 1)).isEqualTo(2)
+ }
+
class TestListener : DesktopRepository.ActiveTasksListener {
var activeChangesOnDefaultDisplay = 0
var activeChangesOnSecondaryDisplay = 0
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 27deb0b..b3c10d6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -42,6 +42,7 @@
import android.os.Binder
import android.os.Bundle
import android.os.Handler
+import android.os.IBinder
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
@@ -99,6 +100,7 @@
import com.android.wm.shell.desktopmode.persistence.Desktop
import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository
import com.android.wm.shell.draganddrop.DragAndDropController
+import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
import com.android.wm.shell.recents.RecentTasksController
import com.android.wm.shell.recents.RecentsTransitionHandler
import com.android.wm.shell.recents.RecentsTransitionStateListener
@@ -144,13 +146,11 @@
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.Mockito.times
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
-import org.mockito.kotlin.argThat
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.capture
import org.mockito.kotlin.eq
@@ -201,6 +201,7 @@
private lateinit var mockInteractionJankMonitor: InteractionJankMonitor
@Mock private lateinit var mockSurface: SurfaceControl
@Mock private lateinit var taskbarDesktopTaskListener: TaskbarDesktopTaskListener
+ @Mock private lateinit var freeformTaskTransitionStarter: FreeformTaskTransitionStarter
@Mock private lateinit var mockHandler: Handler
@Mock lateinit var persistentRepository: DesktopPersistentRepository
@@ -266,6 +267,7 @@
controller = createController()
controller.setSplitScreenController(splitScreenController)
+ controller.freeformTaskTransitionStarter = freeformTaskTransitionStarter
shellInit.init()
@@ -1542,75 +1544,142 @@
}
@Test
- fun onDesktopWindowMinimize_noActiveTask_doesntUpdateTransaction() {
- val wct = WindowContainerTransaction()
- controller.onDesktopWindowMinimize(wct, taskId = 1)
- // Nothing happens.
- assertThat(wct.hierarchyOps).isEmpty()
+ fun onDesktopWindowMinimize_noActiveTask_doesntRemoveWallpaper() {
+ val task = setUpFreeformTask(active = false)
+ val transition = Binder()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
+ val wallpaperToken = MockToken().token()
+ taskRepository.wallpaperActivityToken = wallpaperToken
+
+ controller.minimizeTask(task)
+
+ val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+ captor.value.hierarchyOps.none { hop ->
+ hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
+ }
}
@Test
- fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntUpdateTransaction() {
- val task = setUpFreeformTask()
- val wct = WindowContainerTransaction()
- controller.onDesktopWindowMinimize(wct, taskId = task.taskId)
- // Nothing happens.
- assertThat(wct.hierarchyOps).isEmpty()
+ fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntRemoveWallpaper() {
+ val task = setUpFreeformTask(active = true)
+ val transition = Binder()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
+
+ controller.minimizeTask(task)
+
+ val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+ captor.value.hierarchyOps.none { hop ->
+ hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK
+ }
}
@Test
fun onDesktopWindowMinimize_singleActiveTask_hasWallpaperActivityToken_removesWallpaper() {
val task = setUpFreeformTask()
+ val transition = Binder()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
val wallpaperToken = MockToken().token()
taskRepository.wallpaperActivityToken = wallpaperToken
- val wct = WindowContainerTransaction()
// The only active task is being minimized.
- controller.onDesktopWindowMinimize(wct, taskId = task.taskId)
+ controller.minimizeTask(task)
+
+ val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
// Adds remove wallpaper operation
- wct.assertRemoveAt(index = 0, wallpaperToken)
+ captor.value.assertRemoveAt(index = 0, wallpaperToken)
}
@Test
- fun onDesktopWindowMinimize_singleActiveTask_alreadyMinimized_doesntUpdateTransaction() {
+ fun onDesktopWindowMinimize_singleActiveTask_alreadyMinimized_doesntRemoveWallpaper() {
val task = setUpFreeformTask()
+ val transition = Binder()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
val wallpaperToken = MockToken().token()
taskRepository.wallpaperActivityToken = wallpaperToken
taskRepository.minimizeTask(DEFAULT_DISPLAY, task.taskId)
- val wct = WindowContainerTransaction()
// The only active task is already minimized.
- controller.onDesktopWindowMinimize(wct, taskId = task.taskId)
- // Doesn't modify transaction
- assertThat(wct.hierarchyOps).isEmpty()
+ controller.minimizeTask(task)
+
+ val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+ captor.value.hierarchyOps.none { hop ->
+ hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
+ }
}
@Test
- fun onDesktopWindowMinimize_multipleActiveTasks_doesntUpdateTransaction() {
- val task1 = setUpFreeformTask()
- setUpFreeformTask()
+ fun onDesktopWindowMinimize_multipleActiveTasks_doesntRemoveWallpaper() {
+ val task1 = setUpFreeformTask(active = true)
+ setUpFreeformTask(active = true)
+ val transition = Binder()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
val wallpaperToken = MockToken().token()
taskRepository.wallpaperActivityToken = wallpaperToken
- val wct = WindowContainerTransaction()
- controller.onDesktopWindowMinimize(wct, taskId = task1.taskId)
- // Doesn't modify transaction
- assertThat(wct.hierarchyOps).isEmpty()
+ controller.minimizeTask(task1)
+
+ val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+ captor.value.hierarchyOps.none { hop ->
+ hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
+ }
}
@Test
fun onDesktopWindowMinimize_multipleActiveTasks_minimizesTheOnlyVisibleTask_removesWallpaper() {
- val task1 = setUpFreeformTask()
- val task2 = setUpFreeformTask()
+ val task1 = setUpFreeformTask(active = true)
+ val task2 = setUpFreeformTask(active = true)
+ val transition = Binder()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
val wallpaperToken = MockToken().token()
taskRepository.wallpaperActivityToken = wallpaperToken
taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId)
- val wct = WindowContainerTransaction()
// task1 is the only visible task as task2 is minimized.
- controller.onDesktopWindowMinimize(wct, taskId = task1.taskId)
+ controller.minimizeTask(task1)
// Adds remove wallpaper operation
- wct.assertRemoveAt(index = 0, wallpaperToken)
+ val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+ // Adds remove wallpaper operation
+ captor.value.assertRemoveAt(index = 0, wallpaperToken)
+ }
+
+ @Test
+ fun onDesktopWindowMinimize_triesToExitImmersive() {
+ val task = setUpFreeformTask()
+ val transition = Binder()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
+
+ controller.minimizeTask(task)
+
+ verify(mockDesktopFullImmersiveTransitionHandler).exitImmersiveIfApplicable(any(), eq(task))
+ }
+
+ @Test
+ fun onDesktopWindowMinimize_invokesImmersiveTransitionStartCallback() {
+ val task = setUpFreeformTask()
+ val transition = Binder()
+ val runOnTransit = RunOnStartTransitionCallback()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
+ whenever(mockDesktopFullImmersiveTransitionHandler.exitImmersiveIfApplicable(any(), eq(task)))
+ .thenReturn(runOnTransit)
+
+ controller.minimizeTask(task)
+
+ assertThat(runOnTransit.invocations).isEqualTo(1)
+ assertThat(runOnTransit.lastInvoked).isEqualTo(transition)
}
@Test
@@ -3166,27 +3235,23 @@
}
@Test
- fun toggleImmersive_enter_resizesToDisplayBounds() {
+ fun toggleImmersive_enter_movesToImmersive() {
val task = setUpFreeformTask(DEFAULT_DISPLAY)
taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, false /* immersive */)
controller.toggleDesktopTaskFullImmersiveState(task)
- verify(mockDesktopFullImmersiveTransitionHandler).enterImmersive(eq(task), argThat { wct ->
- wct.hasBoundsChange(task.token, Rect())
- })
+ verify(mockDesktopFullImmersiveTransitionHandler).moveTaskToImmersive(task)
}
@Test
- fun toggleImmersive_exit_resizesToStableBounds() {
+ fun toggleImmersive_exit_movesToNonImmersive() {
val task = setUpFreeformTask(DEFAULT_DISPLAY)
taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, true /* immersive */)
controller.toggleDesktopTaskFullImmersiveState(task)
- verify(mockDesktopFullImmersiveTransitionHandler).exitImmersive(eq(task), argThat { wct ->
- wct.hasBoundsChange(task.token, STABLE_BOUNDS)
- })
+ verify(mockDesktopFullImmersiveTransitionHandler).moveTaskToNonImmersive(task)
}
@Test
@@ -3198,7 +3263,7 @@
task.requestedVisibleTypes = WindowInsets.Type.statusBars()
controller.onTaskInfoChanged(task)
- verify(mockDesktopFullImmersiveTransitionHandler).exitImmersive(eq(task), any())
+ verify(mockDesktopFullImmersiveTransitionHandler).moveTaskToNonImmersive(task)
}
@Test
@@ -3210,7 +3275,113 @@
task.requestedVisibleTypes = WindowInsets.Type.statusBars()
controller.onTaskInfoChanged(task)
- verify(mockDesktopFullImmersiveTransitionHandler, never()).exitImmersive(eq(task), any())
+ verify(mockDesktopFullImmersiveTransitionHandler, never()).moveTaskToNonImmersive(task)
+ }
+
+ @Test
+ fun moveTaskToDesktop_background_attemptsImmersiveExit() {
+ val task = setUpFreeformTask(background = true)
+ val wct = WindowContainerTransaction()
+ val runOnStartTransit = RunOnStartTransitionCallback()
+ val transition = Binder()
+ whenever(mockDesktopFullImmersiveTransitionHandler
+ .exitImmersiveIfApplicable(wct, task.displayId)).thenReturn(runOnStartTransit)
+ whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition)
+
+ controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN)
+
+ verify(mockDesktopFullImmersiveTransitionHandler).exitImmersiveIfApplicable(wct, task.displayId)
+ runOnStartTransit.assertOnlyInvocation(transition)
+ }
+
+ @Test
+ fun moveTaskToDesktop_foreground_attemptsImmersiveExit() {
+ val task = setUpFreeformTask(background = false)
+ val wct = WindowContainerTransaction()
+ val runOnStartTransit = RunOnStartTransitionCallback()
+ val transition = Binder()
+ whenever(mockDesktopFullImmersiveTransitionHandler
+ .exitImmersiveIfApplicable(wct, task.displayId)).thenReturn(runOnStartTransit)
+ whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition)
+
+ controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN)
+
+ verify(mockDesktopFullImmersiveTransitionHandler).exitImmersiveIfApplicable(wct, task.displayId)
+ runOnStartTransit.assertOnlyInvocation(transition)
+ }
+
+ @Test
+ fun moveTaskToFront_background_attemptsImmersiveExit() {
+ val task = setUpFreeformTask(background = true)
+ val runOnStartTransit = RunOnStartTransitionCallback()
+ val transition = Binder()
+ whenever(mockDesktopFullImmersiveTransitionHandler
+ .exitImmersiveIfApplicable(any(), eq(task.displayId))).thenReturn(runOnStartTransit)
+ whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition)
+
+ controller.moveTaskToFront(task.taskId)
+
+ verify(mockDesktopFullImmersiveTransitionHandler)
+ .exitImmersiveIfApplicable(any(), eq(task.displayId))
+ runOnStartTransit.assertOnlyInvocation(transition)
+ }
+
+ @Test
+ fun moveTaskToFront_foreground_attemptsImmersiveExit() {
+ val task = setUpFreeformTask(background = false)
+ val runOnStartTransit = RunOnStartTransitionCallback()
+ val transition = Binder()
+ whenever(mockDesktopFullImmersiveTransitionHandler
+ .exitImmersiveIfApplicable(any(), eq(task.displayId))).thenReturn(runOnStartTransit)
+ whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition)
+
+ controller.moveTaskToFront(task.taskId)
+
+ verify(mockDesktopFullImmersiveTransitionHandler)
+ .exitImmersiveIfApplicable(any(), eq(task.displayId))
+ runOnStartTransit.assertOnlyInvocation(transition)
+ }
+
+ @Test
+ fun handleRequest_freeformLaunchToDesktop_attemptsImmersiveExit() {
+ markTaskVisible(setUpFreeformTask())
+ val task = setUpFreeformTask()
+ markTaskVisible(task)
+ val binder = Binder()
+
+ controller.handleRequest(binder, createTransition(task))
+
+ verify(mockDesktopFullImmersiveTransitionHandler)
+ .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId))
+ }
+
+ @Test
+ fun handleRequest_fullscreenLaunchToDesktop_attemptsImmersiveExit() {
+ setUpFreeformTask()
+ val task = setUpFullscreenTask()
+ val binder = Binder()
+
+ controller.handleRequest(binder, createTransition(task))
+
+ verify(mockDesktopFullImmersiveTransitionHandler)
+ .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId))
+ }
+
+ private class RunOnStartTransitionCallback : ((IBinder) -> Unit) {
+ var invocations = 0
+ private set
+ var lastInvoked: IBinder? = null
+ private set
+
+ override fun invoke(transition: IBinder) {
+ invocations++
+ lastInvoked = transition
+ }
+ }
+
+ private fun RunOnStartTransitionCallback.assertOnlyInvocation(transition: IBinder) {
+ assertThat(invocations).isEqualTo(1)
+ assertThat(lastInvoked).isEqualTo(transition)
}
/**
@@ -3291,18 +3462,27 @@
private fun setUpFreeformTask(
displayId: Int = DEFAULT_DISPLAY,
bounds: Rect? = null,
- active: Boolean = true
+ active: Boolean = true,
+ background: Boolean = false,
): RunningTaskInfo {
val task = createFreeformTask(displayId, bounds)
val activityInfo = ActivityInfo()
task.topActivityInfo = activityInfo
- whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ if (background) {
+ whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null)
+ whenever(recentTasksController.findTaskInBackground(task.taskId))
+ .thenReturn(createTaskInfo(task.taskId))
+ } else {
+ whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ }
if (active) {
taskRepository.addActiveTask(displayId, task.taskId)
taskRepository.updateTaskVisibility(displayId, task.taskId, visible = true)
}
taskRepository.addOrMoveFreeformTaskToTop(displayId, task.taskId)
- runningTasks.add(task)
+ if (!background) {
+ runningTasks.add(task)
+ }
return task
}
@@ -3556,6 +3736,21 @@
assertThat(op.container).isEqualTo(token.asBinder())
}
+private fun WindowContainerTransaction.assertNoRemoveAt(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.hasRemoveAt(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.assertPendingIntentAt(index: Int, intent: Intent) {
assertIndexInBounds(index)
val op = hierarchyOps[index]
@@ -3578,13 +3773,6 @@
.isEqualTo(windowingMode)
}
-private fun WindowContainerTransaction.hasBoundsChange(
- token: WindowContainerToken,
- bounds: Rect
-): Boolean = this.changes.any { change ->
- change.key == token.asBinder() && change.value.configuration.windowConfiguration.bounds == bounds
-}
-
private fun WindowContainerTransaction?.anyDensityConfigChange(
token: WindowContainerToken
): Boolean {
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 598df34..fe87aa8 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.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 @@
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 @@
}
}
+ 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/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
index 145819f3..7ae0bcd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
@@ -329,7 +329,7 @@
mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
- verify(mDesktopFullImmersiveTransitionHandler).onTransitionReady(transition);
+ verify(mDesktopFullImmersiveTransitionHandler).onTransitionReady(transition, info);
}
private static TransitionInfo.Change createChange(int mode, int taskId, int windowingMode) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt
index 0f16b9d..5ebf517 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt
@@ -49,7 +49,8 @@
false,
true /* isStatusBarVisible */,
false /* isKeyguardVisibleAndOccluded */,
- InsetsState()
+ InsetsState(),
+ true /* hasGlobalFocus */
)
Truth.assertThat(relayoutParams.hasInputFeatureSpy()).isTrue()
@@ -70,7 +71,8 @@
false,
true /* isStatusBarVisible */,
false /* isKeyguardVisibleAndOccluded */,
- InsetsState()
+ InsetsState(),
+ true /* hasGlobalFocus */
)
Truth.assertThat(relayoutParams.hasInputFeatureSpy()).isFalse()
@@ -87,7 +89,8 @@
false,
true /* isStatusBarVisible */,
false /* isKeyguardVisibleAndOccluded */,
- InsetsState()
+ InsetsState(),
+ true /* hasGlobalFocus */
)
Truth.assertThat(relayoutParams.mOccludingCaptionElements.size).isEqualTo(2)
Truth.assertThat(relayoutParams.mOccludingCaptionElements[0].mAlignment).isEqualTo(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index 4aa7e18..175fbd2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -100,6 +100,7 @@
import com.android.wm.shell.sysui.ShellCommandHandler
import com.android.wm.shell.sysui.ShellController
import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.FocusTransitionObserver
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeKeyguardChangeListener
import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener
@@ -126,7 +127,6 @@
import org.mockito.Mockito.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.any
-import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.argThat
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doNothing
@@ -192,6 +192,7 @@
DesktopModeWindowDecorViewModel.TaskPositionerFactory
@Mock private lateinit var mockTaskPositioner: TaskPositioner
@Mock private lateinit var mockAppHandleEducationController: AppHandleEducationController
+ @Mock private lateinit var mockFocusTransitionObserver: FocusTransitionObserver
@Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository
private lateinit var spyContext: TestableContext
@@ -254,7 +255,8 @@
mockAppHandleEducationController,
mockCaptionHandleRepository,
Optional.of(mockActivityOrientationChangeHandler),
- mockTaskPositionerFactory
+ mockTaskPositionerFactory,
+ mockFocusTransitionObserver
)
desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController)
whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout)
@@ -455,24 +457,13 @@
onClickListenerCaptor.value.onClick(view)
- val transactionCaptor = argumentCaptor<WindowContainerTransaction>()
- verify(mockFreeformTaskTransitionStarter)
- .startMinimizedModeTransition(transactionCaptor.capture())
- val wct = transactionCaptor.firstValue
-
- verify(mockTasksLimiter).addPendingMinimizeChange(
- anyOrNull(), eq(DEFAULT_DISPLAY), eq(decor.mTaskInfo.taskId))
-
- assertEquals(1, wct.getHierarchyOps().size)
- assertEquals(HierarchyOp.HIERARCHY_OP_TYPE_REORDER, wct.getHierarchyOps().get(0).getType())
- assertFalse(wct.getHierarchyOps().get(0).getToTop())
- assertEquals(decor.mTaskInfo.token.asBinder(), wct.getHierarchyOps().get(0).getContainer())
+ verify(mockDesktopTasksController).minimizeTask(decor.mTaskInfo)
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
fun testDecorationIsCreatedForTopTranslucentActivitiesWithStyleFloating() {
- val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true).apply {
+ val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN).apply {
isTopActivityTransparent = true
isTopActivityStyleFloating = true
numActivities = 1
@@ -487,7 +478,7 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
fun testDecorationIsNotCreatedForTopTranslucentActivitiesWithoutStyleFloating() {
- val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true).apply {
+ val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN).apply {
isTopActivityTransparent = true
isTopActivityStyleFloating = false
numActivities = 1
@@ -500,7 +491,7 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
fun testDecorationIsNotCreatedForSystemUIActivities() {
- val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true)
+ val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN)
// Set task as systemUI package
val systemUIPackageName = context.resources.getString(
@@ -573,7 +564,7 @@
// Simulate default enforce device restrictions system property
whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
- val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true)
+ val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN)
// Simulate device that doesn't support desktop mode
doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
@@ -589,7 +580,7 @@
// Simulate device that doesn't support desktop mode
doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true)
+ val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN)
setUpMockDecorationsForTasks(task)
onTaskOpening(task)
@@ -602,7 +593,7 @@
// Simulate default enforce device restrictions system property
whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
- val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true)
+ val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN)
doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
setUpMockDecorationsForTasks(task)
@@ -1045,7 +1036,7 @@
@Test
fun testOnDisplayRotation_tasksOutOfValidArea_taskBoundsUpdated() {
- val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM)
+ val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
val secondTask =
createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM)
val thirdTask =
@@ -1073,7 +1064,7 @@
@Test
fun testOnDisplayRotation_taskInValidArea_taskBoundsNotUpdated() {
- val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM)
+ val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
val secondTask =
createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM)
val thirdTask =
@@ -1100,7 +1091,7 @@
@Test
fun testOnDisplayRotation_sameOrientationRotation_taskBoundsNotUpdated() {
- val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM)
+ val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
val secondTask =
createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM)
val thirdTask =
@@ -1124,7 +1115,7 @@
@Test
fun testOnDisplayRotation_differentDisplayId_taskBoundsNotUpdated() {
- val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM)
+ val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
val secondTask = createTask(displayId = -2, windowingMode = WINDOWING_MODE_FREEFORM)
val thirdTask = createTask(displayId = -3, windowingMode = WINDOWING_MODE_FREEFORM)
@@ -1149,7 +1140,7 @@
@Test
fun testOnDisplayRotation_nonFreeformTask_taskBoundsNotUpdated() {
- val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM)
+ val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
val secondTask = createTask(displayId = -2, windowingMode = WINDOWING_MODE_FULLSCREEN)
val thirdTask = createTask(displayId = -3, windowingMode = WINDOWING_MODE_PINNED)
@@ -1322,7 +1313,6 @@
displayId: Int = DEFAULT_DISPLAY,
@WindowingMode windowingMode: Int,
activityType: Int = ACTIVITY_TYPE_STANDARD,
- focused: Boolean = true,
activityInfo: ActivityInfo = ActivityInfo(),
requestingImmersive: Boolean = false
): RunningTaskInfo {
@@ -1333,7 +1323,6 @@
.setActivityType(activityType)
.build().apply {
topActivityInfo = activityInfo
- isFocused = focused
isResizeable = true
requestedVisibleTypes = if (requestingImmersive) {
statusBars().inv()
@@ -1351,7 +1340,6 @@
any(), any(), any(), any(), any(), any(), any())
).thenReturn(decoration)
decoration.mTaskInfo = task
- whenever(decoration.isFocused).thenReturn(task.isFocused)
whenever(decoration.user).thenReturn(mockUserHandle)
if (task.windowingMode == WINDOWING_MODE_MULTI_WINDOW) {
whenever(mockSplitScreenController.isTaskInSplitScreen(task.taskId))
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index 35be80e..1d11d2e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -279,7 +279,7 @@
final DesktopModeWindowDecoration spyWindowDecor =
spy(createWindowDecoration(taskInfo));
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, false /* hasGlobalFocus */);
// Menus should close if open before the task being invisible causes relayout to return.
verify(spyWindowDecor).closeHandleMenu();
@@ -298,7 +298,8 @@
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.mShadowRadiusId).isNotEqualTo(Resources.ID_NULL);
}
@@ -318,7 +319,8 @@
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.mCornerRadius).isGreaterThan(0);
}
@@ -343,7 +345,8 @@
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.mWindowDecorConfig.densityDpi).isEqualTo(customTaskDensity);
}
@@ -369,7 +372,8 @@
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.mWindowDecorConfig.densityDpi).isEqualTo(systemDensity);
}
@@ -391,7 +395,8 @@
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.hasInputFeatureSpy()).isTrue();
}
@@ -412,7 +417,8 @@
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.hasInputFeatureSpy()).isFalse();
}
@@ -432,7 +438,8 @@
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.hasInputFeatureSpy()).isFalse();
}
@@ -452,7 +459,8 @@
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(hasNoInputChannelFeature(relayoutParams)).isFalse();
}
@@ -473,7 +481,8 @@
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(hasNoInputChannelFeature(relayoutParams)).isTrue();
}
@@ -494,7 +503,8 @@
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(hasNoInputChannelFeature(relayoutParams)).isTrue();
}
@@ -516,7 +526,8 @@
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) != 0).isTrue();
}
@@ -539,7 +550,8 @@
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) == 0).isTrue();
}
@@ -560,7 +572,8 @@
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(
(relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR) != 0)
@@ -583,7 +596,8 @@
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(
(relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR) == 0)
@@ -612,7 +626,8 @@
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ true,
- insetsState);
+ insetsState,
+ /* hasGlobalFocus= */ true);
// Takes status bar inset as padding, ignores caption bar inset.
assertThat(relayoutParams.mCaptionTopPadding).isEqualTo(50);
@@ -634,7 +649,8 @@
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ true,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.mIsInsetSource).isFalse();
}
@@ -655,7 +671,8 @@
/* isStatusBarVisible */ false,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
// Header is always shown because it's assumed the status bar is always visible.
assertThat(relayoutParams.mIsCaptionVisible).isTrue();
@@ -676,7 +693,8 @@
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.mIsCaptionVisible).isTrue();
}
@@ -696,7 +714,8 @@
/* isStatusBarVisible */ false,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.mIsCaptionVisible).isFalse();
}
@@ -716,7 +735,8 @@
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ true,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.mIsCaptionVisible).isFalse();
}
@@ -737,7 +757,8 @@
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ true,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.mIsCaptionVisible).isTrue();
@@ -750,7 +771,8 @@
/* isStatusBarVisible */ false,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ true,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.mIsCaptionVisible).isFalse();
}
@@ -771,7 +793,8 @@
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ true,
/* inFullImmersiveMode */ true,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.mIsCaptionVisible).isFalse();
}
@@ -782,7 +805,7 @@
final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockTransaction).apply();
verify(mMockRootSurfaceControl, never()).applyTransactionOnDraw(any());
@@ -797,7 +820,7 @@
// Make non-resizable to avoid dealing with input-permissions (MONITOR_INPUT)
taskInfo.isResizeable = false;
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockTransaction, never()).apply();
verify(mMockRootSurfaceControl).applyTransactionOnDraw(mMockTransaction);
@@ -809,7 +832,7 @@
final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockSurfaceControlViewHostFactory, never()).create(any(), any(), any());
}
@@ -821,7 +844,7 @@
taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
// Once for view host, the other for the AppHandle input layer.
verify(mMockHandler, times(2)).post(runnableArgument.capture());
@@ -838,7 +861,7 @@
// Make non-resizable to avoid dealing with input-permissions (MONITOR_INPUT)
taskInfo.isResizeable = false;
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockSurfaceControlViewHostFactory).create(any(), any(), any());
verify(mMockHandler, never()).post(any());
@@ -850,11 +873,11 @@
final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
// Once for view host, the other for the AppHandle input layer.
verify(mMockHandler, times(2)).post(runnableArgument.capture());
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockHandler).removeCallbacks(runnableArgument.getValue());
}
@@ -865,7 +888,7 @@
final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
// Once for view host, the other for the AppHandle input layer.
verify(mMockHandler, times(2)).post(runnableArgument.capture());
@@ -998,7 +1021,7 @@
runnableArgument.getValue().run();
// Relayout decor with same captured link
- decor.relayout(taskInfo);
+ decor.relayout(taskInfo, true /* hasGlobalFocus */);
// Verify handle menu's browser link not set to captured link since link is expired
createHandleMenu(decor);
@@ -1147,7 +1170,7 @@
final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockCaptionHandleRepository, never()).notifyCaptionChanged(any());
}
@@ -1164,7 +1187,7 @@
ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
CaptionState.class);
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
captionStateArgumentCaptor.capture());
@@ -1191,7 +1214,7 @@
ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
CaptionState.class);
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockAppHeaderViewHolder, atLeastOnce()).runOnAppChipGlobalLayout(
runnableArgumentCaptor.capture());
runnableArgumentCaptor.getValue().invoke();
@@ -1214,7 +1237,7 @@
ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
CaptionState.class);
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, false /* hasGlobalFocus */);
verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
captionStateArgumentCaptor.capture());
@@ -1234,7 +1257,7 @@
ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
CaptionState.class);
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
createHandleMenu(spyWindowDecor);
verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
@@ -1259,7 +1282,7 @@
ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
CaptionState.class);
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
createHandleMenu(spyWindowDecor);
spyWindowDecor.closeHandleMenu();
@@ -1356,7 +1379,7 @@
windowDecor.setOpenInBrowserClickListener(mMockOpenInBrowserClickListener);
windowDecor.mDecorWindowContext = mContext;
if (relayout) {
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
}
return windowDecor;
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
index 7543fed..ca1f9ab 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
@@ -624,7 +624,7 @@
@Test
fun testDragResize_resize_resizingTaskReorderedToTopWhenNotFocused() {
- mockWindowDecoration.mTaskInfo.isFocused = false
+ mockWindowDecoration.mHasGlobalFocus = false
taskPositioner.onDragPositioningStart(
CTRL_TYPE_RIGHT, // Resize right
STARTING_BOUNDS.left.toFloat(),
@@ -640,7 +640,7 @@
@Test
fun testDragResize_resize_resizingTaskNotReorderedToTopWhenFocused() {
- mockWindowDecoration.mTaskInfo.isFocused = true
+ mockWindowDecoration.mHasGlobalFocus = true
taskPositioner.onDragPositioningStart(
CTRL_TYPE_RIGHT, // Resize right
STARTING_BOUNDS.left.toFloat(),
@@ -656,7 +656,7 @@
@Test
fun testDragResize_drag_draggedTaskNotReorderedToTop() {
- mockWindowDecoration.mTaskInfo.isFocused = false
+ mockWindowDecoration.mHasGlobalFocus = false
taskPositioner.onDragPositioningStart(
CTRL_TYPE_UNDEFINED, // drag
STARTING_BOUNDS.left.toFloat(),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
index 1273ee8..1dfbd67 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
@@ -323,7 +323,7 @@
@Test
fun testDragResize_resize_resizingTaskReorderedToTopWhenNotFocused() = runOnUiThread {
- mockDesktopWindowDecoration.mTaskInfo.isFocused = false
+ mockDesktopWindowDecoration.mHasGlobalFocus = false
taskPositioner.onDragPositioningStart(
CTRL_TYPE_RIGHT, // Resize right
STARTING_BOUNDS.left.toFloat(),
@@ -339,7 +339,7 @@
@Test
fun testDragResize_resize_resizingTaskNotReorderedToTopWhenFocused() = runOnUiThread {
- mockDesktopWindowDecoration.mTaskInfo.isFocused = true
+ mockDesktopWindowDecoration.mHasGlobalFocus = true
taskPositioner.onDragPositioningStart(
CTRL_TYPE_RIGHT, // Resize right
STARTING_BOUNDS.left.toFloat(),
@@ -355,7 +355,7 @@
@Test
fun testDragResize_drag_draggedTaskNotReorderedToTop() = runOnUiThread {
- mockDesktopWindowDecoration.mTaskInfo.isFocused = false
+ mockDesktopWindowDecoration.mHasGlobalFocus = false
taskPositioner.onDragPositioningStart(
CTRL_TYPE_UNDEFINED, // drag
STARTING_BOUNDS.left.toFloat(),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index 54dd15ba..bb41e9c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -203,13 +203,12 @@
.setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
.setVisible(false)
.build();
- taskInfo.isFocused = false;
// Density is 2. Shadow radius is 10px. Caption height is 64px.
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, false /* hasGlobalFocus */);
verify(decorContainerSurfaceBuilder, never()).build();
verify(taskBackgroundSurfaceBuilder, never()).build();
@@ -243,13 +242,12 @@
.setVisible(true)
.setWindowingMode(WINDOWING_MODE_FREEFORM)
.build();
- taskInfo.isFocused = true;
// Density is 2. Shadow radius is 10px. Caption height is 64px.
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
mRelayoutParams.mIsCaptionVisible = true;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(decorContainerSurfaceBuilder).setParent(mMockTaskSurface);
verify(decorContainerSurfaceBuilder).setContainerLayer();
@@ -316,14 +314,13 @@
.setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
.setVisible(true)
.build();
- taskInfo.isFocused = true;
// Density is 2. Shadow radius is 10px. Caption height is 64px.
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
mRelayoutParams.mIsCaptionVisible = true;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockSurfaceControlViewHost, never()).release();
verify(t, never()).apply();
@@ -333,7 +330,7 @@
final SurfaceControl.Transaction t2 = mock(SurfaceControl.Transaction.class);
mMockSurfaceControlTransactions.add(t2);
taskInfo.isVisible = false;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, false /* hasGlobalFocus */);
final InOrder releaseOrder = inOrder(t2, mMockSurfaceControlViewHost);
releaseOrder.verify(mMockSurfaceControlViewHost).release();
@@ -361,7 +358,7 @@
.build();
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
// It shouldn't show the window decoration when it can't obtain the display instance.
assertThat(mRelayoutResult.mRootView).isNull();
@@ -417,10 +414,9 @@
.setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
.setVisible(true)
.build();
- taskInfo.isFocused = true;
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
final SurfaceControl additionalWindowSurface = mock(SurfaceControl.class);
final SurfaceControl.Builder additionalWindowSurfaceBuilder =
@@ -470,11 +466,10 @@
.setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
.setVisible(true)
.build();
- taskInfo.isFocused = true;
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface);
verify(captionContainerSurfaceBuilder).setContainerLayer();
@@ -510,11 +505,11 @@
.setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
.setVisible(true)
.build();
- taskInfo.isFocused = true;
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
- windowDecor.relayout(taskInfo, true /* applyStartTransactionOnDraw */);
+ windowDecor.relayout(taskInfo, true /* applyStartTransactionOnDraw */,
+ true /* hasGlobalFocus */);
verify(mMockRootSurfaceControl).applyTransactionOnDraw(mMockSurfaceControlStartT);
}
@@ -549,10 +544,9 @@
.setVisible(true)
.setWindowingMode(WINDOWING_MODE_FREEFORM)
.build();
- taskInfo.isFocused = true;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockSurfaceControlStartT).setColor(mMockTaskSurface, new float[]{1.f, 1.f, 0.f});
@@ -575,7 +569,7 @@
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
mRelayoutParams.mIsCaptionVisible = true;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(),
eq(0) /* index */, eq(captionBar()), any(), any(), anyInt());
@@ -611,10 +605,9 @@
.setVisible(true)
.setWindowingMode(WINDOWING_MODE_FULLSCREEN)
.build();
- taskInfo.isFocused = true;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockSurfaceControlStartT).unsetColor(mMockTaskSurface);
@@ -635,7 +628,7 @@
// Hidden from the beginning, so no insets were ever added.
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
mRelayoutParams.mIsCaptionVisible = false;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
// Never added.
verify(mMockWindowContainerTransaction, never()).addInsetsSource(eq(taskInfo.token), any(),
@@ -663,7 +656,7 @@
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
mRelayoutParams.mIsInsetSource = false;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
// Never added.
verify(mMockWindowContainerTransaction, never()).addInsetsSource(eq(taskInfo.token), any(),
@@ -687,11 +680,11 @@
mRelayoutParams.mIsCaptionVisible = true;
mRelayoutParams.mIsInsetSource = true;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
mRelayoutParams.mIsCaptionVisible = true;
mRelayoutParams.mIsInsetSource = false;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
// Insets should be removed.
verify(mMockWindowContainerTransaction).removeInsetsSource(eq(taskInfo.token), any(),
@@ -715,7 +708,7 @@
// Relayout will add insets.
mRelayoutParams.mIsCaptionVisible = true;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(),
eq(0) /* index */, eq(captionBar()), any(), any(), anyInt());
verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(),
@@ -768,10 +761,10 @@
final ActivityManager.RunningTaskInfo firstTaskInfo =
builder.setToken(token).setBounds(new Rect(0, 0, 1000, 1000)).build();
final TestWindowDecoration windowDecor = createWindowDecoration(firstTaskInfo);
- windowDecor.relayout(firstTaskInfo);
+ windowDecor.relayout(firstTaskInfo, true /* hasGlobalFocus */);
final ActivityManager.RunningTaskInfo secondTaskInfo =
builder.setToken(token).setBounds(new Rect(50, 50, 1000, 1000)).build();
- windowDecor.relayout(secondTaskInfo);
+ windowDecor.relayout(secondTaskInfo, true /* hasGlobalFocus */);
// Insets should be applied twice.
verify(mMockWindowContainerTransaction, times(2)).addInsetsSource(eq(token), any(),
@@ -796,10 +789,10 @@
final ActivityManager.RunningTaskInfo firstTaskInfo =
builder.setToken(token).setBounds(new Rect(0, 0, 1000, 1000)).build();
final TestWindowDecoration windowDecor = createWindowDecoration(firstTaskInfo);
- windowDecor.relayout(firstTaskInfo);
+ windowDecor.relayout(firstTaskInfo, true /* hasGlobalFocus */);
final ActivityManager.RunningTaskInfo secondTaskInfo =
builder.setToken(token).setBounds(new Rect(0, 0, 1000, 1000)).build();
- windowDecor.relayout(secondTaskInfo);
+ windowDecor.relayout(secondTaskInfo, true /* hasGlobalFocus */);
// Insets should only need to be applied once.
verify(mMockWindowContainerTransaction, times(1)).addInsetsSource(eq(token), any(),
@@ -824,7 +817,7 @@
mRelayoutParams.mIsCaptionVisible = true;
mRelayoutParams.mInsetSourceFlags =
FLAG_FORCE_CONSUMING | FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
// Caption inset source should add params' flags.
verify(mMockWindowContainerTransaction).addInsetsSource(eq(token), any(),
@@ -845,14 +838,13 @@
.setVisible(true)
.setWindowingMode(WINDOWING_MODE_FREEFORM)
.build();
- taskInfo.isFocused = true;
// Density is 2. Shadow radius is 10px. Caption height is 64px.
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
mRelayoutParams.mSetTaskPositionAndCrop = false;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockSurfaceControlStartT, never()).setWindowCrop(
eq(mMockTaskSurface), anyInt(), anyInt());
@@ -875,13 +867,12 @@
.setVisible(true)
.setWindowingMode(WINDOWING_MODE_FREEFORM)
.build();
- taskInfo.isFocused = true;
// Density is 2. Shadow radius is 10px. Caption height is 64px.
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
mRelayoutParams.mSetTaskPositionAndCrop = true;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockSurfaceControlStartT).setWindowCrop(
eq(mMockTaskSurface), anyInt(), anyInt());
@@ -932,12 +923,12 @@
when(mMockDisplayController.getInsetsState(task.displayId))
.thenReturn(createInsetsState(statusBars(), true /* visible */));
final TestWindowDecoration decor = spy(createWindowDecoration(task));
- decor.relayout(task);
+ decor.relayout(task, true /* hasGlobalFocus */);
assertTrue(decor.mIsStatusBarVisible);
decor.onInsetsStateChanged(createInsetsState(statusBars(), false /* visible */));
- verify(decor, times(2)).relayout(task);
+ verify(decor, times(2)).relayout(task, true /* hasGlobalFocus */);
}
@Test
@@ -947,11 +938,11 @@
when(mMockDisplayController.getInsetsState(task.displayId))
.thenReturn(createInsetsState(statusBars(), true /* visible */));
final TestWindowDecoration decor = spy(createWindowDecoration(task));
- decor.relayout(task);
+ decor.relayout(task, true /* hasGlobalFocus */);
decor.onInsetsStateChanged(createInsetsState(statusBars(), true /* visible */));
- verify(decor, times(1)).relayout(task);
+ verify(decor, times(1)).relayout(task, true /* hasGlobalFocus */);
}
@Test
@@ -960,13 +951,13 @@
when(mMockDisplayController.getInsetsState(task.displayId))
.thenReturn(createInsetsState(statusBars(), true /* visible */));
final TestWindowDecoration decor = spy(createWindowDecoration(task));
- decor.relayout(task);
+ decor.relayout(task, true /* hasGlobalFocus */);
assertFalse(decor.mIsKeyguardVisibleAndOccluded);
decor.onKeyguardStateChanged(true /* visible */, true /* occluding */);
assertTrue(decor.mIsKeyguardVisibleAndOccluded);
- verify(decor, times(2)).relayout(task);
+ verify(decor, times(2)).relayout(task, true /* hasGlobalFocus */);
}
@Test
@@ -975,19 +966,18 @@
when(mMockDisplayController.getInsetsState(task.displayId))
.thenReturn(createInsetsState(statusBars(), true /* visible */));
final TestWindowDecoration decor = spy(createWindowDecoration(task));
- decor.relayout(task);
+ decor.relayout(task, true /* hasGlobalFocus */);
assertFalse(decor.mIsKeyguardVisibleAndOccluded);
decor.onKeyguardStateChanged(false /* visible */, true /* occluding */);
- verify(decor, times(1)).relayout(task);
+ verify(decor, times(1)).relayout(task, true /* hasGlobalFocus */);
}
private ActivityManager.RunningTaskInfo createTaskInfo() {
final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
.setVisible(true)
.build();
- taskInfo.isFocused = true;
return taskInfo;
}
@@ -1055,8 +1045,8 @@
}
@Override
- void relayout(ActivityManager.RunningTaskInfo taskInfo) {
- relayout(taskInfo, false /* applyStartTransactionOnDraw */);
+ void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean hasGlobalFocus) {
+ relayout(taskInfo, false /* applyStartTransactionOnDraw */, hasGlobalFocus);
}
@Override
@@ -1078,10 +1068,11 @@
}
void relayout(ActivityManager.RunningTaskInfo taskInfo,
- boolean applyStartTransactionOnDraw) {
+ boolean applyStartTransactionOnDraw, boolean hasGlobalFocus) {
mRelayoutParams.mRunningTaskInfo = taskInfo;
mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
mRelayoutParams.mLayoutResId = R.layout.caption_layout;
+ mRelayoutParams.mHasGlobalFocus = hasGlobalFocus;
relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT,
mMockWindowContainerTransaction, mMockView, mRelayoutResult);
}
diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt
index bc269fe..e9845c1 100644
--- a/libs/appfunctions/api/current.txt
+++ b/libs/appfunctions/api/current.txt
@@ -15,7 +15,8 @@
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 6e91de6..2a168e8 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.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 @@
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,13 +110,51 @@
* 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 cancellationSignal A {@link CancellationSignal} to cancel the 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,
@NonNull Consumer<ExecuteAppFunctionResponse> callback) {
onExecuteFunction(request, callback);
@@ -138,7 +179,6 @@
*
* @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 d87fec79..969e5d5 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 @@
@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/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java
index 39bbc25..50419f7 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java
@@ -263,8 +263,8 @@
}
@Override
- protected void onStop() {
- super.onStop();
+ protected void onDestroy() {
+ super.onDestroy();
// TODO: handle config changes without cancelling.
if (!isDone()) {
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 0000000..952562e
--- /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 2a251a5..dfd296f 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.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.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 @@
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 c9c81aa..cb05504 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 @@
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 0000000..c2b67cb
--- /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 831b439..3cfb536 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 @@
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 0000000..c6e834a
--- /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 ab95162..5dca637 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 @@
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 58a83fa..4cf270d 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.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 @@
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 c68ec78..acb96be 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.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.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 @@
.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 b28e88e..e6a2366 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.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 @@
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 @@
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 b771f36..5419223 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 @@
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 8b6351e..7fdb32c 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 @@
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 6dab224..b9f16ed 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.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 @@
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 @@
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 @@
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 364e95c..6a9d568 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 @@
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.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 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 @@
/** 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 @@
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 (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);
}
- if (targetDevice == null) {
- Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, target is null");
+ 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 getFallbackActiveGroupId() {
+ 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 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 24815fa..91a99ae 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 @@
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 9cf4907..c85756e 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.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 4af0504..a33fcc6 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.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.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 @@
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 @@
null
}
}
- .first()
+ .firstOrNull() ?: false
}
private var config =
@@ -131,9 +136,15 @@
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 @@
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 @@
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/media/InputMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java
index 1d17b00..6335e71 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java
@@ -49,19 +49,23 @@
private final boolean mIsVolumeFixed;
+ private final String mProductName;
+
private InputMediaDevice(
@NonNull Context context,
@NonNull String id,
@AudioDeviceType int audioDeviceInfoType,
int maxVolume,
int currentVolume,
- boolean isVolumeFixed) {
+ boolean isVolumeFixed,
+ @Nullable String productName) {
super(context, /* info= */ null, /* item= */ null);
mId = id;
mAudioDeviceInfoType = audioDeviceInfoType;
mMaxVolume = maxVolume;
mCurrentVolume = currentVolume;
mIsVolumeFixed = isVolumeFixed;
+ mProductName = productName;
initDeviceRecord();
}
@@ -72,13 +76,20 @@
@AudioDeviceType int audioDeviceInfoType,
int maxVolume,
int currentVolume,
- boolean isVolumeFixed) {
+ boolean isVolumeFixed,
+ @Nullable String productName) {
if (!isSupportedInputDevice(audioDeviceInfoType)) {
return null;
}
return new InputMediaDevice(
- context, id, audioDeviceInfoType, maxVolume, currentVolume, isVolumeFixed);
+ context,
+ id,
+ audioDeviceInfoType,
+ maxVolume,
+ currentVolume,
+ isVolumeFixed,
+ productName);
}
public @AudioDeviceType int getAudioDeviceInfoType() {
@@ -98,18 +109,25 @@
};
}
+ @Nullable
+ public String getProductName() {
+ return mProductName;
+ }
+
@Override
public @NonNull String getName() {
- CharSequence name = switch (mAudioDeviceInfoType) {
- case TYPE_WIRED_HEADSET -> mContext.getString(
- R.string.media_transfer_wired_device_mic_name);
- case TYPE_USB_DEVICE, TYPE_USB_HEADSET, TYPE_USB_ACCESSORY -> mContext.getString(
- R.string.media_transfer_usb_device_mic_name);
- case TYPE_BLUETOOTH_SCO -> mContext.getString(
- R.string.media_transfer_bt_device_mic_name);
+ return switch (mAudioDeviceInfoType) {
+ case TYPE_WIRED_HEADSET ->
+ mContext.getString(R.string.media_transfer_wired_device_mic_name);
+ case TYPE_USB_DEVICE, TYPE_USB_HEADSET, TYPE_USB_ACCESSORY ->
+ // The product name is assumed to be a well-formed string if it's not null.
+ mProductName != null
+ ? mProductName
+ : mContext.getString(R.string.media_transfer_usb_device_mic_name);
+ case TYPE_BLUETOOTH_SCO ->
+ mContext.getString(R.string.media_transfer_bt_device_mic_name);
default -> mContext.getString(R.string.media_transfer_this_device_name_desktop);
};
- return name.toString();
}
@Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
index a72ba8d..727662b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
@@ -151,7 +151,8 @@
info.getType(),
getMaxInputGain(),
getCurrentInputGain(),
- isInputGainFixed());
+ isInputGainFixed(),
+ getProductNameFromAudioDeviceInfo(info));
if (mediaDevice != null) {
if (info.getType() == selectedInputDeviceAttributesType) {
mediaDevice.setState(STATE_SELECTED);
@@ -169,6 +170,25 @@
}
}
+ /**
+ * Gets the product name for the given {@link AudioDeviceInfo}.
+ *
+ * @return The product name for the given {@link AudioDeviceInfo}, or null if a suitable name
+ * cannot be found.
+ */
+ @Nullable
+ private String getProductNameFromAudioDeviceInfo(AudioDeviceInfo deviceInfo) {
+ CharSequence productName = deviceInfo.getProductName();
+ if (productName == null) {
+ return null;
+ }
+ String productNameString = productName.toString();
+ if (productNameString.isBlank()) {
+ return null;
+ }
+ return productNameString;
+ }
+
public void selectDevice(@NonNull MediaDevice device) {
if (!(device instanceof InputMediaDevice)) {
Slog.w(TAG, "This device is not an InputMediaDevice: " + device.getName());
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
index feaf7fb..b94e906 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
@@ -15,6 +15,7 @@
*/
package com.android.settingslib.media;
+import static android.content.pm.PackageManager.FEATURE_PC;
import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER;
import static android.media.MediaRoute2Info.TYPE_DOCK;
import static android.media.MediaRoute2Info.TYPE_HDMI;
@@ -29,8 +30,6 @@
import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER;
import android.Manifest;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
@@ -43,6 +42,8 @@
import android.util.Log;
import androidx.annotation.VisibleForTesting;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.android.settingslib.R;
import com.android.settingslib.media.flags.Flags;
@@ -72,7 +73,7 @@
return context.getString(R.string.media_transfer_this_device_name_tv);
} else if (isTablet()) {
return context.getString(R.string.media_transfer_this_device_name_tablet);
- } else if (inputRoutingEnabledAndIsDesktop()) {
+ } else if (inputRoutingEnabledAndIsDesktop(context)) {
return context.getString(R.string.media_transfer_this_device_name_desktop);
} else {
return context.getString(R.string.media_transfer_this_device_name);
@@ -88,7 +89,7 @@
case TYPE_WIRED_HEADSET:
case TYPE_WIRED_HEADPHONES:
name =
- inputRoutingEnabledAndIsDesktop()
+ inputRoutingEnabledAndIsDesktop(context)
? context.getString(R.string.media_transfer_headphone_name)
: context.getString(R.string.media_transfer_wired_headphone_name);
break;
@@ -96,7 +97,7 @@
case TYPE_USB_HEADSET:
case TYPE_USB_ACCESSORY:
name =
- inputRoutingEnabledAndIsDesktop()
+ inputRoutingEnabledAndIsDesktop(context)
? context.getString(R.string.media_transfer_usb_audio_name)
: context.getString(R.string.media_transfer_wired_headphone_name);
break;
@@ -149,14 +150,13 @@
.contains("tablet");
}
- static boolean isDesktop() {
- return Arrays.asList(SystemProperties.get("ro.build.characteristics").split(","))
- .contains("desktop");
+ public static boolean isDesktop(@NonNull Context context) {
+ return context.getPackageManager().hasSystemFeature(FEATURE_PC);
}
- static boolean inputRoutingEnabledAndIsDesktop() {
+ public static boolean inputRoutingEnabledAndIsDesktop(@NonNull Context context) {
return com.android.media.flags.Flags.enableAudioInputDeviceRoutingAndVolumeControl()
- && isDesktop();
+ && isDesktop(context);
}
// MediaRoute2Info.getType was made public on API 34, but exists since API 30.
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 43d7946..23be7ba 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 @@
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 b1489be..22b3150 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.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.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 @@
private LocalBluetoothProfileManager mLocalProfileManager;
@Mock
private BluetoothUtils.ErrorListener mErrorListener;
+ @Mock
+ private LocalBluetoothLeBroadcast mBroadcast;
private Context mContext;
private Intent mIntent;
@@ -107,7 +111,7 @@
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- mContext = RuntimeEnvironment.application;
+ mContext = spy(RuntimeEnvironment.application);
mBluetoothEventManager =
new BluetoothEventManager(
@@ -208,28 +212,14 @@
*/
@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 @@
*/
@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 @@
*/
@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 @@
*/
@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 @@
*/
@Test
public void dispatchProfileConnectionStateChanged_audioSharing_updateFallbackDevice() {
- 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_BROADCAST_ASSISTANT);
- verify(broadcast).updateFallbackActiveDeviceIfNeeded();
+ 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());
+ 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(enableProfile);
+ LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class);
+ when(profileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
+ when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant);
+ when(mBtManager.getProfileManager()).thenReturn(profileManager);
+ 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 b180b69..fd14d1f 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.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 @@
private LocalBluetoothLeBroadcast mBroadcast;
@Mock
private LocalBluetoothLeBroadcastAssistant mAssistant;
+ @Mock
+ private UserManager mUserManager;
private ShadowBluetoothAdapter mShadowBluetoothAdapter;
private CachedBluetoothDevice mCachedDevice1;
@@ -128,7 +131,7 @@
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 @@
}
@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/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java
index 30e4637..6c1cb70 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java
@@ -41,6 +41,9 @@
private final int MAX_VOLUME = 1;
private final int CURRENT_VOLUME = 0;
private final boolean IS_VOLUME_FIXED = true;
+ private static final String PRODUCT_NAME_BUILTIN_MIC = "Built-in Mic";
+ private static final String PRODUCT_NAME_WIRED_HEADSET = "My Wired Headset";
+ private static final String PRODUCT_NAME_USB_HEADSET = "My USB Headset";
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -60,7 +63,8 @@
AudioDeviceInfo.TYPE_BUILTIN_MIC,
MAX_VOLUME,
CURRENT_VOLUME,
- IS_VOLUME_FIXED);
+ IS_VOLUME_FIXED,
+ PRODUCT_NAME_BUILTIN_MIC);
assertThat(builtinMediaDevice).isNotNull();
assertThat(builtinMediaDevice.getDrawableResId()).isEqualTo(R.drawable.ic_media_microphone);
}
@@ -74,7 +78,8 @@
AudioDeviceInfo.TYPE_BUILTIN_MIC,
MAX_VOLUME,
CURRENT_VOLUME,
- IS_VOLUME_FIXED);
+ IS_VOLUME_FIXED,
+ PRODUCT_NAME_BUILTIN_MIC);
assertThat(builtinMediaDevice).isNotNull();
assertThat(builtinMediaDevice.getName())
.isEqualTo(mContext.getString(R.string.media_transfer_this_device_name_desktop));
@@ -89,7 +94,8 @@
AudioDeviceInfo.TYPE_WIRED_HEADSET,
MAX_VOLUME,
CURRENT_VOLUME,
- IS_VOLUME_FIXED);
+ IS_VOLUME_FIXED,
+ PRODUCT_NAME_WIRED_HEADSET);
assertThat(wiredMediaDevice).isNotNull();
assertThat(wiredMediaDevice.getName())
.isEqualTo(mContext.getString(R.string.media_transfer_wired_device_mic_name));
@@ -104,7 +110,23 @@
AudioDeviceInfo.TYPE_USB_HEADSET,
MAX_VOLUME,
CURRENT_VOLUME,
- IS_VOLUME_FIXED);
+ IS_VOLUME_FIXED,
+ PRODUCT_NAME_USB_HEADSET);
+ assertThat(usbMediaDevice).isNotNull();
+ assertThat(usbMediaDevice.getName()).isEqualTo(PRODUCT_NAME_USB_HEADSET);
+ }
+
+ @Test
+ public void getName_returnCorrectName_usbHeadset_nullProductName() {
+ InputMediaDevice usbMediaDevice =
+ InputMediaDevice.create(
+ mContext,
+ String.valueOf(USB_HEADSET_ID),
+ AudioDeviceInfo.TYPE_USB_HEADSET,
+ MAX_VOLUME,
+ CURRENT_VOLUME,
+ IS_VOLUME_FIXED,
+ null);
assertThat(usbMediaDevice).isNotNull();
assertThat(usbMediaDevice.getName())
.isEqualTo(mContext.getString(R.string.media_transfer_usb_device_mic_name));
@@ -119,7 +141,8 @@
AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
MAX_VOLUME,
CURRENT_VOLUME,
- IS_VOLUME_FIXED);
+ IS_VOLUME_FIXED,
+ null);
assertThat(btMediaDevice).isNotNull();
assertThat(btMediaDevice.getName())
.isEqualTo(mContext.getString(R.string.media_transfer_bt_device_mic_name));
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
index 29cc403..f63bfc7 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
@@ -58,6 +58,11 @@
private static final int MAX_VOLUME = 1;
private static final int CURRENT_VOLUME = 0;
private static final boolean VOLUME_FIXED_TRUE = true;
+ private static final String PRODUCT_NAME_BUILTIN_MIC = "Built-in Mic";
+ private static final String PRODUCT_NAME_WIRED_HEADSET = "My Wired Headset";
+ private static final String PRODUCT_NAME_USB_HEADSET = "My USB Headset";
+ private static final String PRODUCT_NAME_USB_DEVICE = "My USB Device";
+ private static final String PRODUCT_NAME_USB_ACCESSORY = "My USB Accessory";
private final Context mContext = spy(RuntimeEnvironment.application);
private InputRouteManager mInputRouteManager;
@@ -75,25 +80,31 @@
final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
when(info1.getId()).thenReturn(BUILTIN_MIC_ID);
+ when(info1.getProductName()).thenReturn(PRODUCT_NAME_BUILTIN_MIC);
final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
when(info2.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+ when(info2.getProductName()).thenReturn(PRODUCT_NAME_WIRED_HEADSET);
final AudioDeviceInfo info3 = mock(AudioDeviceInfo.class);
when(info3.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_DEVICE);
when(info3.getId()).thenReturn(INPUT_USB_DEVICE_ID);
+ when(info3.getProductName()).thenReturn(PRODUCT_NAME_USB_DEVICE);
final AudioDeviceInfo info4 = mock(AudioDeviceInfo.class);
when(info4.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_HEADSET);
when(info4.getId()).thenReturn(INPUT_USB_HEADSET_ID);
+ when(info4.getProductName()).thenReturn(PRODUCT_NAME_USB_HEADSET);
final AudioDeviceInfo info5 = mock(AudioDeviceInfo.class);
when(info5.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_ACCESSORY);
when(info5.getId()).thenReturn(INPUT_USB_ACCESSORY_ID);
+ when(info5.getProductName()).thenReturn(PRODUCT_NAME_USB_ACCESSORY);
final AudioDeviceInfo unsupportedInfo = mock(AudioDeviceInfo.class);
when(unsupportedInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_HDMI);
+ when(unsupportedInfo.getProductName()).thenReturn("HDMI device");
final AudioManager audioManager = mock(AudioManager.class);
AudioDeviceInfo[] devices = {info1, info2, info3, info4, info5, unsupportedInfo};
@@ -142,10 +153,12 @@
final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
when(info1.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+ when(info1.getProductName()).thenReturn(PRODUCT_NAME_WIRED_HEADSET);
final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
when(info2.getId()).thenReturn(BUILTIN_MIC_ID);
+ when(info2.getProductName()).thenReturn(PRODUCT_NAME_BUILTIN_MIC);
final AudioManager audioManager = mock(AudioManager.class);
AudioDeviceInfo[] devices = {info1, info2};
@@ -171,10 +184,12 @@
final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
when(info1.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+ when(info1.getProductName()).thenReturn(PRODUCT_NAME_WIRED_HEADSET);
final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
when(info2.getId()).thenReturn(BUILTIN_MIC_ID);
+ when(info2.getProductName()).thenReturn(PRODUCT_NAME_BUILTIN_MIC);
final AudioManager audioManager = mock(AudioManager.class);
AudioDeviceInfo[] devices = {info1, info2};
@@ -204,10 +219,12 @@
final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
when(info1.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+ when(info1.getProductName()).thenReturn(PRODUCT_NAME_WIRED_HEADSET);
final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
when(info2.getId()).thenReturn(BUILTIN_MIC_ID);
+ when(info2.getProductName()).thenReturn(PRODUCT_NAME_BUILTIN_MIC);
final AudioManager audioManager = mock(AudioManager.class);
AudioDeviceInfo[] devices = {info1, info2};
@@ -239,7 +256,8 @@
AudioDeviceInfo.TYPE_BUILTIN_MIC,
MAX_VOLUME,
CURRENT_VOLUME,
- VOLUME_FIXED_TRUE);
+ VOLUME_FIXED_TRUE,
+ PRODUCT_NAME_BUILTIN_MIC);
inputRouteManager.selectDevice(inputMediaDevice);
AudioDeviceAttributes deviceAttributes =
@@ -267,4 +285,51 @@
public void isInputGainFixed() {
assertThat(mInputRouteManager.isInputGainFixed()).isTrue();
}
+
+ @Test
+ public void onAudioDevicesAdded_shouldSetProductNameCorrectly() {
+ final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
+ when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+ when(info1.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+ String firstProductName = "My first headset";
+ when(info1.getProductName()).thenReturn(firstProductName);
+
+ final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
+ when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+ when(info2.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+ String secondProductName = "My second headset";
+ when(info2.getProductName()).thenReturn(secondProductName);
+
+ final AudioDeviceInfo infoWithNullProductName = mock(AudioDeviceInfo.class);
+ when(infoWithNullProductName.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+ when(infoWithNullProductName.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+ when(infoWithNullProductName.getProductName()).thenReturn(null);
+
+ final AudioDeviceInfo infoWithBlankProductName = mock(AudioDeviceInfo.class);
+ when(infoWithBlankProductName.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+ when(infoWithBlankProductName.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+ when(infoWithBlankProductName.getProductName()).thenReturn("");
+
+ final AudioManager audioManager = mock(AudioManager.class);
+ AudioDeviceInfo[] devices = {
+ info1, info2, infoWithNullProductName, infoWithBlankProductName
+ };
+ when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(devices);
+
+ InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
+
+ assertThat(inputRouteManager.mInputMediaDevices).isEmpty();
+
+ inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+
+ assertThat(getProductNameAtIndex(inputRouteManager, 1)).isEqualTo(firstProductName);
+ assertThat(getProductNameAtIndex(inputRouteManager, 2)).isEqualTo(secondProductName);
+ assertThat(getProductNameAtIndex(inputRouteManager, 3)).isNull();
+ assertThat(getProductNameAtIndex(inputRouteManager, 4)).isNull();
+ }
+
+ private String getProductNameAtIndex(InputRouteManager inputRouteManager, int index) {
+ return ((InputMediaDevice) inputRouteManager.mInputMediaDevices.get(index))
+ .getProductName();
+ }
}
diff --git a/packages/SettingsProvider/Android.bp b/packages/SettingsProvider/Android.bp
index 65b2275..1a99d25 100644
--- a/packages/SettingsProvider/Android.bp
+++ b/packages/SettingsProvider/Android.bp
@@ -39,7 +39,6 @@
"configinfra_framework_flags_java_lib",
"device_config_service_flags_java",
"libaconfig_java_proto_lite",
- "notification_flags_lib",
"SettingsLibDeviceStateRotationLock",
"SettingsLibDisplayUtils",
],
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
index ec3bd90..6c31831 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
@@ -29,7 +29,6 @@
import android.icu.util.ULocale;
import android.media.AudioManager;
import android.media.RingtoneManager;
-import android.media.Utils;
import android.net.Uri;
import android.os.LocaleList;
import android.os.RemoteException;
@@ -310,13 +309,6 @@
return SILENT_RINGTONE;
}
} else {
- // If the ringtone/notification support the vibration, use the original value.
- final int ringtoneType = getRingtoneType(name);
- if ((Settings.System.RINGTONE.equals(name)
- || Settings.System.NOTIFICATION_SOUND.equals(name))
- && hasVibrationSettings(value, ringtoneType)) {
- return value;
- }
return getCanonicalRingtoneValue(value);
}
}
@@ -370,15 +362,6 @@
return;
}
- // If the ringtone/notification has vibration, we backup original value in onBackupValue.
- // So use the value directly for restoring.
- if ((ringtoneType == RingtoneManager.TYPE_RINGTONE
- || ringtoneType == RingtoneManager.TYPE_NOTIFICATION)
- && hasVibrationSettings(value, ringtoneType)) {
- RingtoneManager.setActualDefaultRingtoneUri(mContext, ringtoneType, Uri.parse(value));
- return;
- }
-
Uri ringtoneUri = null;
try {
ringtoneUri =
@@ -634,19 +617,6 @@
return allLocales.remove(toFullLocale(filteredLocale));
}
- private boolean hasVibrationSettings(String value, int type) {
- if (Utils.hasVibration(Uri.parse(value)) && mContext.getResources().getBoolean(
- com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported)) {
- if (type == RingtoneManager.TYPE_RINGTONE) {
- return android.media.audio.Flags.enableRingtoneHapticsCustomization();
- }
- if (type == RingtoneManager.TYPE_NOTIFICATION) {
- return com.android.server.notification.Flags.notificationVibrationInSoundUri();
- }
- }
- return false;
- }
-
/**
* Sets the locale specified. Input data is the byte representation of comma separated
* multiple BCP-47 language tags. For backwards compatibility, strings of the form
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java
index babc1a3..4b10b56 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java
@@ -37,12 +37,9 @@
import android.database.Cursor;
import android.database.MatrixCursor;
import android.media.AudioManager;
-import android.media.Utils;
import android.net.Uri;
import android.os.Bundle;
import android.os.LocaleList;
-import android.platform.test.annotations.EnableFlags;
-import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.BaseColumns;
import android.provider.MediaStore;
import android.provider.Settings;
@@ -57,7 +54,6 @@
import org.junit.After;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -78,13 +74,9 @@
"content://media/internal/audio/media/20?title=DefaultNotification&canonical=1";
private static final String DEFAULT_ALARM_VALUE =
"content://media/internal/audio/media/30?title=DefaultAlarm&canonical=1";
- private static final String VIBRATION_FILE_NAME = "haptics.xml";
private SettingsHelper mSettingsHelper;
- @Rule
- public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
-
@Mock private Context mContext;
@Mock private Resources mResources;
@Mock private AudioManager mAudioManager;
@@ -128,22 +120,6 @@
}
@Test
- @EnableFlags({android.media.audio.Flags.FLAG_ENABLE_RINGTONE_HAPTICS_CUSTOMIZATION,
- com.android.server.notification.Flags.FLAG_NOTIFICATION_VIBRATION_IN_SOUND_URI})
- public void testOnBackupValue_ringtoneVibrationSupport_returnsSameValue() {
- when(mResources.getBoolean(
- com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported)).thenReturn(
- true);
- String testRingtoneVibrationValue = createUriWithVibration(DEFAULT_RINGTONE_VALUE);
- String testNotificationVibrationValue = createUriWithVibration(DEFAULT_NOTIFICATION_VALUE);
-
- assertEquals(testRingtoneVibrationValue, mSettingsHelper.onBackupValue(
- Settings.System.RINGTONE, testRingtoneVibrationValue));
- assertEquals(testNotificationVibrationValue, mSettingsHelper.onBackupValue(
- Settings.System.NOTIFICATION_SOUND, testNotificationVibrationValue));
- }
-
- @Test
public void testGetRealValue_settingNotReplaced_returnsSameValue() {
when(mSettingsHelper.isReplacedSystemSetting(eq(SETTING_KEY))).thenReturn(false);
@@ -699,30 +675,6 @@
.isEqualTo(null);
}
- @Test
- @EnableFlags({android.media.audio.Flags.FLAG_ENABLE_RINGTONE_HAPTICS_CUSTOMIZATION,
- com.android.server.notification.Flags.FLAG_NOTIFICATION_VIBRATION_IN_SOUND_URI})
- public void testRestoreValue_ringtoneVibrationSupport_restoreValue() {
- when(mResources.getBoolean(
- com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported)).thenReturn(
- true);
- String testRingtoneVibrationValue = createUriWithVibration(DEFAULT_RINGTONE_VALUE);
- String testNotificationVibrationValue = createUriWithVibration(DEFAULT_NOTIFICATION_VALUE);
- ContentProvider mockMediaContentProvider =
- new MockContentProvider(mContext) {
- @Override
- public String getType(Uri url) {
- return "audio/ogg";
- }
- };
- mContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider);
- resetRingtoneSettingsToDefault();
-
- assertRingtoneSettingsRestoring(Settings.System.RINGTONE, testRingtoneVibrationValue);
- assertRingtoneSettingsRestoring(
- Settings.System.NOTIFICATION_SOUND, testNotificationVibrationValue);
- }
-
private static class MockSettingsProvider extends MockContentProvider {
private final ArrayMap<String, String> mKeyValueStore = new ArrayMap<>();
MockSettingsProvider(Context context) {
@@ -814,25 +766,4 @@
assertThat(Settings.System.getString(mContentResolver, Settings.System.ALARM_ALERT))
.isEqualTo(DEFAULT_ALARM_VALUE);
}
-
- private String createUriWithVibration(String defaultUriString) {
- return Uri.parse(defaultUriString).buildUpon()
- .appendQueryParameter(
- Utils.VIBRATION_URI_PARAM, VIBRATION_FILE_NAME).build().toString();
- }
-
- private void assertRingtoneSettingsRestoring(
- String settings, String testRingtoneSettingsValue) {
- mSettingsHelper.restoreValue(
- mContext,
- mContentResolver,
- new ContentValues(),
- Uri.EMPTY,
- settings,
- testRingtoneSettingsValue,
- 0);
-
- assertThat(Settings.System.getString(mContentResolver, settings))
- .isEqualTo(testRingtoneSettingsValue);
- }
}
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 456fedf..408ed1e 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/brightness/data/repository/ScreenBrightnessDisplayManagerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/data/repository/ScreenBrightnessDisplayManagerRepositoryTest.kt
index a676c7d..0983105 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/data/repository/ScreenBrightnessDisplayManagerRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/data/repository/ScreenBrightnessDisplayManagerRepositoryTest.kt
@@ -31,12 +31,11 @@
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.core.FakeLogBuffer
-import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logcatTableLogBuffer
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -78,7 +77,7 @@
displayId,
displayManager,
FakeLogBuffer.Factory.create(),
- mock<TableLogBuffer>(),
+ logcatTableLogBuffer(kosmos, "screenBrightness"),
kosmos.applicationCoroutineScope,
kosmos.testDispatcher,
)
@@ -163,7 +162,7 @@
changeBrightnessInfoAndNotify(
BrightnessInfo(0.5f, 0.1f, 0.7f),
- listenerCaptor.value
+ listenerCaptor.value,
)
runCurrent()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorTest.kt
index b6616bf..18e7a7e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorTest.kt
@@ -27,9 +27,8 @@
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testScope
-import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logcatTableLogBuffer
import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
@@ -49,7 +48,7 @@
ScreenBrightnessInteractor(
screenBrightnessRepository,
applicationCoroutineScope,
- mock<TableLogBuffer>()
+ logcatTableLogBuffer(this, "screenBrightness"),
)
}
@@ -112,7 +111,7 @@
BrightnessUtils.convertGammaToLinearFloat(
gammaBrightness,
min.floatValue,
- max.floatValue
+ max.floatValue,
)
assertThat(temporaryBrightness!!.floatValue)
.isWithin(1e-5f)
@@ -136,7 +135,7 @@
BrightnessUtils.convertGammaToLinearFloat(
gammaBrightness,
min.floatValue,
- max.floatValue
+ max.floatValue,
)
assertThat(brightness!!.floatValue).isWithin(1e-5f).of(expectedBrightness)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
index dd5ad17..2b0928f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
@@ -21,7 +21,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
-import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logcatTableLogBuffer
import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.testKosmos
@@ -44,7 +44,6 @@
class CommunalMediaRepositoryImplTest : SysuiTestCase() {
private val mediaDataManager = mock<MediaDataManager>()
private val mediaData = mock<MediaData>()
- private val tableLogBuffer = mock<TableLogBuffer>()
private lateinit var underTest: CommunalMediaRepositoryImpl
@@ -52,14 +51,11 @@
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
+ private val tableLogBuffer = logcatTableLogBuffer(kosmos, "CommunalMediaRepositoryImplTest")
@Before
fun setUp() {
- underTest =
- CommunalMediaRepositoryImpl(
- mediaDataManager,
- tableLogBuffer,
- )
+ underTest = CommunalMediaRepositoryImpl(mediaDataManager, tableLogBuffer)
}
@Test
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 d9dcfdc..9c6fd4b 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 @@
// Adding twice should not invoke twice
reset(callback)
+ underTest.onStartDream()
underTest.addCallback(callback)
underTest.onWakeUp()
verify(callback, times(1)).onWakeUp()
@@ -68,6 +69,19 @@
}
@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 a3314e8..f5d2d42 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 @@
// 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/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt
index de3dc57..1d80826 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt
@@ -28,6 +28,7 @@
import com.android.settingslib.notification.modes.TestModeBuilder
import com.android.systemui.SysuiTestCase
import com.android.systemui.SysuiTestableContext
+import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.shared.model.asIcon
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
@@ -144,13 +145,13 @@
// Tile starts with the generic Modes icon.
runCurrent()
- assertThat(tileData?.icon).isEqualTo(MODES_ICON)
+ assertThat(tileData?.icon).isEqualTo(MODES_RESOURCE_ICON)
assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID)
// Add an inactive mode -> Still modes icon
zenModeRepository.addMode(id = "Mode", active = false)
runCurrent()
- assertThat(tileData?.icon).isEqualTo(MODES_ICON)
+ assertThat(tileData?.icon).isEqualTo(MODES_RESOURCE_ICON)
assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID)
// Add an active mode with a default icon: icon should be the mode icon, and the
@@ -158,7 +159,7 @@
zenModeRepository.addMode(
id = "Bedtime with default icon",
type = AutomaticZenRule.TYPE_BEDTIME,
- active = true
+ active = true,
)
runCurrent()
assertThat(tileData?.icon).isEqualTo(BEDTIME_ICON)
@@ -189,7 +190,7 @@
// Deactivate remaining mode: back to the default modes icon
zenModeRepository.deactivateMode("Driving with custom icon")
runCurrent()
- assertThat(tileData?.icon).isEqualTo(MODES_ICON)
+ assertThat(tileData?.icon).isEqualTo(MODES_RESOURCE_ICON)
assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID)
}
@@ -204,18 +205,18 @@
)
runCurrent()
- assertThat(tileData?.icon).isEqualTo(MODES_ICON)
+ assertThat(tileData?.icon).isEqualTo(MODES_RESOURCE_ICON)
assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID)
// Activate a Mode -> Icon doesn't change.
zenModeRepository.addMode(id = "Mode", active = true)
runCurrent()
- assertThat(tileData?.icon).isEqualTo(MODES_ICON)
+ assertThat(tileData?.icon).isEqualTo(MODES_RESOURCE_ICON)
assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID)
zenModeRepository.deactivateMode(id = "Mode")
runCurrent()
- assertThat(tileData?.icon).isEqualTo(MODES_ICON)
+ assertThat(tileData?.icon).isEqualTo(MODES_RESOURCE_ICON)
assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID)
}
@@ -263,7 +264,7 @@
val BEDTIME_DRAWABLE = TestStubDrawable("bedtime")
val CUSTOM_DRAWABLE = TestStubDrawable("custom")
- val MODES_ICON = MODES_DRAWABLE.asIcon()
+ val MODES_RESOURCE_ICON = Icon.Resource(MODES_DRAWABLE_ID, null)
val BEDTIME_ICON = BEDTIME_DRAWABLE.asIcon()
val CUSTOM_ICON = CUSTOM_DRAWABLE.asIcon()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt
index c3d45db..a58cb9c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt
@@ -22,7 +22,9 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.shared.model.asIcon
+import com.android.systemui.qs.tiles.ModesTile
import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
import com.android.systemui.qs.tiles.viewmodel.QSTileConfigTestBuilder
import com.android.systemui.qs.tiles.viewmodel.QSTileState
@@ -51,6 +53,11 @@
.apply {
addOverride(R.drawable.qs_dnd_icon_on, TestStubDrawable())
addOverride(R.drawable.qs_dnd_icon_off, TestStubDrawable())
+ addOverride(
+ ModesTile.ICON_RES_ID,
+ TestStubDrawable(ModesTile.ICON_RES_ID.toString()),
+ )
+ addOverride(123, TestStubDrawable("123"))
}
.resources,
context.theme,
@@ -59,12 +66,7 @@
@Test
fun inactiveState() {
val icon = TestStubDrawable("res123").asIcon()
- val model =
- ModesTileModel(
- isActivated = false,
- activeModes = emptyList(),
- icon = icon,
- )
+ val model = ModesTileModel(isActivated = false, activeModes = emptyList(), icon = icon)
val state = underTest.map(config, model)
@@ -76,12 +78,7 @@
@Test
fun activeState_oneMode() {
val icon = TestStubDrawable("res123").asIcon()
- val model =
- ModesTileModel(
- isActivated = true,
- activeModes = listOf("DND"),
- icon = icon,
- )
+ val model = ModesTileModel(isActivated = true, activeModes = listOf("DND"), icon = icon)
val state = underTest.map(config, model)
@@ -108,19 +105,36 @@
}
@Test
- fun state_modelHasIconResId_includesIconResId() {
- val icon = TestStubDrawable("res123").asIcon()
+ fun resourceIconModel_whenResIdsIdentical_mapsToLoadedIconWithInputResId() {
+ val icon = Icon.Resource(123, null)
val model =
ModesTileModel(
isActivated = false,
activeModes = emptyList(),
icon = icon,
- iconResId = 123
+ iconResId = 123,
)
val state = underTest.map(config, model)
- assertThat(state.icon()).isEqualTo(icon)
+ assertThat(state.icon()).isEqualTo(TestStubDrawable("123").asIcon())
+ assertThat(state.iconRes).isEqualTo(123)
+ }
+
+ @Test
+ fun resourceIconModel_whenResIdsNonIdentical_mapsToLoadedIconWithIconResourceId() {
+ val icon = Icon.Resource(123, null)
+ val model =
+ ModesTileModel(
+ isActivated = false,
+ activeModes = emptyList(),
+ icon = icon,
+ iconResId = 321, // Note: NOT 123. This will be ignored.
+ )
+
+ val state = underTest.map(config, model)
+
+ assertThat(state.icon()).isEqualTo(TestStubDrawable("123").asIcon())
assertThat(state.iconRes).isEqualTo(123)
}
}
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 c5ccf9e..74d4178 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 @@
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_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 @@
}
@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/mediaoutput/domain/interactor/MediaOutputComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorTest.kt
index fa7f37c..449dc20 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorTest.kt
@@ -16,11 +16,16 @@
package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor
+import android.content.mockedContext
+import android.content.packageManager
+import android.content.pm.PackageManager.FEATURE_PC
import android.graphics.drawable.TestStubDrawable
import android.media.AudioManager
+import android.platform.test.annotations.EnableFlags
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.media.flags.Flags;
import com.android.settingslib.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
@@ -42,6 +47,7 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.whenever
private const val builtInDeviceName = "This phone"
@@ -79,6 +85,8 @@
fun inCall_stateIs_Calling() =
with(kosmos) {
testScope.runTest {
+ whenever(mockedContext.getPackageManager()).thenReturn(packageManager)
+ whenever(packageManager.hasSystemFeature(FEATURE_PC)).thenReturn(false)
with(audioRepository) {
setMode(AudioManager.MODE_IN_CALL)
setCommunicationDevice(TestAudioDevicesFactory.builtInDevice())
@@ -98,6 +106,33 @@
}
}
+ @EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+ @Test
+ fun inCall_stateIs_Calling_enableInputRouting_desktop() =
+ with(kosmos) {
+ testScope.runTest {
+ whenever(mockedContext.getPackageManager()).thenReturn(packageManager)
+ whenever(packageManager.hasSystemFeature(FEATURE_PC)).thenReturn(true)
+
+ with(audioRepository) {
+ setMode(AudioManager.MODE_IN_CALL)
+ setCommunicationDevice(TestAudioDevicesFactory.builtInDevice())
+ }
+
+ val model by collectLastValue(underTest.mediaOutputModel.filterData())
+ runCurrent()
+
+ assertThat(model)
+ .isEqualTo(
+ MediaOutputComponentModel.Calling(
+ device = AudioOutputDevice.BuiltIn(builtInDeviceName, testIcon),
+ isInAudioSharing = false,
+ canOpenAudioSwitcher = true,
+ )
+ )
+ }
+ }
+
@Test
fun hasSession_stateIs_MediaSession() =
with(kosmos) {
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 0000000..f80b36a
--- /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 96a85d7..7225061 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/accessibility/floatingmenu/AccessibilityFloatingMenuController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
index 275147e..41b9d33 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
@@ -226,7 +226,11 @@
mBtnTargets =
mAccessibilityButtonTargetsObserver.getCurrentAccessibilityButtonTargets();
mHandler.post(
- () -> handleFloatingMenuVisibility(mIsKeyguardVisible, mBtnMode, mBtnTargets));
+ () -> {
+ // Force a refresh by destroying the menu if it exists.
+ destroyFloatingMenu();
+ handleFloatingMenuVisibility(mIsKeyguardVisible, mBtnMode, mBtnTargets);
+ });
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
index e0f73a6..cbdb882 100644
--- a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
@@ -319,7 +319,7 @@
if (item == null && active) {
item = new AppOpItem(code, uid, packageName, mClock.elapsedRealtime());
if (isOpMicrophone(code)) {
- item.setDisabled(isAnyRecordingPausedLocked(uid));
+ item.setDisabled(isAllRecordingPausedLocked(uid));
} else if (isOpCamera(code)) {
item.setDisabled(mCameraDisabled);
}
@@ -521,18 +521,21 @@
}
- private boolean isAnyRecordingPausedLocked(int uid) {
+ // TODO(b/365843152) remove AudioRecordingConfiguration listening
+ private boolean isAllRecordingPausedLocked(int uid) {
if (mMicMuted) {
return true;
}
List<AudioRecordingConfiguration> configs = mRecordingsByUid.get(uid);
if (configs == null) return false;
+ // If we are aware of AudioRecordConfigs, suppress the indicator if all of them are known
+ // to be silenced.
int configsNum = configs.size();
for (int i = 0; i < configsNum; i++) {
AudioRecordingConfiguration config = configs.get(i);
- if (config.isClientSilenced()) return true;
+ if (!config.isClientSilenced()) return false;
}
- return false;
+ return true;
}
private void updateSensorDisabledStatus() {
@@ -543,7 +546,7 @@
boolean paused = false;
if (isOpMicrophone(item.getCode())) {
- paused = isAnyRecordingPausedLocked(item.getUid());
+ paused = isAllRecordingPausedLocked(item.getUid());
} else if (isOpCamera(item.getCode())) {
paused = mCameraDisabled;
}
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 373671d0..0949ea4 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.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 @@
flowOf(false)
}
}
+ .distinctUntilChanged()
+ .onEach { Log.d(TAG, "canShowAlternateBouncer changed to $it") }
.stateIn(
scope = scope,
started = WhileSubscribed(),
@@ -234,5 +238,7 @@
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 d5ff8f2..2b61752 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 @@
}
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 83f86a7..7a6ca08 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 @@
mStateController.setLowLightActive(false);
mStateController.setEntryAnimationsFinished(false);
+ mDreamOverlayCallbackController.onWakeUp();
+
if (mDreamOverlayContainerViewController != null) {
mDreamOverlayContainerViewController.destroy();
mDreamOverlayContainerViewController = null;
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 2dd2a1e..d28b08f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -2460,6 +2460,12 @@
android.util.Log.i(TAG, "Ignoring request to dismiss (user switch in progress?)");
return;
}
+
+ if (mKeyguardStateController.isKeyguardGoingAway()) {
+ Log.i(TAG, "Ignoring dismiss because we're already going away.");
+ return;
+ }
+
mHandler.obtainMessage(DISMISS, new DismissMessage(callback, message)).sendToTarget();
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileIcon.kt b/packages/SystemUI/src/com/android/systemui/qs/QSTileIcon.kt
index ef7e7eb..62694ce 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileIcon.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileIcon.kt
@@ -22,13 +22,18 @@
/**
* Creates a [QSTile.Icon] from an [Icon].
- * * [Icon.Loaded] -> [QSTileImpl.DrawableIcon]
+ * * [Icon.Loaded] && [resId] null -> [QSTileImpl.DrawableIcon]
+ * * [Icon.Loaded] && [resId] available -> [QSTileImpl.DrawableIconWithRes]
* * [Icon.Resource] -> [QSTileImpl.ResourceIcon]
*/
-fun Icon.asQSTileIcon(): QSTile.Icon {
+fun Icon.asQSTileIcon(resId: Int?): QSTile.Icon {
return when (this) {
is Icon.Loaded -> {
- QSTileImpl.DrawableIcon(this.drawable)
+ if (resId != null) {
+ QSTileImpl.DrawableIconWithRes(this.drawable, resId)
+ } else {
+ QSTileImpl.DrawableIcon(this.drawable)
+ }
}
is Icon.Resource -> {
QSTileImpl.ResourceIcon.get(this.res)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt
index cf2db6c..3bbe624 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt
@@ -121,7 +121,7 @@
state?.apply {
this.state = tileState.activationState.legacyState
val tileStateIcon = tileState.icon()
- icon = tileStateIcon?.asQSTileIcon() ?: ResourceIcon.get(ICON_RES_ID)
+ icon = tileStateIcon?.asQSTileIcon(tileState.iconRes) ?: ResourceIcon.get(ICON_RES_ID)
label = tileLabel
secondaryLabel = tileState.secondaryLabel
contentDescription = tileState.contentDescription
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt
index 5d44ead..40591bf 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt
@@ -76,14 +76,14 @@
} else {
return ModesTileModel(
isActivated = activeModes.isAnyActive(),
- icon = context.getDrawable(ModesTile.ICON_RES_ID)!!.asIcon(),
+ icon = Icon.Resource(ModesTile.ICON_RES_ID, null),
iconResId = ModesTile.ICON_RES_ID,
activeModes = activeModes.modeNames,
)
}
}
- private data class TileIcon(val icon: Icon.Loaded, val resId: Int?)
+ private data class TileIcon(val icon: Icon, val resId: Int?)
private fun getTileIcon(activeMode: ZenModeInfo?): TileIcon {
return if (activeMode != null) {
@@ -94,7 +94,7 @@
TileIcon(activeMode.icon.drawable.asIcon(), null)
}
} else {
- TileIcon(context.getDrawable(ModesTile.ICON_RES_ID)!!.asIcon(), ModesTile.ICON_RES_ID)
+ TileIcon(Icon.Resource(ModesTile.ICON_RES_ID, null), ModesTile.ICON_RES_ID)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt
index db48123..9c31e32 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt
@@ -21,12 +21,12 @@
data class ModesTileModel(
val isActivated: Boolean,
val activeModes: List<String>,
- val icon: Icon.Loaded,
+ val icon: Icon,
/**
* Resource id corresponding to [icon]. Will only be present if it's know to correspond to a
* resource with a known id in SystemUI (such as resources from `android.R`,
* `com.android.internal.R`, or `com.android.systemui.res` itself).
*/
- val iconResId: Int? = null
+ val iconResId: Int? = null,
)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
index 69da313..801a0ce 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
@@ -18,7 +18,9 @@
import android.content.res.Resources
import android.icu.text.MessageFormat
+import android.util.Log
import android.widget.Button
+import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
@@ -30,14 +32,30 @@
class ModesTileMapper
@Inject
-constructor(
- @Main private val resources: Resources,
- val theme: Resources.Theme,
-) : QSTileDataToStateMapper<ModesTileModel> {
+constructor(@Main private val resources: Resources, val theme: Resources.Theme) :
+ QSTileDataToStateMapper<ModesTileModel> {
override fun map(config: QSTileConfig, data: ModesTileModel): QSTileState =
QSTileState.build(resources, theme, config.uiConfig) {
- iconRes = data.iconResId
- icon = { data.icon }
+ val loadedIcon: Icon.Loaded =
+ when (val dataIcon = data.icon) {
+ is Icon.Resource -> {
+ if (iconRes != dataIcon.res) {
+ Log.wtf(
+ "ModesTileMapper",
+ "Icon.Resource.res & iconResId are not identical",
+ )
+ }
+ iconRes = dataIcon.res
+ Icon.Loaded(resources.getDrawable(dataIcon.res, theme), null)
+ }
+ is Icon.Loaded -> {
+ iconRes = data.iconResId
+ dataIcon
+ }
+ }
+
+ icon = { loadedIcon }
+
activationState =
if (data.isActivated) {
QSTileState.ActivationState.ACTIVE
@@ -47,10 +65,7 @@
secondaryLabel = getModesStatus(data, resources)
contentDescription = "$label. $secondaryLabel"
supportedActions =
- setOf(
- QSTileState.UserAction.CLICK,
- QSTileState.UserAction.LONG_CLICK,
- )
+ setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
sideViewIcon = QSTileState.SideViewIcon.Chevron
expandedAccessibilityClass = Button::class
}
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 7b6a2cb..560028c 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 @@
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 0474344..7e5b455 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.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.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 @@
@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 @@
}
}
- 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 @@
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 479ffb7..17bd538 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 @@
this::consumeKeyguardAuthenticatedBiometricsHandled
);
} else {
+ // Collector that keeps the AlternateBouncerInteractor#canShowAlternateBouncer flow hot.
mListenForCanShowAlternateBouncer = mJavaAdapter.alwaysCollectFlow(
mAlternateBouncerInteractor.getCanShowAlternateBouncer(),
this::consumeCanShowAlternateBouncer
@@ -578,8 +579,17 @@
}
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 daba109..9839f9d 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 @@
.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/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt
index 94e19de..3c31efa 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt
@@ -84,7 +84,7 @@
) {
TutorialButton(
text = stringResource(R.string.touchpad_tutorial_home_gesture_button),
- icon = Icons.AutoMirrored.Outlined.ArrowBack,
+ icon = ImageVector.vectorResource(id = R.drawable.touchpad_tutorial_home_icon),
iconColor = MaterialTheme.colorScheme.onPrimary,
onClick = onHomeTutorialClicked,
backgroundColor = MaterialTheme.colorScheme.primary,
@@ -92,7 +92,7 @@
)
TutorialButton(
text = stringResource(R.string.touchpad_tutorial_back_gesture_button),
- icon = ImageVector.vectorResource(id = R.drawable.touchpad_tutorial_home_icon),
+ icon = Icons.AutoMirrored.Outlined.ArrowBack,
iconColor = MaterialTheme.colorScheme.onTertiary,
onClick = onBackTutorialClicked,
backgroundColor = MaterialTheme.colorScheme.tertiary,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractor.kt
index f94cbda..609ba02 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractor.kt
@@ -16,7 +16,10 @@
package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor
+import com.android.settingslib.media.PhoneMediaDevice.inputRoutingEnabledAndIsDesktop
+import android.content.Context
import com.android.settingslib.volume.domain.interactor.AudioModeInteractor
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.volume.domain.interactor.AudioOutputInteractor
import com.android.systemui.volume.domain.interactor.AudioSharingInteractor
import com.android.systemui.volume.domain.model.AudioOutputDevice
@@ -46,6 +49,7 @@
class MediaOutputComponentInteractor
@Inject
constructor(
+ @Application private val context: Context,
@VolumePanelScope private val coroutineScope: CoroutineScope,
private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor,
audioOutputInteractor: AudioOutputInteractor,
@@ -91,7 +95,8 @@
MediaOutputComponentModel.Calling(
device = currentAudioDevice,
isInAudioSharing = isInAudioSharing,
- canOpenAudioSwitcher = false,
+ /* allow open switcher when input routing is enabled in desktop */
+ canOpenAudioSwitcher = inputRoutingEnabledAndIsDesktop(context),
)
)
} else {
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 ffb1f11..2aa1ac9 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 @@
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.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 @@
@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 @@
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 @@
)
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 @@
private fun AudioStreamModel.toState(
isEnabled: Boolean,
ringerMode: RingerMode,
+ disabledMessage: String?,
): State {
val label =
labelsByStream[audioStream]?.let(context::getString)
@@ -148,13 +182,7 @@
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 @@
)
}
+ 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/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
index 5e37d4c..51a7b5f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
@@ -338,6 +338,25 @@
assertThat(mController.mFloatingMenu).isInstanceOf(MenuViewLayerController.class);
}
+ @Test
+ public void onUserInitializationComplete_destroysOldWidget() {
+ enableAccessibilityFloatingMenuConfig();
+ mController = setUpController();
+
+ captureKeyguardUpdateMonitorCallback();
+ mKeyguardCallback.onUserUnlocked();
+ mKeyguardCallback.onKeyguardVisibilityChanged(false);
+
+ IAccessibilityFloatingMenu floatingMenu = mController.mFloatingMenu;
+
+ mController.mUserInitializationCompleteCallback
+ .onUserInitializationComplete(mContext.getUserId());
+ mTestableLooper.processAllMessages();
+
+ assertThat(mController.mFloatingMenu).isNotNull();
+ assertThat(mController.mFloatingMenu).isNotSameInstanceAs(floatingMenu);
+ }
+
private AccessibilityFloatingMenuController setUpController() {
final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
final ViewCaptureAwareWindowManager viewCaptureAwareWindowManager =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java
index 476d6e3..7c08928 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java
@@ -103,11 +103,10 @@
@Mock()
private BroadcastDispatcher mDispatcher;
@Mock(stubOnly = true)
- private AudioManager.AudioRecordingCallback mRecordingCallback;
- @Mock(stubOnly = true)
private AudioRecordingConfiguration mPausedMockRecording;
private AppOpsControllerImpl mController;
+ private AudioManager.AudioRecordingCallback mRecordingCallback;
private TestableLooper mTestableLooper;
private final FakeExecutor mBgExecutor = new FakeExecutor(new FakeSystemClock());
@@ -975,6 +974,7 @@
}
private void verifyUnPausedSentActive(int micOpCode) {
+ // Setup stubs the initial active recording list with a single silenced client
mController.addCallback(new int[]{micOpCode}, mCallback);
mBgExecutor.runAllReady();
mTestableLooper.processAllMessages();
@@ -982,11 +982,20 @@
TEST_PACKAGE_NAME, true);
mTestableLooper.processAllMessages();
- mRecordingCallback.onRecordingConfigChanged(Collections.emptyList());
+
+ // Update with multiple recording configs, of which one is unsilenced
+ var mockARCUnsilenced = mock(AudioRecordingConfiguration.class);
+ when(mockARCUnsilenced.getClientUid()).thenReturn(TEST_UID);
+ when(mockARCUnsilenced.isClientSilenced()).thenReturn(false);
+
+ mRecordingCallback.onRecordingConfigChanged(List.of(
+ mockARCUnsilenced, mPausedMockRecording));
mTestableLooper.processAllMessages();
verify(mCallback).onActiveStateChanged(micOpCode, TEST_UID, TEST_PACKAGE_NAME, true);
+ // For consistency since this runs in a loop
+ mController.removeCallback(new int[]{micOpCode}, mCallback);
}
private void verifyAudioPausedSentInactive(int micOpCode) {
@@ -997,11 +1006,16 @@
TEST_PACKAGE_NAME, true);
mTestableLooper.processAllMessages();
- AudioRecordingConfiguration mockARC = mock(AudioRecordingConfiguration.class);
- when(mockARC.getClientUid()).thenReturn(TEST_UID_OTHER);
- when(mockARC.isClientSilenced()).thenReturn(true);
+ // Multiple recording configs, which are all silenced
+ AudioRecordingConfiguration mockARCOne = mock(AudioRecordingConfiguration.class);
+ when(mockARCOne.getClientUid()).thenReturn(TEST_UID_OTHER);
+ when(mockARCOne.isClientSilenced()).thenReturn(true);
- mRecordingCallback.onRecordingConfigChanged(List.of(mockARC));
+ AudioRecordingConfiguration mockARCTwo = mock(AudioRecordingConfiguration.class);
+ when(mockARCTwo.getClientUid()).thenReturn(TEST_UID_OTHER);
+ when(mockARCTwo.isClientSilenced()).thenReturn(true);
+
+ mRecordingCallback.onRecordingConfigChanged(List.of(mockARCOne, mockARCTwo));
mTestableLooper.processAllMessages();
InOrder inOrder = inOrder(mCallback);
@@ -1009,6 +1023,8 @@
micOpCode, TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
inOrder.verify(mCallback).onActiveStateChanged(
micOpCode, TEST_UID_OTHER, TEST_PACKAGE_NAME, false);
+ // For consistency since this runs in a loop
+ mController.removeCallback(new int[]{micOpCode}, mCallback);
}
private void verifySingleActiveOps(int op) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
index 07e48b9..bf4ef50 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
@@ -125,6 +125,8 @@
private static final boolean VOLUME_FIXED_TRUE = true;
private static final int LATCH_COUNT_DOWN_TIME_IN_SECOND = 5;
private static final int LATCH_TIME_OUT_TIME_IN_SECOND = 10;
+ private static final String PRODUCT_NAME_BUILTIN_MIC = "Built-in Mic";
+ private static final String PRODUCT_NAME_WIRED_HEADSET = "My Wired Headset";
@Mock
private DialogTransitionAnimator mDialogTransitionAnimator;
@@ -568,7 +570,8 @@
AudioDeviceInfo.TYPE_BUILTIN_MIC,
MAX_VOLUME,
CURRENT_VOLUME,
- VOLUME_FIXED_TRUE);
+ VOLUME_FIXED_TRUE,
+ PRODUCT_NAME_BUILTIN_MIC);
final MediaDevice mediaDevice4 =
InputMediaDevice.create(
mContext,
@@ -576,7 +579,8 @@
AudioDeviceInfo.TYPE_WIRED_HEADSET,
MAX_VOLUME,
CURRENT_VOLUME,
- VOLUME_FIXED_TRUE);
+ VOLUME_FIXED_TRUE,
+ PRODUCT_NAME_WIRED_HEADSET);
final List<MediaDevice> inputDevices = new ArrayList<>();
inputDevices.add(mediaDevice3);
inputDevices.add(mediaDevice4);
@@ -1355,7 +1359,8 @@
AudioDeviceInfo.TYPE_BUILTIN_MIC,
MAX_VOLUME,
CURRENT_VOLUME,
- VOLUME_FIXED_TRUE);
+ VOLUME_FIXED_TRUE,
+ PRODUCT_NAME_BUILTIN_MIC);
mMediaSwitchingController.connectDevice(inputMediaDevice);
CountDownLatch latch = new CountDownLatch(LATCH_COUNT_DOWN_TIME_IN_SECOND);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index 4b6e313..328d310 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
@@ -50,8 +50,8 @@
import com.android.systemui.flags.FakeFeatureFlagsClassic
import com.android.systemui.flags.Flags
import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.TableLogBufferFactory
+import com.android.systemui.log.table.logcatTableLogBuffer
import com.android.systemui.statusbar.connectivity.WifiPickerTrackerFactory
import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger
@@ -66,6 +66,7 @@
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl
+import com.android.systemui.testKosmos
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
@@ -104,6 +105,7 @@
// to run the callback and this makes the looper place nicely with TestScope etc.
@TestableLooper.RunWithLooper
class MobileConnectionsRepositoryTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
private val flags =
FakeFeatureFlagsClassic().also { it.set(Flags.ROAMING_INDICATOR_VIA_DISPLAY_INFO, true) }
@@ -120,13 +122,13 @@
@Mock private lateinit var subscriptionManager: SubscriptionManager
@Mock private lateinit var telephonyManager: TelephonyManager
@Mock private lateinit var logger: MobileInputLogger
- @Mock private lateinit var summaryLogger: TableLogBuffer
+ private val summaryLogger = logcatTableLogBuffer(kosmos, "summaryLogger")
@Mock private lateinit var logBufferFactory: TableLogBufferFactory
@Mock private lateinit var updateMonitor: KeyguardUpdateMonitor
@Mock private lateinit var wifiManager: WifiManager
@Mock private lateinit var wifiPickerTrackerFactory: WifiPickerTrackerFactory
@Mock private lateinit var wifiPickerTracker: WifiPickerTracker
- @Mock private lateinit var wifiTableLogBuffer: TableLogBuffer
+ private val wifiTableLogBuffer = logcatTableLogBuffer(kosmos, "wifiTableLog")
private val mobileMappings = FakeMobileMappingsProxy()
private val subscriptionManagerProxy = FakeSubscriptionManagerProxy()
@@ -153,7 +155,7 @@
}
whenever(logBufferFactory.getOrCreate(anyString(), anyInt())).thenAnswer { _ ->
- mock<TableLogBuffer>()
+ logcatTableLogBuffer(kosmos, "test")
}
whenever(wifiPickerTrackerFactory.create(any(), capture(wifiPickerTrackerCallback), any()))
@@ -606,10 +608,7 @@
// WHEN an appropriate intent gets sent out
val intent = serviceStateIntent(subId = -1)
- fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
- context,
- intent,
- )
+ fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
runCurrent()
// THEN the repo's state is updated despite no listeners
@@ -636,10 +635,7 @@
// GIVEN a broadcast goes out for the appropriate subID
val intent = serviceStateIntent(subId = -1)
- fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
- context,
- intent,
- )
+ fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
runCurrent()
// THEN the device is in ECM, because one of the service states is
@@ -666,10 +662,7 @@
// GIVEN a broadcast goes out for the appropriate subID
val intent = serviceStateIntent(subId = -1)
- fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
- context,
- intent,
- )
+ fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
runCurrent()
// THEN the device is in ECM, because one of the service states is
@@ -820,17 +813,9 @@
// Get repos to trigger creation
underTest.getRepoForSubId(SUB_1_ID)
- verify(logBufferFactory)
- .getOrCreate(
- eq(tableBufferLogName(SUB_1_ID)),
- anyInt(),
- )
+ verify(logBufferFactory).getOrCreate(eq(tableBufferLogName(SUB_1_ID)), anyInt())
underTest.getRepoForSubId(SUB_2_ID)
- verify(logBufferFactory)
- .getOrCreate(
- eq(tableBufferLogName(SUB_2_ID)),
- anyInt(),
- )
+ verify(logBufferFactory).getOrCreate(eq(tableBufferLogName(SUB_2_ID)), anyInt())
}
@Test
@@ -1578,9 +1563,7 @@
* To properly mimic telephony manager, create a service state, and then turn it into an
* intent
*/
- private fun serviceStateIntent(
- subId: Int,
- ): Intent {
+ private fun serviceStateIntent(subId: Int): Intent {
return Intent(Intent.ACTION_SERVICE_STATE).apply {
putExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, subId)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
index fd4b77d..44e1437 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
@@ -26,19 +26,20 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logcatTableLogBuffer
import com.android.systemui.statusbar.connectivity.WifiPickerTrackerFactory
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl.Companion.WIFI_NETWORK_DEFAULT
import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiScanEntry
+import com.android.systemui.testKosmos
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.time.FakeSystemClock
+import com.android.systemui.util.time.fakeSystemClock
import com.android.wifitrackerlib.HotspotNetworkEntry
import com.android.wifitrackerlib.HotspotNetworkEntry.DeviceType
import com.android.wifitrackerlib.MergedCarrierEntry
@@ -67,6 +68,7 @@
@SmallTest
@TestableLooper.RunWithLooper(setAsMainLooper = true)
class WifiRepositoryImplTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
// Using lazy means that the class will only be constructed once it's fetched. Because the
// repository internally sets some values on construction, we need to set up some test
@@ -84,9 +86,9 @@
)
}
- private val executor = FakeExecutor(FakeSystemClock())
+ private val executor = FakeExecutor(kosmos.fakeSystemClock)
private val logger = LogBuffer("name", maxSize = 100, logcatEchoTracker = mock())
- private val tableLogger = mock<TableLogBuffer>()
+ private val tableLogger = logcatTableLogBuffer(kosmos, "WifiRepositoryImplTest")
private val wifiManager =
mock<WifiManager>().apply { whenever(this.maxSignalLevel).thenReturn(10) }
private val wifiPickerTrackerFactory = mock<WifiPickerTrackerFactory>()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorKosmos.kt
index 0e84273..e470e37 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorKosmos.kt
@@ -19,14 +19,13 @@
import com.android.systemui.brightness.data.repository.screenBrightnessRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.log.table.TableLogBuffer
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.log.table.logcatTableLogBuffer
val Kosmos.screenBrightnessInteractor by
Kosmos.Fixture {
ScreenBrightnessInteractor(
screenBrightnessRepository,
applicationCoroutineScope,
- mock<TableLogBuffer>(),
+ logcatTableLogBuffer(this, "screenBrightness"),
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorKosmos.kt
index 63a1325..db66c3e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorKosmos.kt
@@ -16,6 +16,7 @@
package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor
+import android.content.mockedContext
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.volume.domain.interactor.audioModeInteractor
@@ -27,6 +28,7 @@
val Kosmos.mediaOutputComponentInteractor by
Kosmos.Fixture {
MediaOutputComponentInteractor(
+ mockedContext,
testScope.backgroundScope,
mediaDeviceSessionInteractor,
audioOutputInteractor,
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 e6b52f0..55f0a28 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 @@
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 @@
coroutineScope,
applicationContext,
audioVolumeInteractor,
+ zenModeInteractor,
uiEventLogger,
volumePanelLogger,
)
diff --git a/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java b/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java
index 4b97745..1df5d1a 100644
--- a/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java
+++ b/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java
@@ -138,8 +138,8 @@
DIAGONAL_UP_LEFT_MOVE(KeyEvent.KEYCODE_7),
UP_MOVE_OR_SCROLL(KeyEvent.KEYCODE_8),
DIAGONAL_UP_RIGHT_MOVE(KeyEvent.KEYCODE_9),
- LEFT_MOVE(KeyEvent.KEYCODE_U),
- RIGHT_MOVE(KeyEvent.KEYCODE_O),
+ LEFT_MOVE_OR_SCROLL(KeyEvent.KEYCODE_U),
+ RIGHT_MOVE_OR_SCROLL(KeyEvent.KEYCODE_O),
DIAGONAL_DOWN_LEFT_MOVE(KeyEvent.KEYCODE_J),
DOWN_MOVE_OR_SCROLL(KeyEvent.KEYCODE_K),
DIAGONAL_DOWN_RIGHT_MOVE(KeyEvent.KEYCODE_L),
@@ -267,6 +267,16 @@
);
}
+ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+ private void sendVirtualMouseScrollEvent(float x, float y) {
+ waitForVirtualMouseCreation();
+ mVirtualMouse.sendScrollEvent(new VirtualMouseScrollEvent.Builder()
+ .setXAxisMovement(x)
+ .setYAxisMovement(y)
+ .build()
+ );
+ }
+
/**
* Performs a mouse scroll action based on the provided key code.
* The scroll action will only be performed if the scroll toggle is on.
@@ -284,19 +294,31 @@
private void performMouseScrollAction(int keyCode) {
MouseKeyEvent mouseKeyEvent = MouseKeyEvent.from(
keyCode, mActiveInputDeviceId, mDeviceKeyCodeMap);
- float y = switch (mouseKeyEvent) {
- case UP_MOVE_OR_SCROLL -> MOUSE_SCROLL_STEP;
- case DOWN_MOVE_OR_SCROLL -> -MOUSE_SCROLL_STEP;
- default -> 0.0f;
- };
- waitForVirtualMouseCreation();
- mVirtualMouse.sendScrollEvent(new VirtualMouseScrollEvent.Builder()
- .setYAxisMovement(y)
- .build()
- );
+ float x = 0f;
+ float y = 0f;
+
+ switch (mouseKeyEvent) {
+ case UP_MOVE_OR_SCROLL -> {
+ y = MOUSE_SCROLL_STEP;
+ }
+ case DOWN_MOVE_OR_SCROLL -> {
+ y = -MOUSE_SCROLL_STEP;
+ }
+ case LEFT_MOVE_OR_SCROLL -> {
+ x = MOUSE_SCROLL_STEP;
+ }
+ case RIGHT_MOVE_OR_SCROLL -> {
+ x = -MOUSE_SCROLL_STEP;
+ }
+ default -> {
+ x = 0.0f;
+ y = 0.0f;
+ }
+ }
+ sendVirtualMouseScrollEvent(x, y);
if (DEBUG) {
Slog.d(LOG_TAG, "Performed mouse key event: " + mouseKeyEvent.name()
- + " for scroll action with axis movement (y=" + y + ")");
+ + " for scroll action with axis movement (x=" + x + ", y=" + y + ")");
}
}
@@ -344,8 +366,8 @@
* The method calculates the relative movement of the mouse pointer
* and sends the corresponding event to the virtual mouse.
*
- * The UP and DOWN pointer actions will only take place for their respective keys
- * if the scroll toggle is off.
+ * The UP, DOWN, LEFT, RIGHT pointer actions will only take place for their
+ * respective keys if the scroll toggle is off.
*
* @param keyCode The key code representing the direction or button press.
* Supported keys are:
@@ -353,8 +375,8 @@
* <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_DOWN_LEFT_MOVE}
* <li>{@link MouseKeysInterceptor.MouseKeyEvent#DOWN_MOVE_OR_SCROLL}
* <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_DOWN_RIGHT_MOVE}
- * <li>{@link MouseKeysInterceptor.MouseKeyEvent#LEFT_MOVE}
- * <li>{@link MouseKeysInterceptor.MouseKeyEvent#RIGHT_MOVE}
+ * <li>{@link MouseKeysInterceptor.MouseKeyEvent#LEFT_MOVE_OR_SCROLL}
+ * <li>{@link MouseKeysInterceptor.MouseKeyEvent#RIGHT_MOVE_OR_SCROLL}
* <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_UP_LEFT_MOVE}
* <li>{@link MouseKeysInterceptor.MouseKeyEvent#UP_MOVE_OR_SCROLL}
* <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_UP_RIGHT_MOVE}
@@ -381,10 +403,10 @@
x = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
y = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
}
- case LEFT_MOVE -> {
+ case LEFT_MOVE_OR_SCROLL -> {
x = -MOUSE_POINTER_MOVEMENT_STEP;
}
- case RIGHT_MOVE -> {
+ case RIGHT_MOVE_OR_SCROLL -> {
x = MOUSE_POINTER_MOVEMENT_STEP;
}
case DIAGONAL_UP_LEFT_MOVE -> {
@@ -424,7 +446,9 @@
private boolean isMouseScrollKey(int keyCode, InputDevice inputDevice) {
return keyCode == MouseKeyEvent.UP_MOVE_OR_SCROLL.getKeyCode(inputDevice)
- || keyCode == MouseKeyEvent.DOWN_MOVE_OR_SCROLL.getKeyCode(inputDevice);
+ || keyCode == MouseKeyEvent.DOWN_MOVE_OR_SCROLL.getKeyCode(inputDevice)
+ || keyCode == MouseKeyEvent.LEFT_MOVE_OR_SCROLL.getKeyCode(inputDevice)
+ || keyCode == MouseKeyEvent.RIGHT_MOVE_OR_SCROLL.getKeyCode(inputDevice);
}
/**
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionDumpHelper.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionDumpHelper.java
index 9fc413f..a832545 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 @@
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 6d350e6..c5fef19 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 @@
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 45cbdb4..c38ff14 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 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 @@
* 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 c3be342..c8bc538 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 @@
}
});
}
+
+ @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 bbf6c0b..96be769 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 @@
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 7820390..129be65 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 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 @@
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 @@
try {
service.executeAppFunction(
mRequestInternal.getClientRequest(),
+ mRequestInternal.getCallingPackage(),
mCancellationCallback,
new IExecuteAppFunctionCallback.Stub() {
@Override
@@ -88,7 +74,7 @@
@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/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index e8f7b5f..776a345 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -2553,7 +2553,7 @@
double cachedRestoreThreshold =
mProcessList.getCachedRestoreThresholdKb() * thresholdModifier;
- if (isLastMemoryLevelNormal() && lastPssOrRss >= cachedRestoreThreshold) {
+ if (!isLastMemoryLevelNormal() && lastPssOrRss >= cachedRestoreThreshold) {
state.setServiceHighRam(true);
state.setServiceB(true);
//Slog.i(TAG, "ADJ " + app + " high ram!");
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index e0cf96f..e97629b 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -72,6 +72,10 @@
import static android.content.pm.PermissionInfo.PROTECTION_FLAG_APPOP;
import static android.permission.flags.Flags.deviceAwareAppOpNewSchemaEnabled;
+import static com.android.internal.util.FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED;
+import static com.android.internal.util.FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__CHECK_OPERATION;
+import static com.android.internal.util.FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_OPERATION;
+import static com.android.internal.util.FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_PROXY_OPERATION;
import static com.android.server.appop.AppOpsService.ModeCallback.ALL_OPS;
import android.Manifest;
@@ -160,6 +164,7 @@
import com.android.internal.pm.pkg.component.ParsedAttribution;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
+import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.Preconditions;
import com.android.internal.util.XmlUtils;
import com.android.internal.util.function.pooled.PooledLambda;
@@ -2829,12 +2834,26 @@
@Override
public int checkOperation(int code, int uid, String packageName) {
+ if (Binder.getCallingPid() != Process.myPid()
+ && Flags.appopAccessTrackingLoggingEnabled()) {
+ FrameworkStatsLog.write(
+ APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED, uid, code,
+ APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__CHECK_OPERATION,
+ false);
+ }
return mCheckOpsDelegateDispatcher.checkOperation(code, uid, packageName, null,
Context.DEVICE_ID_DEFAULT, false /*raw*/);
}
@Override
public int checkOperationForDevice(int code, int uid, String packageName, int virtualDeviceId) {
+ if (Binder.getCallingPid() != Process.myPid()
+ && Flags.appopAccessTrackingLoggingEnabled()) {
+ FrameworkStatsLog.write(
+ APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED, uid, code,
+ APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__CHECK_OPERATION,
+ false);
+ }
return mCheckOpsDelegateDispatcher.checkOperation(code, uid, packageName, null,
virtualDeviceId, false /*raw*/);
}
@@ -3015,6 +3034,13 @@
public SyncNotedAppOp noteProxyOperationWithState(int code,
AttributionSourceState attributionSourceState, boolean shouldCollectAsyncNotedOp,
String message, boolean shouldCollectMessage, boolean skipProxyOperation) {
+ if (Binder.getCallingPid() != Process.myPid()
+ && Flags.appopAccessTrackingLoggingEnabled()) {
+ FrameworkStatsLog.write(
+ APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED, attributionSourceState.uid, code,
+ APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_PROXY_OPERATION,
+ attributionSourceState.attributionTag != null);
+ }
AttributionSource attributionSource = new AttributionSource(attributionSourceState);
return mCheckOpsDelegateDispatcher.noteProxyOperation(code, attributionSource,
shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation);
@@ -3096,6 +3122,13 @@
public SyncNotedAppOp noteOperation(int code, int uid, String packageName,
String attributionTag, boolean shouldCollectAsyncNotedOp, String message,
boolean shouldCollectMessage) {
+ if (Binder.getCallingPid() != Process.myPid()
+ && Flags.appopAccessTrackingLoggingEnabled()) {
+ FrameworkStatsLog.write(
+ APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED, uid, code,
+ APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_OPERATION,
+ attributionTag != null);
+ }
return mCheckOpsDelegateDispatcher.noteOperation(code, uid, packageName,
attributionTag, Context.DEVICE_ID_DEFAULT, shouldCollectAsyncNotedOp, message,
shouldCollectMessage);
diff --git a/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java b/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java
index 03c8156..ed41f2e 100644
--- a/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java
+++ b/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java
@@ -36,6 +36,7 @@
import static android.app.AppOpsManager.UID_STATE_MAX_LAST_NON_RESTRICTED;
import static android.app.AppOpsManager.UID_STATE_NONEXISTENT;
import static android.app.AppOpsManager.UID_STATE_TOP;
+import static android.permission.flags.Flags.delayUidStateChangesFromCapabilityUpdates;
import static android.permission.flags.Flags.finishRunningOpsForKilledPackages;
import static com.android.server.appop.AppOpsUidStateTracker.processStateToUidState;
@@ -236,20 +237,26 @@
mPendingUidStates.put(uid, uidState);
mPendingCapability.put(uid, capability);
+ boolean hasLostCapability = (prevCapability & ~capability) != 0;
+
if (procState == PROCESS_STATE_NONEXISTENT) {
mPendingGone.put(uid, true);
commitUidPendingState(uid);
- } else if (uidState < prevUidState
- || (uidState <= UID_STATE_MAX_LAST_NON_RESTRICTED
- && prevUidState > UID_STATE_MAX_LAST_NON_RESTRICTED)) {
+ } else if (uidState < prevUidState) {
// We are moving to a more important state, or the new state may be in the
// foreground and the old state is in the background, then always do it
// immediately.
commitUidPendingState(uid);
- } else if (uidState == prevUidState && capability != prevCapability) {
+ } else if (delayUidStateChangesFromCapabilityUpdates()
+ && uidState == prevUidState && !hasLostCapability) {
+ // No change on process state, but process capability hasn't decreased.
+ commitUidPendingState(uid);
+ } else if (!delayUidStateChangesFromCapabilityUpdates()
+ && uidState == prevUidState && capability != prevCapability) {
// No change on process state, but process capability has changed.
commitUidPendingState(uid);
- } else if (uidState <= UID_STATE_MAX_LAST_NON_RESTRICTED) {
+ } else if (uidState <= UID_STATE_MAX_LAST_NON_RESTRICTED
+ && (!delayUidStateChangesFromCapabilityUpdates() || !hasLostCapability)) {
// We are moving to a less important state, but it doesn't cross the restriction
// threshold.
commitUidPendingState(uid);
diff --git a/services/core/java/com/android/server/crashrecovery/CrashRecoveryUtils.java b/services/core/java/com/android/server/crashrecovery/CrashRecoveryUtils.java
index 3eb3380..2e2a937 100644
--- a/services/core/java/com/android/server/crashrecovery/CrashRecoveryUtils.java
+++ b/services/core/java/com/android/server/crashrecovery/CrashRecoveryUtils.java
@@ -18,7 +18,7 @@
import android.os.Environment;
import android.util.IndentingPrintWriter;
-import android.util.Slog;
+import android.util.Log;
import java.io.BufferedReader;
import java.io.File;
@@ -41,7 +41,7 @@
/** Persist recovery related events in crashrecovery events file.**/
public static void logCrashRecoveryEvent(int priority, String msg) {
- Slog.println(priority, TAG, msg);
+ Log.println(priority, TAG, msg);
try {
File fname = getCrashRecoveryEventsFile();
synchronized (sFileLock) {
@@ -52,7 +52,7 @@
pw.close();
}
} catch (IOException e) {
- Slog.e(TAG, "Unable to log CrashRecoveryEvents " + e.getMessage());
+ Log.e(TAG, "Unable to log CrashRecoveryEvents " + e.getMessage());
}
}
@@ -72,7 +72,7 @@
pw.println(line);
}
} catch (IOException e) {
- Slog.e(TAG, "Unable to dump CrashRecoveryEvents " + e.getMessage());
+ Log.e(TAG, "Unable to dump CrashRecoveryEvents " + e.getMessage());
}
}
pw.decreaseIndent();
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 8d96ba9..c4e1036 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 @@
* <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 @@
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 @@
/** 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 @@
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 85e81f9..1a18b00 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 @@
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 @@
};
mPowerThrottlingConfigData = powerData.getPowerThrottlingConfigData();
if (mPowerThrottlingConfigData != null) {
- mCustomAnimationRateSecDeviceConfig = mPowerThrottlingConfigData.customAnimationRateSec;
+ mCustomAnimationRateDeviceConfig = mPowerThrottlingConfigData.customAnimationRate;
}
mThermalLevelListener = new ThermalLevelListener(handler);
mPmicMonitor =
@@ -228,10 +228,6 @@
}
mPowerThrottlingConfigData = data.getPowerThrottlingConfigData();
- if (mPowerThrottlingConfigData == null) {
- Slog.d(TAG,
- "Power throttling data is missing for configuration data.");
- }
}
private void recalculateBrightnessCap() {
@@ -282,13 +278,13 @@
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 @@
+ mPowerThrottlingConfigData.pollingWindowMinMillis + " msec.");
return;
}
- mCustomAnimationRateSecDeviceConfig = mPowerThrottlingConfigData.customAnimationRateSec;
+ mCustomAnimationRateDeviceConfig = mPowerThrottlingConfigData.customAnimationRate;
mThermalLevelListener.start();
}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 6c2d4f7..88334eb 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -12577,18 +12577,31 @@
// Checks if this is a request to notify system UI about a notification that
// has been lifetime extended.
- // (We only need to check old for the flag, because in both cancellation and
- // update cases, old should have the flag, whereas in update cases the
- // new will NOT have the flag.)
- // If it is such a request, and this is system UI, we send the post request
- // only to System UI, and break as we don't need to continue checking other
- // Managed Services.
- if (info.isSystemUi() && old != null && old.getNotification() != null
+ // We check both old and new for the flag, to avoid catching updates
+ // (where new will not have the flag).
+ // If it is such a request, and this is the system UI listener, we send
+ // the post request. If it's any other listener, we skip it.
+ if (old != null && old.getNotification() != null
&& (old.getNotification().flags
+ & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0
+ && sbn != null && sbn.getNotification() != null
+ && (sbn.getNotification().flags
& FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) {
- final NotificationRankingUpdate update = makeRankingUpdateLocked(info);
- listenerCalls.add(() -> notifyPosted(info, sbnToPost, update));
- break;
+ if (info.isSystemUi()) {
+ final NotificationRankingUpdate update =
+ makeRankingUpdateLocked(info);
+ listenerCalls.add(() -> notifyPosted(info, sbnToPost, update));
+ break;
+ } else {
+ // Skipping because this is the direct-reply "update" and we only
+ // need to send it to sysui, so we immediately continue, before it
+ // can get sent to other listeners below.
+ if (DBG) {
+ Slog.d(TAG, "prepareNotifyPostedLocked: direct reply update, "
+ + "skipping post to " + info.toString());
+ }
+ continue;
+ }
}
}
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 4665a72..b228bb9 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 @@
}
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 @@
}
}
+ 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 0d420a5..dcb47a7 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 @@
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 35ec5ad..0580d4a 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.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 @@
* 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 2229807..82c7a93 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 @@
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 776de2e..20c69ac 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 110a5a2..a8f18b3 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 @@
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/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 2be999f..7e450dd 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -11874,75 +11874,51 @@
throw new IllegalArgumentException("Invalid package name: " + validationResult);
}
- if (Flags.setApplicationRestrictionsCoexistence()) {
- EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin(
- who,
- MANAGE_DEVICE_POLICY_APP_RESTRICTIONS,
- caller.getPackageName(),
- caller.getUserId()
- );
-
+ final boolean isRoleHolder;
+ if (who != null) {
+ // DO or PO
+ Preconditions.checkCallAuthorization(
+ (isProfileOwner(caller) || isDefaultDeviceOwner(caller)));
+ Preconditions.checkCallAuthorization(!parent,
+ "DO or PO cannot call this on parent");
+ // Caller has opted to be treated as DPC (by passing a non-null who), so don't
+ // consider it as the DMRH, even if the caller is both the DPC and the DMRH.
+ isRoleHolder = false;
+ } else {
+ // Delegates, or the DMRH. Only DMRH can call this on COPE parent
+ isRoleHolder = isCallerDevicePolicyManagementRoleHolder(caller);
+ if (parent) {
+ Preconditions.checkCallAuthorization(isRoleHolder);
+ Preconditions.checkState(isOrganizationOwnedDeviceWithManagedProfile(),
+ "Role Holder can only operate parent app restriction on COPE devices");
+ } else {
+ Preconditions.checkCallAuthorization(isRoleHolder
+ || isCallerDelegate(caller, DELEGATION_APP_RESTRICTIONS));
+ }
+ }
+ // DMRH caller uses policy engine, others still use legacy code path
+ if (isRoleHolder) {
+ EnforcingAdmin enforcingAdmin = getEnforcingAdminForCaller(/* who */ null,
+ caller.getPackageName());
+ int affectedUserId = parent
+ ? getProfileParentId(caller.getUserId()) : caller.getUserId();
if (restrictions == null || restrictions.isEmpty()) {
mDevicePolicyEngine.removeLocalPolicy(
PolicyDefinition.APPLICATION_RESTRICTIONS(packageName),
enforcingAdmin,
- caller.getUserId());
+ affectedUserId);
} else {
mDevicePolicyEngine.setLocalPolicy(
PolicyDefinition.APPLICATION_RESTRICTIONS(packageName),
enforcingAdmin,
new BundlePolicyValue(restrictions),
- caller.getUserId());
+ affectedUserId);
}
- setBackwardsCompatibleAppRestrictions(
- caller, packageName, restrictions, caller.getUserHandle());
} else {
- final boolean isRoleHolder;
- if (who != null) {
- // DO or PO
- Preconditions.checkCallAuthorization(
- (isProfileOwner(caller) || isDefaultDeviceOwner(caller)));
- Preconditions.checkCallAuthorization(!parent,
- "DO or PO cannot call this on parent");
- // Caller has opted to be treated as DPC (by passing a non-null who), so don't
- // consider it as the DMRH, even if the caller is both the DPC and the DMRH.
- isRoleHolder = false;
- } else {
- // Delegates, or the DMRH. Only DMRH can call this on COPE parent
- isRoleHolder = isCallerDevicePolicyManagementRoleHolder(caller);
- if (parent) {
- Preconditions.checkCallAuthorization(isRoleHolder);
- Preconditions.checkState(isOrganizationOwnedDeviceWithManagedProfile(),
- "Role Holder can only operate parent app restriction on COPE devices");
- } else {
- Preconditions.checkCallAuthorization(isRoleHolder
- || isCallerDelegate(caller, DELEGATION_APP_RESTRICTIONS));
- }
- }
- // DMRH caller uses policy engine, others still use legacy code path
- if (isRoleHolder) {
- EnforcingAdmin enforcingAdmin = getEnforcingAdminForCaller(/* who */ null,
- caller.getPackageName());
- int affectedUserId = parent
- ? getProfileParentId(caller.getUserId()) : caller.getUserId();
- if (restrictions == null || restrictions.isEmpty()) {
- mDevicePolicyEngine.removeLocalPolicy(
- PolicyDefinition.APPLICATION_RESTRICTIONS(packageName),
- enforcingAdmin,
- affectedUserId);
- } else {
- mDevicePolicyEngine.setLocalPolicy(
- PolicyDefinition.APPLICATION_RESTRICTIONS(packageName),
- enforcingAdmin,
- new BundlePolicyValue(restrictions),
- affectedUserId);
- }
- } else {
- mInjector.binderWithCleanCallingIdentity(() -> {
- mUserManager.setApplicationRestrictions(packageName, restrictions,
- caller.getUserHandle());
- });
- }
+ mInjector.binderWithCleanCallingIdentity(() -> {
+ mUserManager.setApplicationRestrictions(packageName, restrictions,
+ caller.getUserHandle());
+ });
}
DevicePolicyEventLogger
@@ -11953,31 +11929,6 @@
.write();
}
- /**
- * Set app restrictions in user manager for DPC callers only to keep backwards compatibility
- * for the old getApplicationRestrictions API.
- */
- private void setBackwardsCompatibleAppRestrictions(
- CallerIdentity caller, String packageName, Bundle restrictions, UserHandle userHandle) {
- if ((caller.hasAdminComponent() && (isProfileOwner(caller) || isDefaultDeviceOwner(caller)))
- || (caller.hasPackage() && isCallerDelegate(caller, DELEGATION_APP_RESTRICTIONS))) {
- Bundle restrictionsToApply = restrictions == null || restrictions.isEmpty()
- ? getAppRestrictionsSetByAnyAdmin(packageName, userHandle)
- : restrictions;
- mInjector.binderWithCleanCallingIdentity(() -> {
- mUserManager.setApplicationRestrictions(packageName, restrictionsToApply,
- userHandle);
- });
- } else {
- // Notify package of changes via an intent - only sent to explicitly registered
- // receivers. Sending here because For DPCs, this is being sent in UMS.
- final Intent changeIntent = new Intent(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED);
- changeIntent.setPackage(packageName);
- changeIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
- mContext.sendBroadcastAsUser(changeIntent, userHandle);
- }
- }
-
private Bundle getAppRestrictionsSetByAnyAdmin(String packageName, UserHandle userHandle) {
LinkedHashMap<EnforcingAdmin, PolicyValue<Bundle>> policies =
mDevicePolicyEngine.getLocalPoliciesSetByAdmins(
@@ -13257,68 +13208,47 @@
String packageName, boolean parent) {
final CallerIdentity caller = getCallerIdentity(who, callerPackage);
- // IMPORTANT: The code behind the if branch is OUTDATED and requires additional work before
- // enabling the feature flag below.
- // TODO(b/369141952): Update DPM.getApplicationRestrictions coexistence code
- if (Flags.setApplicationRestrictionsCoexistence()) {
- EnforcingAdmin enforcingAdmin = enforceCanQueryAndGetEnforcingAdmin(
- who,
- MANAGE_DEVICE_POLICY_APP_RESTRICTIONS,
- caller.getPackageName(),
- caller.getUserId()
- );
-
+ final boolean isRoleHolder;
+ if (who != null) {
+ // Caller is DO or PO. They cannot call this on parent
+ Preconditions.checkCallAuthorization(!parent
+ && (isProfileOwner(caller) || isDefaultDeviceOwner(caller)));
+ // Caller has opted to be treated as DPC (by passing a non-null who), so don't
+ // consider it as the DMRH, even if the caller is both the DPC and the DMRH.
+ isRoleHolder = false;
+ } else {
+ // Caller is delegates or the DMRH. Only DMRH can call this on parent
+ isRoleHolder = isCallerDevicePolicyManagementRoleHolder(caller);
+ if (parent) {
+ Preconditions.checkCallAuthorization(isRoleHolder);
+ Preconditions.checkState(isOrganizationOwnedDeviceWithManagedProfile(),
+ "Role Holder can only operate parent app restriction on COPE devices");
+ } else {
+ Preconditions.checkCallAuthorization(isRoleHolder
+ || isCallerDelegate(caller, DELEGATION_APP_RESTRICTIONS));
+ }
+ }
+ if (isRoleHolder) {
+ EnforcingAdmin enforcingAdmin = getEnforcingAdminForCaller(/* who */ null,
+ caller.getPackageName());
+ int affectedUserId = parent
+ ? getProfileParentId(caller.getUserId()) : caller.getUserId();
LinkedHashMap<EnforcingAdmin, PolicyValue<Bundle>> policies =
mDevicePolicyEngine.getLocalPoliciesSetByAdmins(
PolicyDefinition.APPLICATION_RESTRICTIONS(packageName),
- caller.getUserId());
- if (policies.isEmpty() || !policies.containsKey(enforcingAdmin)) {
+ affectedUserId);
+ if (!policies.containsKey(enforcingAdmin)) {
return Bundle.EMPTY;
}
return policies.get(enforcingAdmin).getValue();
} else {
- final boolean isRoleHolder;
- if (who != null) {
- // Caller is DO or PO. They cannot call this on parent
- Preconditions.checkCallAuthorization(!parent
- && (isProfileOwner(caller) || isDefaultDeviceOwner(caller)));
- // Caller has opted to be treated as DPC (by passing a non-null who), so don't
- // consider it as the DMRH, even if the caller is both the DPC and the DMRH.
- isRoleHolder = false;
- } else {
- // Caller is delegates or the DMRH. Only DMRH can call this on parent
- isRoleHolder = isCallerDevicePolicyManagementRoleHolder(caller);
- if (parent) {
- Preconditions.checkCallAuthorization(isRoleHolder);
- Preconditions.checkState(isOrganizationOwnedDeviceWithManagedProfile(),
- "Role Holder can only operate parent app restriction on COPE devices");
- } else {
- Preconditions.checkCallAuthorization(isRoleHolder
- || isCallerDelegate(caller, DELEGATION_APP_RESTRICTIONS));
- }
- }
- if (isRoleHolder) {
- EnforcingAdmin enforcingAdmin = getEnforcingAdminForCaller(/* who */ null,
- caller.getPackageName());
- int affectedUserId = parent
- ? getProfileParentId(caller.getUserId()) : caller.getUserId();
- LinkedHashMap<EnforcingAdmin, PolicyValue<Bundle>> policies =
- mDevicePolicyEngine.getLocalPoliciesSetByAdmins(
- PolicyDefinition.APPLICATION_RESTRICTIONS(packageName),
- affectedUserId);
- if (!policies.containsKey(enforcingAdmin)) {
- return Bundle.EMPTY;
- }
- return policies.get(enforcingAdmin).getValue();
- } else {
- return mInjector.binderWithCleanCallingIdentity(() -> {
- Bundle bundle = mUserManager.getApplicationRestrictions(packageName,
- caller.getUserHandle());
- // if no restrictions were saved, mUserManager.getApplicationRestrictions
- // returns null, but DPM method should return an empty Bundle as per JavaDoc
- return bundle != null ? bundle : Bundle.EMPTY;
- });
- }
+ return mInjector.binderWithCleanCallingIdentity(() -> {
+ Bundle bundle = mUserManager.getApplicationRestrictions(packageName,
+ caller.getUserHandle());
+ // if no restrictions were saved, mUserManager.getApplicationRestrictions
+ // returns null, but DPM method should return an empty Bundle as per JavaDoc
+ return bundle != null ? bundle : Bundle.EMPTY;
+ });
}
}
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 5758da8..96fb453 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 @@
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 3976ea4..2220f43 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 @@
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 @@
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/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java
index 1731590..026e72f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java
@@ -31,12 +31,14 @@
import static android.app.AppOpsManager.UID_STATE_FOREGROUND_SERVICE;
import static android.app.AppOpsManager.UID_STATE_MAX_LAST_NON_RESTRICTED;
import static android.app.AppOpsManager.UID_STATE_TOP;
+import static android.permission.flags.Flags.delayUidStateChangesFromCapabilityUpdates;
import static com.android.server.appop.AppOpsUidStateTracker.processStateToUidState;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
@@ -325,6 +327,10 @@
.backgroundState()
.update();
+ assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+ assertEquals(MODE_IGNORED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
+
procStateBuilder(UID)
.backgroundState()
.microphoneCapability()
@@ -342,10 +348,23 @@
.microphoneCapability()
.update();
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
+
procStateBuilder(UID)
.backgroundState()
.update();
+ if (delayUidStateChangesFromCapabilityUpdates()) {
+ mClock.advanceTime(mConstants.BG_STATE_SETTLE_TIME - 1);
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO,
+ MODE_FOREGROUND));
+
+ mClock.advanceTime(1);
+ }
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
assertEquals(MODE_IGNORED,
mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
@@ -357,6 +376,8 @@
.backgroundState()
.update();
+ assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
+
procStateBuilder(UID)
.backgroundState()
.cameraCapability()
@@ -372,10 +393,18 @@
.cameraCapability()
.update();
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
+
procStateBuilder(UID)
.backgroundState()
.update();
+ if (delayUidStateChangesFromCapabilityUpdates()) {
+ mClock.advanceTime(mConstants.BG_STATE_SETTLE_TIME - 1);
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
+
+ mClock.advanceTime(1);
+ }
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
}
@@ -385,6 +414,9 @@
.backgroundState()
.update();
+ assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
+ assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+
procStateBuilder(UID)
.backgroundState()
.locationCapability()
@@ -401,15 +433,55 @@
.locationCapability()
.update();
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+
procStateBuilder(UID)
.backgroundState()
.update();
+ if (delayUidStateChangesFromCapabilityUpdates()) {
+ mClock.advanceTime(mConstants.BG_STATE_SETTLE_TIME - 1);
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+
+ mClock.advanceTime(1);
+ }
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
}
@Test
+ public void testProcStateChangesAndStaysUnrestrictedAndCapabilityRemoved() {
+ assumeTrue(delayUidStateChangesFromCapabilityUpdates());
+
+ procStateBuilder(UID)
+ .topState()
+ .microphoneCapability()
+ .cameraCapability()
+ .locationCapability()
+ .update();
+
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
+
+ procStateBuilder(UID)
+ .foregroundState()
+ .update();
+
+ mClock.advanceTime(mConstants.TOP_STATE_SETTLE_TIME - 1);
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
+
+ mClock.advanceTime(1);
+ assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+ assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
+ assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
+ }
+
+ @Test
public void testVisibleAppWidget() {
procStateBuilder(UID)
.backgroundState()
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index 2724149..c645c08 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/accessibility/MouseKeysInterceptorTest.kt b/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt
index c76392b..5134737 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt
+++ b/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt
@@ -236,6 +236,27 @@
}
@Test
+ fun whenScrollToggleOn_ScrollRightKeyIsPressed_scrollEventIsSent() {
+ // There should be some delay between the downTime of the key event and calling onKeyEvent
+ val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS
+ val keyCodeScrollToggle = MouseKeysInterceptor.MouseKeyEvent.SCROLL_TOGGLE.keyCodeValue
+ val keyCodeScroll = MouseKeysInterceptor.MouseKeyEvent.RIGHT_MOVE_OR_SCROLL.keyCodeValue
+
+ val scrollToggleDownEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
+ keyCodeScrollToggle, 0, 0, DEVICE_ID, 0)
+ val scrollDownEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
+ keyCodeScroll, 0, 0, DEVICE_ID, 0)
+
+ mouseKeysInterceptor.onKeyEvent(scrollToggleDownEvent, 0)
+ mouseKeysInterceptor.onKeyEvent(scrollDownEvent, 0)
+ testLooper.dispatchAll()
+
+ // Verify the sendScrollEvent method is called once and capture the arguments
+ verifyScrollEvents(arrayOf<Float>(-MouseKeysInterceptor.MOUSE_SCROLL_STEP),
+ arrayOf<Float>(0f))
+ }
+
+ @Test
fun whenScrollToggleOff_DirectionalUpKeyIsPressed_RelativeEventIsSent() {
// There should be some delay between the downTime of the key event and calling onKeyEvent
val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS
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 c970a3e..840e5c5 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 @@
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 7f2327aa..e3eca6d 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 @@
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 1abd4eb..b0846f6 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.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.runner.RunWith;
import org.mockito.InOrder;
-import java.util.concurrent.atomic.AtomicInteger;
-
/**
* Tests watching noted ops.
*/
@@ -51,7 +47,7 @@
@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 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 @@
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 8a6ba4d..d46fb90 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 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.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 @@
@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 @@
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 62f5edc..dad45b3 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.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.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 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 @@
@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 @@
/* 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 425bb15..7e22d74 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 @@
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 @@
} 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/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
index 983e694..b34b1fb 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
@@ -835,7 +835,7 @@
}
@Test
- public void testListenerPost_UpdateLifetimeExtended() throws Exception {
+ public void testListenerPostLifetimeExtended_UpdatesOnlySysui() throws Exception {
mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);
// Create original notification, with FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY.
@@ -856,31 +856,51 @@
Notification.Builder nb2 = new Notification.Builder(mContext, channel.getId())
.setContentTitle("new title")
.setSmallIcon(android.R.drawable.sym_def_app_icon)
- .setFlag(Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, false);
+ .setFlag(Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true);
StatusBarNotification sbn2 = new StatusBarNotification(pkg, pkg, 8, "tag", uid, 0,
nb2.build(), userHandle, null, 0);
NotificationRecord toPost = new NotificationRecord(mContext, sbn2, channel);
// Create system ui-like service.
- ManagedServices.ManagedServiceInfo info = mListeners.new ManagedServiceInfo(
+ ManagedServices.ManagedServiceInfo sysuiInfo = mListeners.new ManagedServiceInfo(
null, new ComponentName("a", "a"), sbn2.getUserId(), false, null, 33, 33);
- info.isSystemUi = true;
- INotificationListener l1 = mock(INotificationListener.class);
- info.service = l1;
- List<ManagedServices.ManagedServiceInfo> services = ImmutableList.of(info);
+ sysuiInfo.isSystemUi = true;
+ INotificationListener sysuiListener = mock(INotificationListener.class);
+ sysuiInfo.service = sysuiListener;
+
+ // Create two non-system ui-like services.
+ ManagedServices.ManagedServiceInfo otherInfo1 = mListeners.new ManagedServiceInfo(
+ null, new ComponentName("b", "b"), sbn2.getUserId(), false, null, 33, 33);
+ otherInfo1.isSystemUi = false;
+ INotificationListener otherListener1 = mock(INotificationListener.class);
+ otherInfo1.service = otherListener1;
+
+ ManagedServices.ManagedServiceInfo otherInfo2 = mListeners.new ManagedServiceInfo(
+ null, new ComponentName("c", "c"), sbn2.getUserId(), false, null, 33, 33);
+ otherInfo2.isSystemUi = false;
+ INotificationListener otherListener2 = mock(INotificationListener.class);
+ otherInfo2.service = otherListener2;
+
+ List<ManagedServices.ManagedServiceInfo> services = ImmutableList.of(otherInfo1, sysuiInfo,
+ otherInfo2);
when(mListeners.getServices()).thenReturn(services);
FieldSetter.setField(mNm,
NotificationManagerService.class.getDeclaredField("mHandler"),
mock(NotificationManagerService.WorkerHandler.class));
doReturn(true).when(mNm).isVisibleToListener(any(), anyInt(), any());
- doReturn(mock(NotificationRankingUpdate.class)).when(mNm).makeRankingUpdateLocked(info);
+ doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
+ .makeRankingUpdateLocked(sysuiInfo);
+ doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
+ .makeRankingUpdateLocked(otherInfo1);
+ doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
+ .makeRankingUpdateLocked(otherInfo2);
doReturn(false).when(mNm).isInLockDownMode(anyInt());
doNothing().when(mNm).updateUriPermissions(any(), any(), any(), anyInt());
doReturn(sbn2).when(mListeners).redactStatusBarNotification(sbn2);
doReturn(sbn2).when(mListeners).redactStatusBarNotification(any());
- // The notification change is posted to the service listener.
+ // Post notification change to the service listeners.
mListeners.notifyPostedLocked(toPost, old);
// Verify that the post occcurs with the updated notification value.
@@ -889,11 +909,190 @@
runnableCaptor.getValue().run();
ArgumentCaptor<IStatusBarNotificationHolder> sbnCaptor =
ArgumentCaptor.forClass(IStatusBarNotificationHolder.class);
- verify(l1, times(1)).onNotificationPosted(sbnCaptor.capture(), any());
+ verify(sysuiListener, times(1)).onNotificationPosted(sbnCaptor.capture(), any());
StatusBarNotification sbnResult = sbnCaptor.getValue().get();
assertThat(sbnResult.getNotification()
.extras.getCharSequence(Notification.EXTRA_TITLE).toString())
.isEqualTo("new title");
+
+ verify(otherListener1, never()).onNotificationPosted(any(), any());
+ verify(otherListener2, never()).onNotificationPosted(any(), any());
+ }
+
+ @Test
+ public void testListenerPostLifeimteExtension_postsToAppropriateListeners() throws Exception {
+ mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);
+
+ // Create original notification, with FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY.
+ String pkg = "pkg";
+ int uid = 9;
+ UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
+ NotificationChannel channel = new NotificationChannel("id", "name",
+ NotificationManager.IMPORTANCE_HIGH);
+ Notification.Builder nb = new Notification.Builder(mContext, channel.getId())
+ .setContentTitle("foo")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon)
+ .setFlag(Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true);
+ StatusBarNotification sbn = new StatusBarNotification(pkg, pkg, 8, "tag", uid, 0,
+ nb.build(), userHandle, null, 0);
+ NotificationRecord leRecord = new NotificationRecord(mContext, sbn, channel);
+
+ // Creates updated notification (without FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY)
+ Notification.Builder nb2 = new Notification.Builder(mContext, channel.getId())
+ .setContentTitle("new title")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon)
+ .setFlag(Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, false);
+ StatusBarNotification sbn2 = new StatusBarNotification(pkg, pkg, 8, "tag", uid, 0,
+ nb2.build(), userHandle, null, 0);
+ NotificationRecord nonLeRecord = new NotificationRecord(mContext, sbn2, channel);
+
+ // Create system ui-like service.
+ ManagedServices.ManagedServiceInfo sysuiInfo = mListeners.new ManagedServiceInfo(
+ null, new ComponentName("a", "a"), sbn2.getUserId(), false, null, 33, 33);
+ sysuiInfo.isSystemUi = true;
+ INotificationListener sysuiListener = mock(INotificationListener.class);
+ sysuiInfo.service = sysuiListener;
+
+ // Create two non-system ui-like services.
+ ManagedServices.ManagedServiceInfo otherInfo1 = mListeners.new ManagedServiceInfo(
+ null, new ComponentName("b", "b"), sbn2.getUserId(), false, null, 33, 33);
+ otherInfo1.isSystemUi = false;
+ INotificationListener otherListener1 = mock(INotificationListener.class);
+ otherInfo1.service = otherListener1;
+
+ ManagedServices.ManagedServiceInfo otherInfo2 = mListeners.new ManagedServiceInfo(
+ null, new ComponentName("c", "c"), sbn2.getUserId(), false, null, 33, 33);
+ otherInfo2.isSystemUi = false;
+ INotificationListener otherListener2 = mock(INotificationListener.class);
+ otherInfo2.service = otherListener2;
+
+ List<ManagedServices.ManagedServiceInfo> services = ImmutableList.of(otherInfo1, sysuiInfo,
+ otherInfo2);
+ when(mListeners.getServices()).thenReturn(services);
+
+ FieldSetter.setField(mNm,
+ NotificationManagerService.class.getDeclaredField("mHandler"),
+ mock(NotificationManagerService.WorkerHandler.class));
+ doReturn(true).when(mNm).isVisibleToListener(any(), anyInt(), any());
+ doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
+ .makeRankingUpdateLocked(sysuiInfo);
+ doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
+ .makeRankingUpdateLocked(otherInfo1);
+ doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
+ .makeRankingUpdateLocked(otherInfo2);
+ doReturn(false).when(mNm).isInLockDownMode(anyInt());
+ doNothing().when(mNm).updateUriPermissions(any(), any(), any(), anyInt());
+ doReturn(sbn2).when(mListeners).redactStatusBarNotification(sbn2);
+ doReturn(sbn2).when(mListeners).redactStatusBarNotification(any());
+
+ // The notification change is posted to the service listener.
+ // NonLE to LE should never happen, as LE can't be set in an update by the app.
+ // So we just want to test LE to NonLE.
+ mListeners.notifyPostedLocked(nonLeRecord /*=toPost*/, leRecord /*=old*/);
+
+ // Verify that the post occcurs with the updated notification value.
+ ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+ verify(mNm.mHandler, times(3)).post(runnableCaptor.capture());
+ List<Runnable> capturedRunnable = runnableCaptor.getAllValues();
+ for (Runnable r : capturedRunnable) {
+ r.run();
+ }
+
+ ArgumentCaptor<IStatusBarNotificationHolder> sbnCaptor =
+ ArgumentCaptor.forClass(IStatusBarNotificationHolder.class);
+ verify(sysuiListener, times(1)).onNotificationPosted(sbnCaptor.capture(), any());
+ StatusBarNotification sbnResult = sbnCaptor.getValue().get();
+ assertThat(sbnResult.getNotification()
+ .extras.getCharSequence(Notification.EXTRA_TITLE).toString())
+ .isEqualTo("new title");
+
+ verify(otherListener1, times(1)).onNotificationPosted(any(), any());
+ verify(otherListener2, times(1)).onNotificationPosted(any(), any());
+ }
+
+ @Test
+ public void testNotifyPostedLocked_postsToAppropriateListeners() throws Exception {
+ // Create original notification
+ String pkg = "pkg";
+ int uid = 9;
+ UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
+ NotificationChannel channel = new NotificationChannel("id", "name",
+ NotificationManager.IMPORTANCE_HIGH);
+ Notification.Builder nb = new Notification.Builder(mContext, channel.getId())
+ .setContentTitle("foo")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon);
+ StatusBarNotification sbn = new StatusBarNotification(pkg, pkg, 8, "tag", uid, 0,
+ nb.build(), userHandle, null, 0);
+ NotificationRecord oldRecord = new NotificationRecord(mContext, sbn, channel);
+
+ // Creates updated notification
+ Notification.Builder nb2 = new Notification.Builder(mContext, channel.getId())
+ .setContentTitle("new title")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon);
+ StatusBarNotification sbn2 = new StatusBarNotification(pkg, pkg, 8, "tag", uid, 0,
+ nb2.build(), userHandle, null, 0);
+ NotificationRecord newRecord = new NotificationRecord(mContext, sbn2, channel);
+
+ // Create system ui-like service.
+ ManagedServices.ManagedServiceInfo sysuiInfo = mListeners.new ManagedServiceInfo(
+ null, new ComponentName("a", "a"), sbn2.getUserId(), false, null, 33, 33);
+ sysuiInfo.isSystemUi = true;
+ INotificationListener sysuiListener = mock(INotificationListener.class);
+ sysuiInfo.service = sysuiListener;
+
+ // Create two non-system ui-like services.
+ ManagedServices.ManagedServiceInfo otherInfo1 = mListeners.new ManagedServiceInfo(
+ null, new ComponentName("b", "b"), sbn2.getUserId(), false, null, 33, 33);
+ otherInfo1.isSystemUi = false;
+ INotificationListener otherListener1 = mock(INotificationListener.class);
+ otherInfo1.service = otherListener1;
+
+ ManagedServices.ManagedServiceInfo otherInfo2 = mListeners.new ManagedServiceInfo(
+ null, new ComponentName("c", "c"), sbn2.getUserId(), false, null, 33, 33);
+ otherInfo2.isSystemUi = false;
+ INotificationListener otherListener2 = mock(INotificationListener.class);
+ otherInfo2.service = otherListener2;
+
+ List<ManagedServices.ManagedServiceInfo> services = ImmutableList.of(otherInfo1, sysuiInfo,
+ otherInfo2);
+ when(mListeners.getServices()).thenReturn(services);
+
+ FieldSetter.setField(mNm,
+ NotificationManagerService.class.getDeclaredField("mHandler"),
+ mock(NotificationManagerService.WorkerHandler.class));
+ doReturn(true).when(mNm).isVisibleToListener(any(), anyInt(), any());
+ doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
+ .makeRankingUpdateLocked(sysuiInfo);
+ doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
+ .makeRankingUpdateLocked(otherInfo1);
+ doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
+ .makeRankingUpdateLocked(otherInfo2);
+ doReturn(false).when(mNm).isInLockDownMode(anyInt());
+ doNothing().when(mNm).updateUriPermissions(any(), any(), any(), anyInt());
+ doReturn(sbn2).when(mListeners).redactStatusBarNotification(sbn2);
+ doReturn(sbn2).when(mListeners).redactStatusBarNotification(any());
+
+ // The notification change is posted to the service listeners.
+ mListeners.notifyPostedLocked(newRecord, oldRecord);
+
+ // Verify that the post occcurs with the updated notification value.
+ ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+ verify(mNm.mHandler, times(3)).post(runnableCaptor.capture());
+ List<Runnable> capturedRunnable = runnableCaptor.getAllValues();
+ for (Runnable r : capturedRunnable) {
+ r.run();
+ }
+
+ ArgumentCaptor<IStatusBarNotificationHolder> sbnCaptor =
+ ArgumentCaptor.forClass(IStatusBarNotificationHolder.class);
+ verify(sysuiListener, times(1)).onNotificationPosted(sbnCaptor.capture(), any());
+ StatusBarNotification sbnResult = sbnCaptor.getValue().get();
+ assertThat(sbnResult.getNotification()
+ .extras.getCharSequence(Notification.EXTRA_TITLE).toString())
+ .isEqualTo("new title");
+
+ verify(otherListener1, times(1)).onNotificationPosted(any(), any());
+ verify(otherListener2, times(1)).onNotificationPosted(any(), any());
}
/**
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 f56825f..42e31de 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.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 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.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.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.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 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 @@
}
@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/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
index 1a42e80..19a6ddc 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
@@ -504,6 +504,11 @@
pw.println("\n##Sound Model Stats dump:\n");
mSoundModelStatTracker.dump(pw);
}
+
+ @Override
+ protected void onUnhandledException(int code, int flags, Exception e) {
+ Slog.wtf(TAG, "Unhandled exception code: " + code, e);
+ }
}
class SoundTriggerSessionStub extends ISoundTriggerSession.Stub {
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 634b6ee..8d27c1d 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 @@
@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 f891606..f2e3425 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 e4de2c5..73625da 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 @@
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 0000000..be38c25
--- /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);
+ }
+}