Merge "Reuse StateChange logic in SysUIState" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 2dd16de..4d9fdf0 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -587,6 +587,7 @@
java_aconfig_library {
name: "android.view.inputmethod.flags-aconfig-java",
aconfig_declarations: "android.view.inputmethod.flags-aconfig",
+ host_supported: true,
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
diff --git a/cmds/svc/src/com/android/commands/svc/UsbCommand.java b/cmds/svc/src/com/android/commands/svc/UsbCommand.java
index 26e20f6..6542d08 100644
--- a/cmds/svc/src/com/android/commands/svc/UsbCommand.java
+++ b/cmds/svc/src/com/android/commands/svc/UsbCommand.java
@@ -89,6 +89,11 @@
IUsbManager usbMgr = IUsbManager.Stub.asInterface(ServiceManager.getService(
Context.USB_SERVICE));
+ if (usbMgr == null) {
+ System.err.println("Could not obtain USB service. Try again later.");
+ return;
+ }
+
Executor executor = context.getMainExecutor();
Consumer<Integer> consumer = new Consumer<Integer>(){
public void accept(Integer status){
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index bb023f2..12bfccf 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -4507,7 +4507,7 @@
method @NonNull public android.window.WindowContainerTransaction requestFocusOnTaskFragment(@NonNull android.os.IBinder);
method @NonNull public android.window.WindowContainerTransaction scheduleFinishEnterPip(@NonNull android.window.WindowContainerToken, @NonNull android.graphics.Rect);
method @NonNull public android.window.WindowContainerTransaction setActivityWindowingMode(@NonNull android.window.WindowContainerToken, int);
- method @NonNull public android.window.WindowContainerTransaction setAdjacentRoots(@NonNull android.window.WindowContainerToken, @NonNull android.window.WindowContainerToken);
+ method @Deprecated @NonNull public android.window.WindowContainerTransaction setAdjacentRoots(@NonNull android.window.WindowContainerToken, @NonNull android.window.WindowContainerToken);
method @NonNull public android.window.WindowContainerTransaction setAdjacentTaskFragments(@NonNull android.os.IBinder, @NonNull android.os.IBinder, @Nullable android.window.WindowContainerTransaction.TaskFragmentAdjacentParams);
method @NonNull public android.window.WindowContainerTransaction setAppBounds(@NonNull android.window.WindowContainerToken, @NonNull android.graphics.Rect);
method @NonNull public android.window.WindowContainerTransaction setBounds(@NonNull android.window.WindowContainerToken, @NonNull android.graphics.Rect);
diff --git a/core/java/android/animation/AnimationHandler.java b/core/java/android/animation/AnimationHandler.java
index 5f57a41..d5b2f98 100644
--- a/core/java/android/animation/AnimationHandler.java
+++ b/core/java/android/animation/AnimationHandler.java
@@ -110,8 +110,7 @@
}
};
- public static final ThreadLocal<AnimationHandler> sAnimatorHandler =
- ThreadLocal.withInitial(AnimationHandler::new);
+ public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>();
private static AnimationHandler sTestHandler = null;
private boolean mListDirty = false;
@@ -119,6 +118,9 @@
if (sTestHandler != null) {
return sTestHandler;
}
+ if (sAnimatorHandler.get() == null) {
+ sAnimatorHandler.set(new AnimationHandler());
+ }
return sAnimatorHandler.get();
}
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 73de1b6..c74bd1a 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -6981,6 +6981,8 @@
* <p>The caller must hold the
* {@link android.Manifest.permission#TRIGGER_LOST_MODE} permission.
*
+ * <p>This API accesses the device's location and will only be used when a device is lost.
+ *
* <p>Register a broadcast receiver to receive lost mode location updates. This receiver should
* subscribe to the {@link #ACTION_LOST_MODE_LOCATION_UPDATE} action and receive the location
* from an intent extra {@link #EXTRA_LOST_MODE_LOCATION}.
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index 5c267c9..4afe75f 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -265,6 +265,16 @@
}
flag {
+ name: "expanding_public_view"
+ namespace: "systemui"
+ description: "enables user expanding the public view of a notification"
+ bug: "398853084"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "api_rich_ongoing"
is_exported: true
namespace: "systemui"
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 55d78f9b..cc288b1 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -744,15 +744,22 @@
*/
public static final long BIND_MATCH_QUARANTINED_COMPONENTS = 0x2_0000_0000L;
+ /**
+ * Flag for {@link #bindService} that allows the bound app to be frozen if it is eligible.
+ *
+ * @hide
+ */
+ public static final long BIND_ALLOW_FREEZE = 0x4_0000_0000L;
/**
* These bind flags reduce the strength of the binding such that we shouldn't
* consider it as pulling the process up to the level of the one that is bound to it.
* @hide
*/
- public static final int BIND_REDUCTION_FLAGS =
+ public static final long BIND_REDUCTION_FLAGS =
Context.BIND_ALLOW_OOM_MANAGEMENT | Context.BIND_WAIVE_PRIORITY
- | Context.BIND_NOT_PERCEPTIBLE | Context.BIND_NOT_VISIBLE;
+ | Context.BIND_NOT_PERCEPTIBLE | Context.BIND_NOT_VISIBLE
+ | Context.BIND_ALLOW_FREEZE;
/** @hide */
@IntDef(flag = true, prefix = { "RECEIVER_VISIBLE" }, value = {
diff --git a/core/java/android/hardware/input/IKeyGestureHandler.aidl b/core/java/android/hardware/input/IKeyGestureHandler.aidl
index 509b948..4da991e 100644
--- a/core/java/android/hardware/input/IKeyGestureHandler.aidl
+++ b/core/java/android/hardware/input/IKeyGestureHandler.aidl
@@ -28,15 +28,4 @@
* to that gesture.
*/
boolean handleKeyGesture(in AidlKeyGestureEvent event, in IBinder focusedToken);
-
- /**
- * Called to know if a particular gesture type is supported by the handler.
- *
- * TODO(b/358569822): Remove this call to reduce the binder calls to single call for
- * handleKeyGesture. For this we need to remove dependency of multi-key gestures to identify if
- * a key gesture is supported on first relevant key down.
- * Also, for now we prioritize handlers in the system server process above external handlers to
- * reduce IPC binder calls.
- */
- boolean isKeyGestureSupported(int gestureType);
}
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index 49db54d..d6419af 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -1758,13 +1758,6 @@
*/
boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event,
@Nullable IBinder focusedToken);
-
- /**
- * Called to identify if a particular gesture is of interest to a handler.
- *
- * NOTE: If no active handler supports certain gestures, the gestures will not be captured.
- */
- boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType);
}
/** @hide */
diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java
index a9a45ae..c4b4831 100644
--- a/core/java/android/hardware/input/InputManagerGlobal.java
+++ b/core/java/android/hardware/input/InputManagerGlobal.java
@@ -1193,23 +1193,6 @@
}
return false;
}
-
- @Override
- public boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) {
- synchronized (mKeyGestureEventHandlerLock) {
- if (mKeyGestureEventHandlers == null) {
- return false;
- }
- final int numHandlers = mKeyGestureEventHandlers.size();
- for (int i = 0; i < numHandlers; i++) {
- KeyGestureEventHandler handler = mKeyGestureEventHandlers.get(i);
- if (handler.isKeyGestureSupported(gestureType)) {
- return true;
- }
- }
- }
- return false;
- }
}
/**
diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java
index 1a712d2..9dd1fed 100644
--- a/core/java/android/hardware/input/KeyGestureEvent.java
+++ b/core/java/android/hardware/input/KeyGestureEvent.java
@@ -108,7 +108,8 @@
public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD = 55;
public static final int KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD = 56;
public static final int KEY_GESTURE_TYPE_GLOBAL_ACTIONS = 57;
- public static final int KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD = 58;
+ @Deprecated
+ public static final int DEPRECATED_KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD = 58;
public static final int KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT = 59;
public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT = 60;
public static final int KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS = 61;
@@ -200,7 +201,6 @@
KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD,
KEY_GESTURE_TYPE_GLOBAL_ACTIONS,
- KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT,
KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT,
KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS,
@@ -777,8 +777,6 @@
return "KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD";
case KEY_GESTURE_TYPE_GLOBAL_ACTIONS:
return "KEY_GESTURE_TYPE_GLOBAL_ACTIONS";
- case KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
- return "KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD";
case KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
return "KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT";
case KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT:
diff --git a/core/java/android/service/chooser/flags.aconfig b/core/java/android/service/chooser/flags.aconfig
index ae0b56e..45a21be 100644
--- a/core/java/android/service/chooser/flags.aconfig
+++ b/core/java/android/service/chooser/flags.aconfig
@@ -44,6 +44,16 @@
}
flag {
+ name: "notify_single_item_change_on_icon_load"
+ namespace: "intentresolver"
+ description: "ChooserGridAdapter to notify specific items change when the target icon is loaded (instead of all-item change)."
+ bug: "298193161"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "fix_resolver_memory_leak"
is_exported: true
namespace: "intentresolver"
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index 44c3f9a..0152c52 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -75,9 +75,12 @@
// These should match the constants in framework/base/libs/hwui/hwui/DrawTextFunctor.h
private static final float HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX = 0f;
private static final float HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR = 0f;
- private static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_DP = 5f;
// since we're not using soft light yet, this needs to be much lower than the spec'd 0.8
private static final float HIGH_CONTRAST_TEXT_BACKGROUND_ALPHA_PERCENTAGE = 0.7f;
+ @VisibleForTesting
+ static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP = 5f;
+ @VisibleForTesting
+ static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR = 0.5f;
/** @hide */
@IntDef(prefix = { "BREAK_STRATEGY_" }, value = {
@@ -1030,7 +1033,9 @@
var padding = Math.max(HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX,
mPaint.getTextSize() * HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR);
- var cornerRadius = mPaint.density * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_DP;
+ var cornerRadius = Math.max(
+ mPaint.density * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP,
+ mPaint.getTextSize() * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR);
// We set the alpha on the color itself instead of Paint.setAlpha(), because that function
// actually mutates the color in... *ehem* very strange ways. Also the color might get reset
diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java
index 7c1e497..3a2ec91 100644
--- a/core/java/android/view/Choreographer.java
+++ b/core/java/android/view/Choreographer.java
@@ -967,8 +967,11 @@
DisplayEventReceiver.VsyncEventData vsyncEventData) {
final long startNanos;
final long frameIntervalNanos = vsyncEventData.frameInterval;
- boolean resynced = false;
+ // Original intended vsync time that is not adjusted by jitter
+ // or buffer stuffing recovery. Reported for jank tracking.
+ final long intendedFrameTimeNanos = frameTimeNanos;
long offsetFrameTimeNanos = frameTimeNanos;
+ boolean resynced = false;
// Evaluate if buffer stuffing recovery needs to start or end, and
// what actions need to be taken for recovery.
@@ -1012,7 +1015,6 @@
+ ((offsetFrameTimeNanos - mLastFrameTimeNanos) * 0.000001f) + " ms");
}
- long intendedFrameTimeNanos = offsetFrameTimeNanos;
startNanos = System.nanoTime();
// Calculating jitter involves using the original frame time without
// adjustments from buffer stuffing
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 6f346bd..394ac8f 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -1986,7 +1986,11 @@
// report its requested visibility at the end of the animation, otherwise we would
// lose the leash, and it would disappear during the animation
// TODO(b/326377046) revisit this part and see if we can make it more general
- typesToReport = mRequestedVisibleTypes | (mAnimatingTypes & ime());
+ if (Flags.reportAnimatingInsetsTypes()) {
+ typesToReport = mRequestedVisibleTypes;
+ } else {
+ typesToReport = mRequestedVisibleTypes | (mAnimatingTypes & ime());
+ }
} else {
typesToReport = mRequestedVisibleTypes;
}
diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
index eca798d..2908855 100644
--- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
+++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
@@ -379,7 +379,7 @@
@Nullable IRemoteInputConnection remoteInputConnection,
@Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
int unverifiedTargetSdkVersion, @UserIdInt int userId,
- @NonNull ImeOnBackInvokedDispatcher imeDispatcher) {
+ @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible) {
final IInputMethodManager service = getService();
if (service == null) {
return InputBindResult.NULL;
@@ -388,7 +388,7 @@
return service.startInputOrWindowGainedFocus(startInputReason, client, windowToken,
startInputFlags, softInputMode, windowFlags, editorInfo, remoteInputConnection,
remoteAccessibilityInputConnection, unverifiedTargetSdkVersion, userId,
- imeDispatcher);
+ imeDispatcher, imeRequestedVisible);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -408,7 +408,8 @@
@Nullable IRemoteInputConnection remoteInputConnection,
@Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
int unverifiedTargetSdkVersion, @UserIdInt int userId,
- @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean useAsyncShowHideMethod) {
+ @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible,
+ boolean useAsyncShowHideMethod) {
final IInputMethodManager service = getService();
if (service == null) {
return -1;
@@ -417,7 +418,8 @@
service.startInputOrWindowGainedFocusAsync(startInputReason, client, windowToken,
startInputFlags, softInputMode, windowFlags, editorInfo, remoteInputConnection,
remoteAccessibilityInputConnection, unverifiedTargetSdkVersion, userId,
- imeDispatcher, advanceAngGetStartInputSequenceNumber(), useAsyncShowHideMethod);
+ imeDispatcher, imeRequestedVisible, advanceAngGetStartInputSequenceNumber(),
+ useAsyncShowHideMethod);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 0b34600..a41ab36 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -871,6 +871,19 @@
IInputMethodManagerGlobalInvoker.reportPerceptibleAsync(windowToken, perceptible);
}
+ private static boolean hasViewImeRequestedVisible(View view) {
+ // before the refactor, the requestedVisibleTypes for the IME were not in sync with
+ // the state that was actually requested.
+ if (Flags.refactorInsetsController() && view != null) {
+ final var controller = view.getWindowInsetsController();
+ if (controller != null) {
+ return (view.getWindowInsetsController()
+ .getRequestedVisibleTypes() & WindowInsets.Type.ime()) != 0;
+ }
+ }
+ return false;
+ }
+
private final class DelegateImpl implements
ImeFocusController.InputMethodManagerDelegate {
@@ -941,6 +954,9 @@
Log.v(TAG, "Reporting focus gain, without startInput");
}
+ final boolean imeRequestedVisible = hasViewImeRequestedVisible(
+ mCurRootView.getView());
+
// ignore the result
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMM.startInputOrWindowGainedFocus");
IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocus(
@@ -950,7 +966,7 @@
null,
null, null,
mCurRootView.mContext.getApplicationInfo().targetSdkVersion,
- UserHandle.myUserId(), mImeDispatcher);
+ UserHandle.myUserId(), mImeDispatcher, imeRequestedVisible);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}
}
@@ -2441,9 +2457,8 @@
ImeTracker.forLogging().onProgress(statsToken,
ImeTracker.PHASE_CLIENT_NO_ONGOING_USER_ANIMATION);
if (resultReceiver != null) {
- final boolean imeReqVisible =
- (viewRootImpl.getInsetsController().getRequestedVisibleTypes()
- & WindowInsets.Type.ime()) != 0;
+ final boolean imeReqVisible = hasViewImeRequestedVisible(
+ viewRootImpl.getView());
resultReceiver.send(
imeReqVisible ? InputMethodManager.RESULT_UNCHANGED_SHOWN
: InputMethodManager.RESULT_SHOWN, null);
@@ -2656,9 +2671,8 @@
ImeTracker.forLogging().onProgress(statsToken,
ImeTracker.PHASE_CLIENT_VIEW_HANDLER_AVAILABLE);
- final boolean imeReqVisible =
- (viewRootImpl.getInsetsController().getRequestedVisibleTypes()
- & WindowInsets.Type.ime()) != 0;
+ final boolean imeReqVisible = hasViewImeRequestedVisible(
+ viewRootImpl.getView());
if (resultReceiver != null) {
resultReceiver.send(
!imeReqVisible ? InputMethodManager.RESULT_UNCHANGED_HIDDEN
@@ -3412,6 +3426,7 @@
final Handler icHandler;
InputBindResult res = null;
final boolean hasServedView;
+ final boolean imeRequestedVisible;
synchronized (mH) {
// Now that we are locked again, validate that our state hasn't
// changed.
@@ -3479,10 +3494,13 @@
}
mServedInputConnection = servedInputConnection;
+ imeRequestedVisible = hasViewImeRequestedVisible(servedView);
+
if (DEBUG) {
Log.v(TAG, "START INPUT: view=" + InputMethodDebug.dumpViewInfo(view)
+ " ic=" + ic + " editorInfo=" + editorInfo + " startInputFlags="
- + InputMethodDebug.startInputFlagsToString(startInputFlags));
+ + InputMethodDebug.startInputFlagsToString(startInputFlags)
+ + " imeRequestedVisible=" + imeRequestedVisible);
}
// When we switch between non-editable views, do not call into the IMMS.
@@ -3513,7 +3531,7 @@
servedInputConnection == null ? null
: servedInputConnection.asIRemoteAccessibilityInputConnection(),
view.getContext().getApplicationInfo().targetSdkVersion, targetUserId,
- mImeDispatcher, mAsyncShowHideMethodEnabled);
+ mImeDispatcher, imeRequestedVisible, mAsyncShowHideMethodEnabled);
} else {
res = IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocus(
startInputReason, mClient, windowGainingFocus, startInputFlags,
@@ -3521,7 +3539,7 @@
servedInputConnection == null ? null
: servedInputConnection.asIRemoteAccessibilityInputConnection(),
view.getContext().getApplicationInfo().targetSdkVersion, targetUserId,
- mImeDispatcher);
+ mImeDispatcher, imeRequestedVisible);
}
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
if (Flags.useZeroJankProxy()) {
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index ea345a5..485e7b3 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -684,7 +684,10 @@
* organizer.
* @param root1 the first root.
* @param root2 the second root.
+ * @deprecated replace with {@link #setAdjacentRootSet}
*/
+ @SuppressWarnings("UnflaggedApi") // @TestApi without associated feature.
+ @Deprecated
@NonNull
public WindowContainerTransaction setAdjacentRoots(
@NonNull WindowContainerToken root1, @NonNull WindowContainerToken root2) {
@@ -704,7 +707,7 @@
*
* @param roots the Tasks that should be adjacent to each other.
* @throws IllegalArgumentException if roots have size < 2.
- * @hide // TODO(b/373709676) Rename to setAdjacentRoots and update CTS.
+ * @hide // TODO(b/373709676) Rename to setAdjacentRoots and update CTS in 25Q4.
*/
@NonNull
public WindowContainerTransaction setAdjacentRootSet(@NonNull WindowContainerToken... roots) {
@@ -1003,7 +1006,7 @@
/**
* Sets to TaskFragments adjacent to each other. Containers below two visible adjacent
* TaskFragments will be made invisible. This is similar to
- * {@link #setAdjacentRoots(WindowContainerToken, WindowContainerToken)}, but can be used with
+ * {@link #setAdjacentRootSet(WindowContainerToken...)}, but can be used with
* fragmentTokens when that TaskFragments haven't been created (but will be created in the same
* {@link WindowContainerTransaction}).
* @param fragmentToken1 client assigned unique token to create TaskFragment with specified
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index cbeaeda..94e87c7 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -105,17 +105,6 @@
flag {
namespace: "windowing_sdk"
- name: "allow_multiple_adjacent_task_fragments"
- description: "Refactor to allow more than 2 adjacent TaskFragments"
- bug: "373709676"
- is_fixed_read_only: true
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
- namespace: "windowing_sdk"
name: "track_system_ui_context_before_wms"
description: "Keep track of SystemUiContext before WMS is initialized"
bug: "384428048"
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index c009fc3..9bc6671 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -23,6 +23,7 @@
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE;
import static android.content.ContentProvider.getUriWithoutUserId;
import static android.content.ContentProvider.getUserIdFromUri;
+import static android.service.chooser.Flags.notifySingleItemChangeOnIconLoad;
import static android.stats.devicepolicy.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;
import static android.stats.devicepolicy.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK;
@@ -163,9 +164,11 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -3212,6 +3215,8 @@
private static final int NUM_EXPANSIONS_TO_HIDE_AZ_LABEL = 20;
+ private final Set<ViewHolderBase> mBoundViewHolders = new HashSet<>();
+
ChooserGridAdapter(ChooserListAdapter wrappedAdapter) {
super();
mChooserListAdapter = wrappedAdapter;
@@ -3232,6 +3237,31 @@
notifyDataSetChanged();
}
});
+ if (notifySingleItemChangeOnIconLoad()) {
+ wrappedAdapter.setOnIconLoadedListener(this::onTargetIconLoaded);
+ }
+ }
+
+ private void onTargetIconLoaded(DisplayResolveInfo info) {
+ for (ViewHolderBase holder : mBoundViewHolders) {
+ switch (holder.getViewType()) {
+ case VIEW_TYPE_NORMAL:
+ TargetInfo itemInfo =
+ mChooserListAdapter.getItem(
+ ((ItemViewHolder) holder).mListPosition);
+ if (info == itemInfo) {
+ notifyItemChanged(holder.getAdapterPosition());
+ }
+ break;
+ case VIEW_TYPE_CALLER_AND_RANK:
+ ItemGroupViewHolder groupHolder = (ItemGroupViewHolder) holder;
+ if (suggestedAppsGroupContainsTarget(groupHolder, info)) {
+ notifyItemChanged(holder.getAdapterPosition());
+ }
+ break;
+ }
+
+ }
}
public void setFooterHeight(int height) {
@@ -3382,6 +3412,9 @@
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ if (notifySingleItemChangeOnIconLoad()) {
+ mBoundViewHolders.add((ViewHolderBase) holder);
+ }
int viewType = ((ViewHolderBase) holder).getViewType();
switch (viewType) {
case VIEW_TYPE_DIRECT_SHARE:
@@ -3396,6 +3429,22 @@
}
@Override
+ public void onViewRecycled(RecyclerView.ViewHolder holder) {
+ if (notifySingleItemChangeOnIconLoad()) {
+ mBoundViewHolders.remove((ViewHolderBase) holder);
+ }
+ super.onViewRecycled(holder);
+ }
+
+ @Override
+ public boolean onFailedToRecycleView(RecyclerView.ViewHolder holder) {
+ if (notifySingleItemChangeOnIconLoad()) {
+ mBoundViewHolders.remove((ViewHolderBase) holder);
+ }
+ return super.onFailedToRecycleView(holder);
+ }
+
+ @Override
public int getItemViewType(int position) {
int count;
@@ -3604,6 +3653,33 @@
}
}
+ /**
+ * Checks whether the suggested apps group, {@code holder}, contains the target,
+ * {@code info}.
+ */
+ private boolean suggestedAppsGroupContainsTarget(
+ ItemGroupViewHolder holder, DisplayResolveInfo info) {
+
+ int position = holder.getAdapterPosition();
+ int start = getListPosition(position);
+ int startType = getRowType(start);
+
+ int columnCount = holder.getColumnCount();
+ int end = start + columnCount - 1;
+ while (getRowType(end) != startType && end >= start) {
+ end--;
+ }
+
+ for (int i = 0; i < columnCount; i++) {
+ if (start + i <= end) {
+ if (mChooserListAdapter.getItem(holder.getItemIndex(i)) == info) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
int getListPosition(int position) {
position -= getSystemRowCount() + getProfileRowCount();
diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java
index d38689c..1b8c36d 100644
--- a/core/java/com/android/internal/app/ChooserListAdapter.java
+++ b/core/java/com/android/internal/app/ChooserListAdapter.java
@@ -16,9 +16,12 @@
package com.android.internal.app;
+import static android.service.chooser.Flags.notifySingleItemChangeOnIconLoad;
+
import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER;
+import android.annotation.Nullable;
import android.app.prediction.AppPredictor;
import android.content.ComponentName;
import android.content.Context;
@@ -56,6 +59,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Consumer;
public class ChooserListAdapter extends ResolverListAdapter {
private static final String TAG = "ChooserListAdapter";
@@ -108,6 +112,9 @@
// Represents the UserSpace in which the Initial Intents should be resolved.
private final UserHandle mInitialIntentsUserSpace;
+ @Nullable
+ private Consumer<DisplayResolveInfo> mOnIconLoadedListener;
+
// For pinned direct share labels, if the text spans multiple lines, the TextView will consume
// the full width, even if the characters actually take up less than that. Measure the actual
// line widths and constrain the View's width based upon that so that the pin doesn't end up
@@ -218,6 +225,10 @@
true);
}
+ public void setOnIconLoadedListener(Consumer<DisplayResolveInfo> onIconLoadedListener) {
+ mOnIconLoadedListener = onIconLoadedListener;
+ }
+
AppPredictor getAppPredictor() {
return mAppPredictor;
}
@@ -329,6 +340,15 @@
}
}
+ @Override
+ protected void onIconLoaded(DisplayResolveInfo info) {
+ if (notifySingleItemChangeOnIconLoad() && mOnIconLoadedListener != null) {
+ mOnIconLoadedListener.accept(info);
+ } else {
+ notifyDataSetChanged();
+ }
+ }
+
private void loadDirectShareIcon(SelectableTargetInfo info) {
LoadDirectShareIconTask task = (LoadDirectShareIconTask) mIconLoaders.get(info);
if (task == null) {
diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java
index 54c0e61..4d9ce86 100644
--- a/core/java/com/android/internal/app/ResolverListAdapter.java
+++ b/core/java/com/android/internal/app/ResolverListAdapter.java
@@ -680,6 +680,10 @@
}
}
+ protected void onIconLoaded(DisplayResolveInfo info) {
+ notifyDataSetChanged();
+ }
+
private void loadLabel(DisplayResolveInfo info) {
LoadLabelTask task = mLabelLoaders.get(info);
if (task == null) {
@@ -1004,7 +1008,7 @@
mResolverListCommunicator.updateProfileViewButton();
} else if (!mDisplayResolveInfo.hasDisplayIcon()) {
mDisplayResolveInfo.setDisplayIcon(d);
- notifyDataSetChanged();
+ onIconLoaded(mDisplayResolveInfo);
}
}
}
diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl
index 9380d99..0791612 100644
--- a/core/java/com/android/internal/view/IInputMethodManager.aidl
+++ b/core/java/com/android/internal/view/IInputMethodManager.aidl
@@ -105,7 +105,7 @@
in @nullable EditorInfo editorInfo, in @nullable IRemoteInputConnection inputConnection,
in @nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
int unverifiedTargetSdkVersion, int userId,
- in ImeOnBackInvokedDispatcher imeDispatcher);
+ in ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible);
// If windowToken is null, this just does startInput(). Otherwise this reports that a window
// has gained focus, and if 'editorInfo' is non-null then also does startInput.
@@ -120,8 +120,8 @@
in @nullable EditorInfo editorInfo, in @nullable IRemoteInputConnection inputConnection,
in @nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
int unverifiedTargetSdkVersion, int userId,
- in ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq,
- boolean useAsyncShowHideMethod);
+ in ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible,
+ int startInputSeq, boolean useAsyncShowHideMethod);
void showInputMethodPickerFromClient(in IInputMethodClient client,
int auxiliarySubtypeMode);
@@ -224,7 +224,7 @@
* async **/
oneway void acceptStylusHandwritingDelegationAsync(in IInputMethodClient client, in int userId,
in String delegatePackageName, in String delegatorPackageName, int flags,
- in IBooleanListener callback);
+ in IBooleanListener callback);
/** Returns {@code true} if currently selected IME supports Stylus handwriting. */
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 8c5b20d..7a38dce 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4184,6 +4184,11 @@
<!-- Whether device supports double tap to wake -->
<bool name="config_supportDoubleTapWake">false</bool>
+ <!-- Whether device supports double tap to sleep. This will allow the user to enable/disable
+ double tap gestures in non-action areas in the lock screen and launcher workspace to go to
+ sleep. -->
+ <bool name="config_supportDoubleTapSleep">false</bool>
+
<!-- The RadioAccessFamilies supported by the device.
Empty is viewed as "all". Only used on devices which
don't support RIL_REQUEST_GET_RADIO_CAPABILITY
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 3de30f7..46d18e3 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3142,6 +3142,7 @@
<java-symbol type="color" name="chooser_row_divider" />
<java-symbol type="layout" name="chooser_row_direct_share" />
<java-symbol type="bool" name="config_supportDoubleTapWake" />
+ <java-symbol type="bool" name="config_supportDoubleTapSleep" />
<java-symbol type="drawable" name="ic_perm_device_info" />
<java-symbol type="string" name="config_radio_access_family" />
<java-symbol type="string" name="notification_inbox_ellipsis" />
diff --git a/core/tests/coretests/src/android/text/LayoutTest.java b/core/tests/coretests/src/android/text/LayoutTest.java
index 9e78af5..11ec9f8 100644
--- a/core/tests/coretests/src/android/text/LayoutTest.java
+++ b/core/tests/coretests/src/android/text/LayoutTest.java
@@ -16,6 +16,9 @@
package android.text;
+import static android.text.Layout.HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR;
+import static android.text.Layout.HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP;
+
import static com.android.graphics.hwui.flags.Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT;
import static org.junit.Assert.assertArrayEquals;
@@ -1073,6 +1076,68 @@
}
}
+ @Test
+ @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
+ public void highContrastTextEnabled_testRoundedRectSize_belowMinimum_usesMinimumValue() {
+ mTextPaint.setColor(Color.BLACK);
+ mTextPaint.setTextSize(8); // Value chosen so that N * RADIUS_FACTOR < RADIUS_MIN_DP
+ Layout layout = new StaticLayout("Test text", mTextPaint, mWidth,
+ mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false);
+
+ MockCanvas c = new MockCanvas(/* width= */ 256, /* height= */ 256);
+ c.setHighContrastTextEnabled(true);
+ layout.draw(
+ c,
+ /* highlightPaths= */ null,
+ /* highlightPaints= */ null,
+ /* selectionPath= */ null,
+ /* selectionPaint= */ null,
+ /* cursorOffsetVertical= */ 0
+ );
+
+ final float expectedRoundedRectSize =
+ mTextPaint.density * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP;
+ List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands();
+ for (int i = 0; i < drawCommands.size(); i++) {
+ MockCanvas.DrawCommand drawCommand = drawCommands.get(i);
+ if (drawCommand.rect != null) {
+ expect.that(drawCommand.rX).isEqualTo(expectedRoundedRectSize);
+ expect.that(drawCommand.rY).isEqualTo(expectedRoundedRectSize);
+ }
+ }
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
+ public void highContrastTextEnabled_testRoundedRectSize_aboveMinimum_usesScaledValue() {
+ mTextPaint.setColor(Color.BLACK);
+ mTextPaint.setTextSize(50); // Value chosen so that N * RADIUS_FACTOR > RADIUS_MIN_DP
+ Layout layout = new StaticLayout("Test text", mTextPaint, mWidth,
+ mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false);
+
+ MockCanvas c = new MockCanvas(/* width= */ 256, /* height= */ 256);
+ c.setHighContrastTextEnabled(true);
+ layout.draw(
+ c,
+ /* highlightPaths= */ null,
+ /* highlightPaints= */ null,
+ /* selectionPath= */ null,
+ /* selectionPaint= */ null,
+ /* cursorOffsetVertical= */ 0
+ );
+
+ final float expectedRoundedRectSize =
+ mTextPaint.getTextSize() * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR;
+ List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands();
+ for (int i = 0; i < drawCommands.size(); i++) {
+ MockCanvas.DrawCommand drawCommand = drawCommands.get(i);
+ if (drawCommand.rect != null) {
+ expect.that(drawCommand.rX).isEqualTo(expectedRoundedRectSize);
+ expect.that(drawCommand.rY).isEqualTo(expectedRoundedRectSize);
+ }
+ }
+ }
+
private int removeAlpha(int color) {
return Color.rgb(
Color.red(color),
@@ -1087,6 +1152,8 @@
public final String text;
public final float x;
public final float y;
+ public final float rX;
+ public final float rY;
public final Path path;
public final RectF rect;
public final Paint paint;
@@ -1098,6 +1165,8 @@
this.paint = new Paint(paint);
path = null;
rect = null;
+ this.rX = 0;
+ this.rY = 0;
}
DrawCommand(Path path, Paint paint) {
@@ -1107,15 +1176,19 @@
x = 0;
text = null;
rect = null;
+ this.rX = 0;
+ this.rY = 0;
}
- DrawCommand(RectF rect, Paint paint) {
+ DrawCommand(RectF rect, Paint paint, float rX, float rY) {
this.rect = new RectF(rect);
this.paint = new Paint(paint);
path = null;
y = 0;
x = 0;
text = null;
+ this.rX = rX;
+ this.rY = rY;
}
@Override
@@ -1189,12 +1262,12 @@
@Override
public void drawRect(RectF rect, Paint p) {
- mDrawCommands.add(new DrawCommand(rect, p));
+ mDrawCommands.add(new DrawCommand(rect, p, 0, 0));
}
@Override
public void drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint) {
- mDrawCommands.add(new DrawCommand(rect, paint));
+ mDrawCommands.add(new DrawCommand(rect, paint, rx, ry));
}
List<DrawCommand> getDrawCommands() {
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java
index db69cf2..02a2968 100644
--- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java
@@ -72,7 +72,8 @@
private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry
.getInstrumentation().getTargetContext().getUser();
- private static final UserHandle WORK_USER_HANDLE = UserHandle.of(10);
+ private static final UserHandle WORK_USER_HANDLE =
+ UserHandle.of(PERSONAL_USER_HANDLE.getIdentifier() + 1);
@Rule
public ActivityTestRule<ChooserWrapperActivity> mActivityRule =
diff --git a/libs/WindowManager/Shell/multivalentTests/Android.bp b/libs/WindowManager/Shell/multivalentTests/Android.bp
index 03076c0..5066691 100644
--- a/libs/WindowManager/Shell/multivalentTests/Android.bp
+++ b/libs/WindowManager/Shell/multivalentTests/Android.bp
@@ -51,6 +51,7 @@
"androidx.test.ext.junit",
"mockito-robolectric-prebuilt",
"mockito-kotlin2",
+ "platform-parametric-runner-lib",
"truth",
"flag-junit-base",
"flag-junit",
@@ -74,6 +75,7 @@
"frameworks-base-testutils",
"mockito-kotlin2",
"mockito-target-extended-minus-junit4",
+ "platform-parametric-runner-lib",
"truth",
"platform-test-annotations",
"platform-test-rules",
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleExpandedViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleExpandedViewTest.kt
new file mode 100644
index 0000000..bdfaef2
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleExpandedViewTest.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.bubbles
+
+import android.content.ComponentName
+import android.content.Context
+import android.platform.test.flag.junit.FlagsParameterization
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.Flags
+import com.android.wm.shell.taskview.TaskView
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+/** Tests for [BubbleExpandedView] */
+@SmallTest
+@RunWith(ParameterizedAndroidJunit4::class)
+class BubbleExpandedViewTest(flags: FlagsParameterization) {
+
+ @get:Rule
+ val setFlagsRule = SetFlagsRule(flags)
+
+ private val context = ApplicationProvider.getApplicationContext<Context>()
+ private val componentName = ComponentName(context, "TestClass")
+
+ @Test
+ fun getTaskId_onTaskCreated_returnsCorrectTaskId() {
+ val bubbleTaskView = BubbleTaskView(mock<TaskView>(), directExecutor())
+ val expandedView = BubbleExpandedView(context).apply {
+ initialize(
+ mock<BubbleExpandedViewManager>(),
+ mock<BubbleStackView>(),
+ mock<BubblePositioner>(),
+ false /* isOverflow */,
+ bubbleTaskView,
+ )
+ setAnimating(true) // Skips setContentVisibility for testing.
+ }
+
+ bubbleTaskView.listener.onTaskCreated(123, componentName)
+
+ assertThat(expandedView.getTaskId()).isEqualTo(123)
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams() = FlagsParameterization.allCombinationsOf(
+ Flags.FLAG_ENABLE_BUBBLE_TASK_VIEW_LISTENER,
+ )
+ }
+}
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
index 9087da3..636ff66 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
@@ -266,8 +266,6 @@
optionsCaptor.capture(),
any())
- assertThat((intentCaptor.lastValue.flags
- and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue()
assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only
assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only
assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
@@ -295,8 +293,6 @@
optionsCaptor.capture(),
any())
- assertThat((intentCaptor.lastValue.flags
- and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue()
assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only
assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only
assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
@@ -324,8 +320,6 @@
optionsCaptor.capture(),
any())
- assertThat((intentCaptor.lastValue.flags
- and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue()
assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only
assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only
assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
index c2aa146..3ebe87d 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
@@ -38,11 +38,11 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
- android:layout_marginStart="4dp"
- android:layout_marginEnd="4dp">
+ android:gravity="center_horizontal"
+ android:layout_marginHorizontal="4dp">
<Button
- android:layout_width="94dp"
+ android:layout_width="108dp"
android:layout_height="60dp"
android:id="@+id/maximize_menu_immersive_toggle_button"
style="?android:attr/buttonBarButtonStyle"
@@ -75,8 +75,7 @@
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center_horizontal"
- android:layout_marginStart="4dp"
- android:layout_marginEnd="4dp">
+ android:layout_marginHorizontal="4dp">
<Button
android:layout_width="108dp"
@@ -112,8 +111,7 @@
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center_horizontal"
- android:layout_marginStart="4dp"
- android:layout_marginEnd="4dp">
+ android:layout_marginHorizontal="4dp">
<LinearLayout
android:id="@+id/maximize_menu_snap_menu_layout"
android:layout_width="wrap_content"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index 2c2451c..8ac9230 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -238,7 +238,6 @@
mContext.createContextAsUser(
mBubble.getUser(), Context.CONTEXT_RESTRICTED);
Intent fillInIntent = new Intent();
- fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
PendingIntent pi = PendingIntent.getActivity(
context,
/* requestCode= */ 0,
@@ -467,6 +466,11 @@
new BubbleTaskViewListener.Callback() {
@Override
public void onTaskCreated() {
+ // The taskId is saved to use for removeTask,
+ // preventing appearance in recent tasks.
+ mTaskId = ((BubbleTaskViewListener) mCurrentTaskViewListener)
+ .getTaskId();
+
setContentVisibility(true);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java
index 63d7134..9c20e3a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java
@@ -130,7 +130,6 @@
mContext.createContextAsUser(
mBubble.getUser(), Context.CONTEXT_RESTRICTED);
Intent fillInIntent = new Intent();
- fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
// First try get pending intent from the bubble
PendingIntent pi = mBubble.getPendingIntent();
if (pi == null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java
index d5f2dbd..51a5b12 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java
@@ -639,7 +639,7 @@
@Override
public void continueCollapse() {
mBubble.cleanupTaskView();
- if (mTaskLeash == null) return;
+ if (mTaskLeash == null || !mTaskLeash.isValid()) return;
SurfaceControl.Transaction t = new SurfaceControl.Transaction();
t.reparent(mTaskLeash, mRootLeash);
t.apply();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index 6c840f0..bdb21f2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -447,9 +447,9 @@
bubble.cleanupViews(!inTransition);
endAction.run();
};
- if (mBubbleData.getBubbles().isEmpty()) {
- // we're removing the last bubble. collapse the expanded view and cleanup bubble views
- // at the end.
+ if (mBubbleData.getBubbles().isEmpty() || inTransition) {
+ // If we are removing the last bubble or removing the current bubble via transition,
+ // collapse the expanded view and clean up bubbles at the end.
collapse(cleanUp);
} else {
cleanUp.run();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index dd2050a..f737884 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -440,11 +440,18 @@
statsToken);
}
- // In case of a hide, the statsToken should not been send yet (as the animation
- // is still ongoing). It will be sent at the end of the animation
- boolean hideAnimOngoing = !mImeRequestedVisible && mAnimation != null;
- setVisibleDirectly(mImeRequestedVisible || mAnimation != null,
- hideAnimOngoing ? null : statsToken);
+ boolean hideAnimOngoing;
+ boolean reportVisible;
+ if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) {
+ hideAnimOngoing = false;
+ reportVisible = mImeRequestedVisible;
+ } else {
+ // In case of a hide, the statsToken should not been send yet (as the animation
+ // is still ongoing). It will be sent at the end of the animation.
+ hideAnimOngoing = !mImeRequestedVisible && mAnimation != null;
+ reportVisible = mImeRequestedVisible || mAnimation != null;
+ }
+ setVisibleDirectly(reportVisible, hideAnimOngoing ? null : statsToken);
}
}
@@ -628,7 +635,7 @@
+ " showing:" + (mAnimationDirection == DIRECTION_SHOW));
}
if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) {
- setAnimating(true);
+ setAnimating(true /* imeAnimationOngoing */);
}
int flags = dispatchStartPositioning(mDisplayId, imeTop(hiddenY, defaultY),
imeTop(shownY, defaultY), mAnimationDirection == DIRECTION_SHOW,
@@ -678,7 +685,7 @@
if (!android.view.inputmethod.Flags.refactorInsetsController()) {
dispatchEndPositioning(mDisplayId, mCancelled, t);
} else if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) {
- setAnimating(false);
+ setAnimating(false /* imeAnimationOngoing */);
}
if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) {
ImeTracker.forLogging().onProgress(mStatsToken,
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 92c3020..bc2ed3f 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
@@ -1472,7 +1472,9 @@
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
IWindowManager windowManager,
ShellTaskOrganizer shellTaskOrganizer,
- DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider
+ DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider,
+ InputManager inputManager,
+ @ShellMainThread Handler mainHandler
) {
if (!DesktopModeStatus.canEnterDesktopMode(context)) {
return Optional.empty();
@@ -1484,7 +1486,9 @@
rootTaskDisplayAreaOrganizer,
windowManager,
shellTaskOrganizer,
- desktopWallpaperActivityTokenProvider));
+ desktopWallpaperActivityTokenProvider,
+ inputManager,
+ mainHandler));
}
//
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
index e89aafe..904d862 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
@@ -22,6 +22,8 @@
import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.app.WindowConfiguration.windowingModeToString
import android.content.Context
+import android.hardware.input.InputManager
+import android.os.Handler
import android.provider.Settings
import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
import android.view.Display.DEFAULT_DISPLAY
@@ -29,11 +31,13 @@
import android.view.WindowManager.TRANSIT_CHANGE
import android.window.DesktopExperienceFlags
import android.window.WindowContainerTransaction
+import com.android.internal.annotations.VisibleForTesting
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
+import com.android.wm.shell.shared.annotations.ShellMainThread
import com.android.wm.shell.transition.Transitions
/** Controls the display windowing mode in desktop mode */
@@ -44,8 +48,26 @@
private val windowManager: IWindowManager,
private val shellTaskOrganizer: ShellTaskOrganizer,
private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider,
+ private val inputManager: InputManager,
+ @ShellMainThread private val mainHandler: Handler,
) {
+ private val onTabletModeChangedListener =
+ object : InputManager.OnTabletModeChangedListener {
+ override fun onTabletModeChanged(whenNanos: Long, inTabletMode: Boolean) {
+ refreshDisplayWindowingMode()
+ }
+ }
+
+ init {
+ if (DesktopExperienceFlags.FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH.isTrue) {
+ inputManager.registerOnTabletModeChangedListener(
+ onTabletModeChangedListener,
+ mainHandler,
+ )
+ }
+ }
+
fun refreshDisplayWindowingMode() {
if (!DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue) return
@@ -89,10 +111,20 @@
transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
}
- private fun getTargetWindowingModeForDefaultDisplay(): Int {
+ @VisibleForTesting
+ fun getTargetWindowingModeForDefaultDisplay(): Int {
if (isExtendedDisplayEnabled() && hasExternalDisplay()) {
return WINDOWING_MODE_FREEFORM
}
+ if (DesktopExperienceFlags.FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH.isTrue) {
+ if (isInClamshellMode()) {
+ return WINDOWING_MODE_FREEFORM
+ }
+ return WINDOWING_MODE_FULLSCREEN
+ }
+
+ // If form factor-based desktop first switch is disabled, use the default display windowing
+ // mode here to keep the freeform mode for some form factors (e.g., FEATURE_PC).
return windowManager.getWindowingMode(DEFAULT_DISPLAY)
}
@@ -108,6 +140,8 @@
private fun hasExternalDisplay() =
rootTaskDisplayAreaOrganizer.getDisplayIds().any { it != DEFAULT_DISPLAY }
+ private fun isInClamshellMode() = inputManager.isInTabletMode() == InputManager.SWITCH_STATE_OFF
+
private fun logV(msg: String, vararg arguments: Any?) {
ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
index 5269318..1ea545f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
@@ -56,7 +56,6 @@
override fun handleKeyGestureEvent(event: KeyGestureEvent, focusedToken: IBinder?): Boolean {
if (
- !isKeyGestureSupported(event.keyGestureType) ||
!desktopTasksController.isPresent ||
!desktopModeWindowDecorViewModel.isPresent
) {
@@ -136,19 +135,6 @@
}
}
- override fun isKeyGestureSupported(gestureType: Int): Boolean =
- when (gestureType) {
- KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY ->
- enableMoveToNextDisplayShortcut()
- KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW,
- KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW,
- KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW,
- KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW ->
- DesktopModeFlags.ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS.isTrue &&
- manageKeyGestures()
- else -> false
- }
-
// TODO: b/364154795 - wait for the completion of moveToNextDisplay transition, otherwise it
// will pick a wrong task when a user quickly perform other actions with keyboard shortcuts
// after moveToNextDisplay, and move this to FocusTransitionObserver class.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index c370c0c..75c09829 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -93,7 +93,6 @@
import android.app.ActivityOptions;
import android.app.IActivityTaskManager;
import android.app.PendingIntent;
-import android.app.PictureInPictureParams;
import android.app.TaskInfo;
import android.content.ActivityNotFoundException;
import android.content.Context;
@@ -2249,10 +2248,10 @@
setRootForceTranslucent(true, wct);
if (!enableFlexibleSplit()) {
- //TODO(b/373709676) Need to figure out how adjacentRoots work for flex split
+ // TODO: consider support 3 splits
// Make the stages adjacent to each other so they occlude what's behind them.
- wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
+ wct.setAdjacentRootSet(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
mSplitLayout.getInvisibleBounds(mTempRect1);
wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
}
@@ -2263,7 +2262,7 @@
});
mLaunchAdjacentController.setLaunchAdjacentRoot(mSideStage.mRootTaskInfo.token);
} else {
- // TODO(b/373709676) Need to figure out how adjacentRoots work for flex split
+ // TODO: consider support 3 splits
}
}
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 673c3f6..3f0d931 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
@@ -1096,8 +1096,8 @@
return Resources.ID_NULL;
}
- private PointF calculateMaximizeMenuPosition(int menuWidth, int menuHeight) {
- final PointF position = new PointF();
+ private Point calculateMaximizeMenuPosition(int menuWidth, int menuHeight) {
+ final Point position = new Point();
final Resources resources = mContext.getResources();
final DisplayLayout displayLayout =
mDisplayController.getDisplayLayout(mTaskInfo.displayId);
@@ -1112,11 +1112,11 @@
final int[] maximizeButtonLocation = new int[2];
maximizeWindowButton.getLocationInWindow(maximizeButtonLocation);
- float menuLeft = (mPositionInParent.x + maximizeButtonLocation[0] - ((float) (menuWidth
- - maximizeWindowButton.getWidth()) / 2));
- float menuTop = (mPositionInParent.y + captionHeight);
- final float menuRight = menuLeft + menuWidth;
- final float menuBottom = menuTop + menuHeight;
+ int menuLeft = (mPositionInParent.x + maximizeButtonLocation[0] - (menuWidth
+ - maximizeWindowButton.getWidth()) / 2);
+ int menuTop = (mPositionInParent.y + captionHeight);
+ final int menuRight = menuLeft + menuWidth;
+ final int menuBottom = menuTop + menuHeight;
// If the menu is out of screen bounds, shift it as needed
if (menuLeft < 0) {
@@ -1128,7 +1128,7 @@
menuTop = (displayHeight - menuHeight);
}
- return new PointF(menuLeft, menuTop);
+ return new Point(menuLeft, menuTop);
}
boolean isHandleMenuActive() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
index ad3525a..5d1a7a0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
@@ -26,7 +26,7 @@
import android.content.res.Resources
import android.graphics.Paint
import android.graphics.PixelFormat
-import android.graphics.PointF
+import android.graphics.Point
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
@@ -90,7 +90,7 @@
private val displayController: DisplayController,
private val taskInfo: RunningTaskInfo,
private val decorWindowContext: Context,
- private val positionSupplier: (Int, Int) -> PointF,
+ private val positionSupplier: (Int, Int) -> Point,
private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() }
) {
private var maximizeMenu: AdditionalViewHostViewContainer? = null
@@ -100,14 +100,14 @@
private val cornerRadius = loadDimensionPixelSize(
R.dimen.desktop_mode_maximize_menu_corner_radius
).toFloat()
- private lateinit var menuPosition: PointF
+ private lateinit var menuPosition: Point
private val menuPadding = loadDimensionPixelSize(R.dimen.desktop_mode_menu_padding)
/** Position the menu relative to the caption's position. */
fun positionMenu(t: Transaction) {
menuPosition = positionSupplier(maximizeMenuView?.measureWidth() ?: 0,
maximizeMenuView?.measureHeight() ?: 0)
- t.setPosition(leash, menuPosition.x, menuPosition.y)
+ t.setPosition(leash, menuPosition.x.toFloat(), menuPosition.y.toFloat())
}
/** Creates and shows the maximize window. */
@@ -208,8 +208,8 @@
val menuHeight = menuView.measureHeight()
menuPosition = positionSupplier(menuWidth, menuHeight)
val lp = WindowManager.LayoutParams(
- menuWidth.toInt(),
- menuHeight.toInt(),
+ menuWidth,
+ menuHeight,
WindowManager.LayoutParams.TYPE_APPLICATION,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
@@ -222,7 +222,7 @@
// Bring menu to front when open
t.setLayer(leash, TaskConstants.TASK_CHILD_LAYER_FLOATING_MENU)
- .setPosition(leash, menuPosition.x, menuPosition.y)
+ .setPosition(leash, menuPosition.x.toFloat(), menuPosition.y.toFloat())
.setCornerRadius(leash, cornerRadius)
.show(leash)
maximizeMenu =
@@ -1047,7 +1047,7 @@
displayController: DisplayController,
taskInfo: RunningTaskInfo,
decorWindowContext: Context,
- positionSupplier: (Int, Int) -> PointF,
+ positionSupplier: (Int, Int) -> Point,
transactionSupplier: Supplier<Transaction>
): MaximizeMenu
}
@@ -1060,7 +1060,7 @@
displayController: DisplayController,
taskInfo: RunningTaskInfo,
decorWindowContext: Context,
- positionSupplier: (Int, Int) -> PointF,
+ positionSupplier: (Int, Int) -> Point,
transactionSupplier: Supplier<Transaction>
): MaximizeMenu {
return MaximizeMenu(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt
index c3d15df..9c55f0e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt
@@ -17,6 +17,7 @@
package com.android.wm.shell.windowdecor.tiling
import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_PINNED
import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
@@ -28,9 +29,11 @@
import android.view.SurfaceControl.Transaction
import android.view.WindowManager.TRANSIT_CHANGE
import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_PIP
import android.view.WindowManager.TRANSIT_TO_BACK
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
import android.window.TransitionRequestInfo
import android.window.WindowContainerTransaction
import com.android.internal.annotations.VisibleForTesting
@@ -422,6 +425,8 @@
change.taskInfo?.let {
if (it.isFullscreen || isMinimized(change.mode, info.type)) {
removeTaskIfTiled(it.taskId, /* taskVanished= */ false, it.isFullscreen)
+ } else if (isEnteringPip(change, info.type)) {
+ removeTaskIfTiled(it.taskId, /* taskVanished= */ true, it.isFullscreen)
}
}
}
@@ -434,6 +439,27 @@
infoType == TRANSIT_OPEN))
}
+ private fun isEnteringPip(change: Change, transitType: Int): Boolean {
+ if (change.taskInfo != null && change.taskInfo?.windowingMode == WINDOWING_MODE_PINNED) {
+ // - TRANSIT_PIP: type (from RootWindowContainer)
+ // - TRANSIT_OPEN (from apps that enter PiP instantly on opening, mostly from
+ // CTS/Flicker tests).
+ // - TRANSIT_TO_FRONT, though uncommon with triggering PiP, should semantically also
+ // be allowed to animate if the task in question is pinned already - see b/308054074.
+ // - TRANSIT_CHANGE: This can happen if the request to enter PIP happens when we are
+ // collecting for another transition, such as TRANSIT_CHANGE (display rotation).
+ if (
+ transitType == TRANSIT_PIP ||
+ transitType == TRANSIT_OPEN ||
+ transitType == TRANSIT_TO_FRONT ||
+ transitType == TRANSIT_CHANGE
+ ) {
+ return true
+ }
+ }
+ return false
+ }
+
class AppResizingHelper(
val taskInfo: RunningTaskInfo,
val desktopModeWindowDecoration: DesktopModeWindowDecoration,
@@ -550,12 +576,16 @@
taskVanished: Boolean = false,
shouldDelayUpdate: Boolean = false,
) {
+ val taskRepository = desktopUserRepositories.current
if (taskId == leftTaskResizingHelper?.taskInfo?.taskId) {
removeTask(leftTaskResizingHelper, taskVanished, shouldDelayUpdate)
leftTaskResizingHelper = null
- rightTaskResizingHelper
- ?.desktopModeWindowDecoration
- ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate)
+ val taskId = rightTaskResizingHelper?.taskInfo?.taskId
+ if (taskId != null && taskRepository.isVisibleTask(taskId)) {
+ rightTaskResizingHelper
+ ?.desktopModeWindowDecoration
+ ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate)
+ }
tearDownTiling()
return
}
@@ -563,9 +593,12 @@
if (taskId == rightTaskResizingHelper?.taskInfo?.taskId) {
removeTask(rightTaskResizingHelper, taskVanished, shouldDelayUpdate)
rightTaskResizingHelper = null
- leftTaskResizingHelper
- ?.desktopModeWindowDecoration
- ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate)
+ val taskId = leftTaskResizingHelper?.taskInfo?.taskId
+ if (taskId != null && taskRepository.isVisibleTask(taskId)) {
+ leftTaskResizingHelper
+ ?.desktopModeWindowDecoration
+ ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate)
+ }
tearDownTiling()
}
}
@@ -600,7 +633,6 @@
fun onOverviewAnimationStateChange(isRunning: Boolean) {
if (!isTilingManagerInitialised) return
-
if (isRunning) {
desktopTilingDividerWindowManager?.hideDividerBar()
} else if (allTiledTasksVisible()) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
index cc37c44..450989d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
@@ -21,9 +21,12 @@
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.content.ContentResolver
+import android.hardware.input.InputManager
import android.os.Binder
+import android.os.Handler
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
import android.provider.Settings
import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
import android.view.Display.DEFAULT_DISPLAY
@@ -44,6 +47,7 @@
import com.google.common.truth.Truth.assertThat
import com.google.testing.junit.testparameterinjector.TestParameter
import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -64,13 +68,18 @@
*/
@SmallTest
@RunWith(TestParameterInjector::class)
-class DesktopDisplayModeControllerTest : ShellTestCase() {
+class DesktopDisplayModeControllerTest(
+ @TestParameter(valuesProvider = FlagsParameterizationProvider::class)
+ flags: FlagsParameterization
+) : ShellTestCase() {
private val transitions = mock<Transitions>()
private val rootTaskDisplayAreaOrganizer = mock<RootTaskDisplayAreaOrganizer>()
private val mockWindowManager = mock<IWindowManager>()
private val shellTaskOrganizer = mock<ShellTaskOrganizer>()
private val desktopWallpaperActivityTokenProvider =
mock<DesktopWallpaperActivityTokenProvider>()
+ private val inputManager = mock<InputManager>()
+ private val mainHandler = mock<Handler>()
private lateinit var controller: DesktopDisplayModeController
@@ -82,6 +91,10 @@
private val defaultTDA = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
private val wallpaperToken = MockToken().token()
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
+
@Before
fun setUp() {
whenever(transitions.startTransition(anyInt(), any(), isNull())).thenReturn(Binder())
@@ -95,27 +108,20 @@
mockWindowManager,
shellTaskOrganizer,
desktopWallpaperActivityTokenProvider,
+ inputManager,
+ mainHandler,
)
runningTasks.add(freeformTask)
runningTasks.add(fullscreenTask)
whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(ArrayList(runningTasks))
whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(wallpaperToken)
+ setTabletModeStatus(SwitchState.UNKNOWN)
}
- private fun testDisplayWindowingModeSwitch(
- defaultWindowingMode: Int,
- extendedDisplayEnabled: Boolean,
- expectToSwitch: Boolean,
- ) {
- defaultTDA.configuration.windowConfiguration.windowingMode = defaultWindowingMode
- whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(defaultWindowingMode)
- val settingsSession =
- ExtendedDisplaySettingsSession(
- context.contentResolver,
- if (extendedDisplayEnabled) 1 else 0,
- )
-
- settingsSession.use {
+ private fun testDisplayWindowingModeSwitchOnDisplayConnected(expectToSwitch: Boolean) {
+ defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+ whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(WINDOWING_MODE_FULLSCREEN)
+ ExtendedDisplaySettingsSession(context.contentResolver, 1).use {
connectExternalDisplay()
if (expectToSwitch) {
// Assumes [connectExternalDisplay] properly triggered the switching transition.
@@ -133,7 +139,7 @@
assertThat(arg.firstValue.changes[wallpaperToken.asBinder()]?.windowingMode)
.isEqualTo(WINDOWING_MODE_FULLSCREEN)
assertThat(arg.secondValue.changes[defaultTDA.token.asBinder()]?.windowingMode)
- .isEqualTo(defaultWindowingMode)
+ .isEqualTo(WINDOWING_MODE_FULLSCREEN)
assertThat(arg.secondValue.changes[wallpaperToken.asBinder()]?.windowingMode)
.isEqualTo(WINDOWING_MODE_FULLSCREEN)
} else {
@@ -144,25 +150,64 @@
@Test
@DisableFlags(Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING)
- fun displayWindowingModeSwitchOnDisplayConnected_flagDisabled(
- @TestParameter param: ModeSwitchTestCase
- ) {
- testDisplayWindowingModeSwitch(
- param.defaultWindowingMode,
- param.extendedDisplayEnabled,
- // When the flag is disabled, never switch.
- expectToSwitch = false,
- )
+ fun displayWindowingModeSwitchOnDisplayConnected_flagDisabled() {
+ // When the flag is disabled, never switch.
+ testDisplayWindowingModeSwitchOnDisplayConnected(/* expectToSwitch= */ false)
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING)
- fun displayWindowingModeSwitchOnDisplayConnected(@TestParameter param: ModeSwitchTestCase) {
- testDisplayWindowingModeSwitch(
- param.defaultWindowingMode,
- param.extendedDisplayEnabled,
- param.expectToSwitchByDefault,
- )
+ fun displayWindowingModeSwitchOnDisplayConnected() {
+ testDisplayWindowingModeSwitchOnDisplayConnected(/* expectToSwitch= */ true)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING)
+ @DisableFlags(Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH)
+ fun testTargetWindowingMode_formfactorDisabled(
+ @TestParameter param: ExternalDisplayBasedTargetModeTestCase,
+ @TestParameter tabletModeStatus: SwitchState,
+ ) {
+ whenever(mockWindowManager.getWindowingMode(anyInt()))
+ .thenReturn(param.defaultWindowingMode)
+ if (param.hasExternalDisplay) {
+ connectExternalDisplay()
+ } else {
+ disconnectExternalDisplay()
+ }
+ setTabletModeStatus(tabletModeStatus)
+
+ ExtendedDisplaySettingsSession(
+ context.contentResolver,
+ if (param.extendedDisplayEnabled) 1 else 0,
+ )
+ .use {
+ assertThat(controller.getTargetWindowingModeForDefaultDisplay())
+ .isEqualTo(param.expectedWindowingMode)
+ }
+ }
+
+ @Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING,
+ Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH,
+ )
+ fun testTargetWindowingMode(@TestParameter param: FormFactorBasedTargetModeTestCase) {
+ if (param.hasExternalDisplay) {
+ connectExternalDisplay()
+ } else {
+ disconnectExternalDisplay()
+ }
+ setTabletModeStatus(param.tabletModeStatus)
+
+ ExtendedDisplaySettingsSession(
+ context.contentResolver,
+ if (param.extendedDisplayEnabled) 1 else 0,
+ )
+ .use {
+ assertThat(controller.getTargetWindowingModeForDefaultDisplay())
+ .isEqualTo(param.expectedWindowingMode)
+ }
}
@Test
@@ -217,6 +262,10 @@
controller.refreshDisplayWindowingMode()
}
+ private fun setTabletModeStatus(status: SwitchState) {
+ whenever(inputManager.isInTabletMode()).thenReturn(status.value)
+ }
+
private class ExtendedDisplaySettingsSession(
private val contentResolver: ContentResolver,
private val overrideValue: Int,
@@ -233,33 +282,158 @@
}
}
+ private class FlagsParameterizationProvider : TestParameterValuesProvider() {
+ override fun provideValues(
+ context: TestParameterValuesProvider.Context
+ ): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf(
+ Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH
+ )
+ }
+ }
+
companion object {
const val EXTERNAL_DISPLAY_ID = 100
- enum class ModeSwitchTestCase(
+ enum class SwitchState(val value: Int) {
+ UNKNOWN(InputManager.SWITCH_STATE_UNKNOWN),
+ ON(InputManager.SWITCH_STATE_ON),
+ OFF(InputManager.SWITCH_STATE_OFF),
+ }
+
+ enum class ExternalDisplayBasedTargetModeTestCase(
val defaultWindowingMode: Int,
+ val hasExternalDisplay: Boolean,
val extendedDisplayEnabled: Boolean,
- val expectToSwitchByDefault: Boolean,
+ val expectedWindowingMode: Int,
) {
- FULLSCREEN_DISPLAY(
- defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
- extendedDisplayEnabled = true,
- expectToSwitchByDefault = true,
- ),
- FULLSCREEN_DISPLAY_MIRRORING(
- defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
- extendedDisplayEnabled = false,
- expectToSwitchByDefault = false,
- ),
- FREEFORM_DISPLAY(
+ FREEFORM_EXTERNAL_EXTENDED(
defaultWindowingMode = WINDOWING_MODE_FREEFORM,
+ hasExternalDisplay = true,
extendedDisplayEnabled = true,
- expectToSwitchByDefault = false,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
),
- FREEFORM_DISPLAY_MIRRORING(
+ FULLSCREEN_EXTERNAL_EXTENDED(
+ defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = true,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ FREEFORM_NO_EXTERNAL_EXTENDED(
defaultWindowingMode = WINDOWING_MODE_FREEFORM,
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = true,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ FULLSCREEN_NO_EXTERNAL_EXTENDED(
+ defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = true,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ ),
+ FREEFORM_EXTERNAL_MIRROR(
+ defaultWindowingMode = WINDOWING_MODE_FREEFORM,
+ hasExternalDisplay = true,
extendedDisplayEnabled = false,
- expectToSwitchByDefault = false,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ FULLSCREEN_EXTERNAL_MIRROR(
+ defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = false,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ ),
+ FREEFORM_NO_EXTERNAL_MIRROR(
+ defaultWindowingMode = WINDOWING_MODE_FREEFORM,
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = false,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ FULLSCREEN_NO_EXTERNAL_MIRROR(
+ defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = false,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ ),
+ }
+
+ enum class FormFactorBasedTargetModeTestCase(
+ val hasExternalDisplay: Boolean,
+ val extendedDisplayEnabled: Boolean,
+ val tabletModeStatus: SwitchState,
+ val expectedWindowingMode: Int,
+ ) {
+ EXTERNAL_EXTENDED_TABLET(
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = true,
+ tabletModeStatus = SwitchState.ON,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ NO_EXTERNAL_EXTENDED_TABLET(
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = true,
+ tabletModeStatus = SwitchState.ON,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ ),
+ EXTERNAL_MIRROR_TABLET(
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = false,
+ tabletModeStatus = SwitchState.ON,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ ),
+ NO_EXTERNAL_MIRROR_TABLET(
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = false,
+ tabletModeStatus = SwitchState.ON,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ ),
+ EXTERNAL_EXTENDED_CLAMSHELL(
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = true,
+ tabletModeStatus = SwitchState.OFF,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ NO_EXTERNAL_EXTENDED_CLAMSHELL(
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = true,
+ tabletModeStatus = SwitchState.OFF,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ EXTERNAL_MIRROR_CLAMSHELL(
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = false,
+ tabletModeStatus = SwitchState.OFF,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ NO_EXTERNAL_MIRROR_CLAMSHELL(
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = false,
+ tabletModeStatus = SwitchState.OFF,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ EXTERNAL_EXTENDED_UNKNOWN(
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = true,
+ tabletModeStatus = SwitchState.UNKNOWN,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ NO_EXTERNAL_EXTENDED_UNKNOWN(
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = true,
+ tabletModeStatus = SwitchState.UNKNOWN,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ ),
+ EXTERNAL_MIRROR_UNKNOWN(
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = false,
+ tabletModeStatus = SwitchState.UNKNOWN,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ ),
+ NO_EXTERNAL_MIRROR_UNKNOWN(
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = false,
+ tabletModeStatus = SwitchState.UNKNOWN,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
index c40a04c..b511fc3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
@@ -22,6 +22,7 @@
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
+import android.app.WindowConfiguration.WINDOWING_MODE_PINNED
import android.content.ComponentName
import android.graphics.Rect
import android.view.Display.DEFAULT_DISPLAY
@@ -45,6 +46,17 @@
.apply { bounds?.let { setBounds(it) } }
.build()
+ fun createPinnedTask(displayId: Int = DEFAULT_DISPLAY, bounds: Rect? = null): RunningTaskInfo =
+ TestRunningTaskInfoBuilder()
+ .setDisplayId(displayId)
+ .setParentTaskId(displayId)
+ .setToken(MockToken().token())
+ .setActivityType(ACTIVITY_TYPE_STANDARD)
+ .setWindowingMode(WINDOWING_MODE_PINNED)
+ .setLastActiveTime(100)
+ .apply { bounds?.let { setBounds(it) } }
+ .build()
+
fun createFullscreenTaskBuilder(displayId: Int = DEFAULT_DISPLAY): TestRunningTaskInfoBuilder =
TestRunningTaskInfoBuilder()
.setDisplayId(displayId)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/RemoteTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/RemoteTransitionHandlerTest.kt
new file mode 100644
index 0000000..048981d
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/RemoteTransitionHandlerTest.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.transition
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.view.WindowManager
+import android.window.RemoteTransition
+import android.window.TransitionFilter
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import android.window.WindowContainerTransaction
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestSyncExecutor
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+
+/**
+ * Test class for [RemoteTransitionHandler].
+ *
+ * atest WMShellUnitTests:RemoteTransitionHandlerTest
+ */
+@RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class RemoteTransitionHandlerTest : ShellTestCase() {
+
+ private val testExecutor: TestSyncExecutor = TestSyncExecutor()
+
+ private val testRemoteTransition = RemoteTransition(TestRemoteTransition())
+ private lateinit var handler: RemoteTransitionHandler
+
+ @Before
+ fun setUp() {
+ handler = RemoteTransitionHandler(testExecutor)
+ }
+
+ @Test
+ fun handleRequest_noRemoteTransition_returnsNull() {
+ val request = TransitionRequestInfo(WindowManager.TRANSIT_OPEN, null, null)
+
+ assertNull(handler.handleRequest(mock(), request))
+ }
+
+ @Test
+ fun handleRequest_testRemoteTransition_returnsWindowContainerTransaction() {
+ val request = TransitionRequestInfo(WindowManager.TRANSIT_OPEN, null, testRemoteTransition)
+
+ assertTrue(handler.handleRequest(mock(), request) is WindowContainerTransaction)
+ }
+
+ @Test
+ fun startAnimation_noRemoteTransition_returnsFalse() {
+ val request = TransitionRequestInfo(WindowManager.TRANSIT_OPEN, null, null)
+ handler.handleRequest(mock(), request)
+
+ val isHandled = handler.startAnimation(
+ /* transition= */ mock(),
+ /* info= */ createTransitionInfo(),
+ /* startTransaction= */ mock(),
+ /* finishTransaction= */ mock(),
+ /* finishCallback= */ {},
+ )
+
+ assertFalse(isHandled)
+ }
+
+ @Test
+ fun startAnimation_remoteTransition_returnsTrue() {
+ val request = TransitionRequestInfo(WindowManager.TRANSIT_OPEN, null, testRemoteTransition)
+ handler.addFiltered(TransitionFilter(), testRemoteTransition)
+ handler.handleRequest(mock(), request)
+
+ val isHandled = handler.startAnimation(
+ /* transition= */ testRemoteTransition.remoteTransition.asBinder(),
+ /* info= */ createTransitionInfo(),
+ /* startTransaction= */ mock(),
+ /* finishTransaction= */ mock(),
+ /* finishCallback= */ {},
+ )
+
+ assertTrue(isHandled)
+ }
+
+ private fun createTransitionInfo(
+ type: Int = WindowManager.TRANSIT_OPEN,
+ changeMode: Int = WindowManager.TRANSIT_CLOSE,
+ ): TransitionInfo =
+ TransitionInfo(type, /* flags= */ 0).apply {
+ addChange(
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = changeMode
+ parent = null
+ }
+ )
+ }
+}
\ No newline at end of file
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 1371b38..8783249 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
@@ -60,7 +60,7 @@
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.TypedArray;
-import android.graphics.PointF;
+import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Region;
import android.net.Uri;
@@ -1805,7 +1805,7 @@
@NonNull DisplayController displayController,
@NonNull ActivityManager.RunningTaskInfo taskInfo,
@NonNull Context decorWindowContext,
- @NonNull Function2<? super Integer,? super Integer,? extends PointF>
+ @NonNull Function2<? super Integer,? super Integer,? extends Point>
positionSupplier,
@NonNull Supplier<SurfaceControl.Transaction> transactionSupplier) {
return mMaximizeMenu;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt
index bc8faed..e4424f3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt
@@ -16,6 +16,7 @@
package com.android.wm.shell.windowdecor.tiling
import android.app.ActivityManager
+import android.app.ActivityManager.RunningTaskInfo
import android.content.Context
import android.content.res.Resources
import android.graphics.Rect
@@ -24,8 +25,10 @@
import android.view.MotionEvent
import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_PIP
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
import android.window.WindowContainerTransaction
import androidx.test.filters.SmallTest
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
@@ -40,6 +43,7 @@
import com.android.wm.shell.desktopmode.DesktopRepository
import com.android.wm.shell.desktopmode.DesktopTasksController
import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
+import com.android.wm.shell.desktopmode.DesktopTestHelpers.createPinnedTask
import com.android.wm.shell.desktopmode.DesktopUserRepositories
import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator
import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler
@@ -552,6 +556,37 @@
}
@Test
+ fun taskTiled_shouldBeRemoved_whenEnteringPip() {
+ val task1 = createPipTask()
+ val stableBounds = STABLE_BOUNDS_MOCK
+ whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+ whenever(tiledTaskHelper.taskInfo).thenReturn(task1)
+ whenever(tiledTaskHelper.desktopModeWindowDecoration).thenReturn(desktopWindowDecoration)
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.LEFT,
+ BOUNDS,
+ )
+ tilingDecoration.leftTaskResizingHelper = tiledTaskHelper
+ val changeInfo = createPipChangeTransition(task1)
+ tilingDecoration.onTransitionReady(
+ transition = mock(),
+ info = changeInfo,
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ )
+
+ assertThat(tilingDecoration.leftTaskResizingHelper).isNull()
+ verify(tiledTaskHelper, times(1)).dispose()
+ }
+
+ @Test
fun taskNotTiled_shouldNotBeRemoved_whenNotTiled() {
val task1 = createVisibleTask()
val task2 = createVisibleTask()
@@ -652,6 +687,23 @@
whenever(userRepositories.current.isVisibleTask(eq(it.taskId))).thenReturn(true)
}
+ private fun createPipTask() =
+ createPinnedTask().also {
+ whenever(userRepositories.current.isVisibleTask(eq(it.taskId))).thenReturn(true)
+ }
+
+ private fun createPipChangeTransition(task: RunningTaskInfo?, type: Int = TRANSIT_PIP) =
+ TransitionInfo(type, /* flags= */ 0).apply {
+ addChange(
+ Change(mock(), mock()).apply {
+ mode = TRANSIT_PIP
+ parent = null
+ taskInfo = task
+ flags = flags
+ }
+ )
+ }
+
companion object {
private val NON_STABLE_BOUNDS_MOCK = Rect(50, 55, 100, 100)
private val STABLE_BOUNDS_MOCK = Rect(0, 0, 100, 100)
diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp
index a18c5f5..8ecd6ba 100644
--- a/libs/androidfw/ResourceTypes.cpp
+++ b/libs/androidfw/ResourceTypes.cpp
@@ -6520,41 +6520,79 @@
}
bool ResTable::getResourceFlags(uint32_t resID, uint32_t* outFlags) const {
- if (mError != NO_ERROR) {
- return false;
- }
+ if (mError != NO_ERROR) {
+ return false;
+ }
- const ssize_t p = getResourcePackageIndex(resID);
- const int t = Res_GETTYPE(resID);
- const int e = Res_GETENTRY(resID);
+ const ssize_t p = getResourcePackageIndex(resID);
+ const int t = Res_GETTYPE(resID);
+ const int e = Res_GETENTRY(resID);
- if (p < 0) {
- if (Res_GETPACKAGE(resID)+1 == 0) {
- ALOGW("No package identifier when getting flags for resource number 0x%08x", resID);
- } else {
- ALOGW("No known package when getting flags for resource number 0x%08x", resID);
- }
- return false;
+ if (p < 0) {
+ if (Res_GETPACKAGE(resID)+1 == 0) {
+ ALOGW("No package identifier when getting flags for resource number 0x%08x", resID);
+ } else {
+ ALOGW("No known package when getting flags for resource number 0x%08x", resID);
}
- if (t < 0) {
- ALOGW("No type identifier when getting flags for resource number 0x%08x", resID);
- return false;
- }
+ return false;
+ }
+ if (t < 0) {
+ ALOGW("No type identifier when getting flags for resource number 0x%08x", resID);
+ return false;
+ }
- const PackageGroup* const grp = mPackageGroups[p];
- if (grp == NULL) {
- ALOGW("Bad identifier when getting flags for resource number 0x%08x", resID);
- return false;
- }
+ const PackageGroup* const grp = mPackageGroups[p];
+ if (grp == NULL) {
+ ALOGW("Bad identifier when getting flags for resource number 0x%08x", resID);
+ return false;
+ }
- Entry entry;
- status_t err = getEntry(grp, t, e, NULL, &entry);
- if (err != NO_ERROR) {
- return false;
- }
+ Entry entry;
+ status_t err = getEntry(grp, t, e, NULL, &entry);
+ if (err != NO_ERROR) {
+ return false;
+ }
- *outFlags = entry.specFlags;
- return true;
+ *outFlags = entry.specFlags;
+ return true;
+}
+
+bool ResTable::getResourceEntryFlags(uint32_t resID, uint32_t* outFlags) const {
+ if (mError != NO_ERROR) {
+ return false;
+ }
+
+ const ssize_t p = getResourcePackageIndex(resID);
+ const int t = Res_GETTYPE(resID);
+ const int e = Res_GETENTRY(resID);
+
+ if (p < 0) {
+ if (Res_GETPACKAGE(resID)+1 == 0) {
+ ALOGW("No package identifier when getting flags for resource number 0x%08x", resID);
+ } else {
+ ALOGW("No known package when getting flags for resource number 0x%08x", resID);
+ }
+ return false;
+ }
+ if (t < 0) {
+ ALOGW("No type identifier when getting flags for resource number 0x%08x", resID);
+ return false;
+ }
+
+ const PackageGroup* const grp = mPackageGroups[p];
+ if (grp == NULL) {
+ ALOGW("Bad identifier when getting flags for resource number 0x%08x", resID);
+ return false;
+ }
+
+ Entry entry;
+ status_t err = getEntry(grp, t, e, NULL, &entry);
+ if (err != NO_ERROR) {
+ return false;
+ }
+
+ *outFlags = entry.entry->flags();
+ return true;
}
bool ResTable::isPackageDynamic(uint8_t packageID) const {
diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h
index 0d45149..63b28da 100644
--- a/libs/androidfw/include/androidfw/ResourceTypes.h
+++ b/libs/androidfw/include/androidfw/ResourceTypes.h
@@ -1593,6 +1593,8 @@
// If set, this is a compact entry with data type and value directly
// encoded in the this entry, see ResTable_entry::compact
FLAG_COMPACT = 0x0008,
+ // If set, this entry relies on read write android feature flags
+ FLAG_USES_FEATURE_FLAGS = 0x0010,
};
struct Full {
@@ -1622,6 +1624,7 @@
uint16_t flags() const { return dtohs(full.flags); };
bool is_compact() const { return flags() & FLAG_COMPACT; }
bool is_complex() const { return flags() & FLAG_COMPLEX; }
+ bool uses_feature_flags() const { return flags() & FLAG_USES_FEATURE_FLAGS; }
size_t size() const {
return is_compact() ? sizeof(ResTable_entry) : dtohs(this->full.size);
@@ -2039,6 +2042,8 @@
bool getResourceFlags(uint32_t resID, uint32_t* outFlags) const;
+ bool getResourceEntryFlags(uint32_t resID, uint32_t* outFlags) const;
+
/**
* Returns whether or not the package for the given resource has been dynamically assigned.
* If the resource can't be found, returns 'false'.
diff --git a/media/java/android/media/quality/MediaQualityContract.java b/media/java/android/media/quality/MediaQualityContract.java
index e4de3e4..fccdba8 100644
--- a/media/java/android/media/quality/MediaQualityContract.java
+++ b/media/java/android/media/quality/MediaQualityContract.java
@@ -82,7 +82,7 @@
String PARAMETER_NAME = "_name";
String PARAMETER_PACKAGE = "_package";
String PARAMETER_INPUT_ID = "_input_id";
-
+ String VENDOR_PARAMETERS = "_vendor_parameters";
}
/**
diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
index d55bbb3..bf73962 100644
--- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
+++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
@@ -187,7 +187,9 @@
if (mLottieDynamicColor) {
LottieColorUtils.applyDynamicColors(getContext(), illustrationView);
}
- LottieColorUtils.applyMaterialColor(getContext(), illustrationView);
+ if (SettingsThemeHelper.isExpressiveTheme(getContext())) {
+ LottieColorUtils.applyMaterialColor(getContext(), illustrationView);
+ }
if (mOnBindListener != null) {
mOnBindListener.onBind(illustrationView);
diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java
index 4421424..e59cc81 100644
--- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java
+++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java
@@ -157,10 +157,6 @@
/** Applies material colors. */
public static void applyMaterialColor(@NonNull Context context,
@NonNull LottieAnimationView lottieAnimationView) {
- if (!SettingsThemeHelper.isExpressiveTheme(context)) {
- return;
- }
-
for (String key : MATERIAL_COLOR_MAP.keySet()) {
final int color = context.getColor(MATERIAL_COLOR_MAP.get(key));
lottieAnimationView.addValueCallback(
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserActivity.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserActivity.java
index c5e6f60..2b95fd1 100644
--- a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserActivity.java
+++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserActivity.java
@@ -93,18 +93,10 @@
@Override
public boolean onTouchEvent(@Nullable MotionEvent event) {
- onBackInvoked();
+ cancel();
return super.onTouchEvent(event);
}
- private void onBackInvoked() {
- if (mSetupUserDialog != null) {
- mSetupUserDialog.dismiss();
- }
- setResult(RESULT_CANCELED);
- finish();
- }
-
@VisibleForTesting
void setSuccessResult(String userName, Drawable userIcon, String path, Boolean isAdmin) {
Intent intent = new Intent(this, CreateUserActivity.class);
@@ -112,14 +104,12 @@
intent.putExtra(EXTRA_IS_ADMIN, isAdmin);
intent.putExtra(EXTRA_USER_ICON_PATH, path);
- mSetupUserDialog.dismiss();
setResult(RESULT_OK, intent);
finish();
}
@VisibleForTesting
void cancel() {
- mSetupUserDialog.dismiss();
setResult(RESULT_CANCELED);
finish();
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
index d9f1b63..bce229f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
@@ -282,7 +282,7 @@
mCustomDialogHelper.getDialog().dismiss();
break;
case EXIT_DIALOG:
- finish();
+ mUserCreationDialog.dismiss();
break;
default:
if (mCurrentState < EXIT_DIALOG) {
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserActivityTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserActivityTest.java
index f58eb7c..220c03e 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserActivityTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserActivityTest.java
@@ -65,23 +65,23 @@
}
@Test
- public void onTouchEvent_dismissesDialogAndCancelsResult() {
+ public void onTouchEvent_finishesActivityAndCancelsResult() {
mCreateUserActivity.onTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0,
0));
- assertThat(mCreateUserActivity.mSetupUserDialog.isShowing()).isFalse();
+ assertThat(mCreateUserActivity.isFinishing()).isTrue();
assertThat(shadowOf(mCreateUserActivity).getResultCode())
.isEqualTo(Activity.RESULT_CANCELED);
}
@Test
- public void setSuccessResult_dismissesDialogAndSetsSuccessResult() {
+ public void setSuccessResult_finishesActivityAndSetsSuccessResult() {
Drawable mockDrawable = mock(Drawable.class);
mCreateUserActivity.setSuccessResult(TEST_USER_NAME, mockDrawable, TEST_USER_ICON_PATH,
TEST_IS_ADMIN);
- assertThat(mCreateUserActivity.mSetupUserDialog.isShowing()).isFalse();
+ assertThat(mCreateUserActivity.isFinishing()).isTrue();
assertThat(shadowOf(mCreateUserActivity).getResultCode()).isEqualTo(Activity.RESULT_OK);
Intent resultIntent = shadowOf(mCreateUserActivity).getResultIntent();
@@ -92,10 +92,10 @@
}
@Test
- public void cancel_dismissesDialogAndSetsCancelResult() {
+ public void cancel_finishesActivityAndSetsCancelResult() {
mCreateUserActivity.cancel();
- assertThat(mCreateUserActivity.mSetupUserDialog.isShowing()).isFalse();
+ assertThat(mCreateUserActivity.isFinishing()).isTrue();
assertThat(shadowOf(mCreateUserActivity).getResultCode())
.isEqualTo(Activity.RESULT_CANCELED);
}
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 72ae76a..f628a42 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -312,6 +312,9 @@
<!-- Permission necessary to change car audio volume through CarAudioManager -->
<uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_VOLUME" />
+ <!-- To detect when projecting to Android Auto -->
+ <uses-permission android:name="android.permission.READ_PROJECTION_STATE" />
+
<!-- Permission to control Android Debug Bridge (ADB) -->
<uses-permission android:name="android.permission.MANAGE_DEBUGGING" />
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 3c86f28..9dd49d6 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1401,6 +1401,16 @@
}
flag {
+ name: "media_controls_device_manager_background_execution"
+ namespace: "systemui"
+ description: "Sends some instances creation to background thread"
+ bug: "400200474"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "output_switcher_redesign"
namespace: "systemui"
description: "Enables visual update for Media Output Switcher"
@@ -1843,6 +1853,16 @@
}
flag {
+ name: "disable_shade_visible_with_blur"
+ namespace: "systemui"
+ description: "Removes the check for a blur radius when determining shade window visibility"
+ bug: "356804470"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "notification_row_transparency"
namespace: "systemui"
description: "Enables transparency on the Notification Shade."
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
index e43b8a0..7ee6a6e 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
@@ -107,6 +107,16 @@
*/
// TODO(b/301385865): Remove this flag.
private val disableWmTimeout: Boolean = false,
+
+ /**
+ * Whether we should disable the reparent transaction that puts the opening/closing window above
+ * the view's window. This should be set to true in tests only, where we can't currently use a
+ * valid leash.
+ *
+ * TODO(b/397180418): Remove this flag when we don't have the RemoteAnimation wrapper anymore
+ * and we can just inject a fake transaction.
+ */
+ private val skipReparentTransaction: Boolean = false,
) {
@JvmOverloads
constructor(
@@ -1140,6 +1150,7 @@
DelegatingAnimationCompletionListener(listener, this::dispose),
transitionAnimator,
disableWmTimeout,
+ skipReparentTransaction,
)
}
@@ -1173,6 +1184,16 @@
*/
// TODO(b/301385865): Remove this flag.
disableWmTimeout: Boolean = false,
+
+ /**
+ * Whether we should disable the reparent transaction that puts the opening/closing window
+ * above the view's window. This should be set to true in tests only, where we can't
+ * currently use a valid leash.
+ *
+ * TODO(b/397180418): Remove this flag when we don't have the RemoteAnimation wrapper
+ * anymore and we can just inject a fake transaction.
+ */
+ private val skipReparentTransaction: Boolean = false,
) : RemoteAnimationDelegate<IRemoteAnimationFinishedCallback> {
private val transitionContainer = controller.transitionContainer
private val context = transitionContainer.context
@@ -1515,7 +1536,7 @@
)
}
- if (moveTransitionAnimationLayer()) {
+ if (moveTransitionAnimationLayer() && !skipReparentTransaction) {
// Ensure that the launching window is rendered above the view's window,
// so it is not obstructed.
// TODO(b/397180418): re-use the start transaction once the
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt
index 9a74687..e07d7b3 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt
@@ -16,17 +16,18 @@
package com.android.systemui.animation
+import kotlin.text.buildString
+
class FontVariationUtils {
private var mWeight = -1
private var mWidth = -1
private var mOpticalSize = -1
private var mRoundness = -1
- private var isUpdated = false
+ private var mCurrentFVar = ""
/*
* generate fontVariationSettings string, used for key in typefaceCache in TextAnimator
* the order of axes should align to the order of parameters
- * if every axis remains unchanged, return ""
*/
fun updateFontVariation(
weight: Int = -1,
@@ -34,15 +35,17 @@
opticalSize: Int = -1,
roundness: Int = -1,
): String {
- isUpdated = false
+ var isUpdated = false
if (weight >= 0 && mWeight != weight) {
isUpdated = true
mWeight = weight
}
+
if (width >= 0 && mWidth != width) {
isUpdated = true
mWidth = width
}
+
if (opticalSize >= 0 && mOpticalSize != opticalSize) {
isUpdated = true
mOpticalSize = opticalSize
@@ -52,23 +55,32 @@
isUpdated = true
mRoundness = roundness
}
- var resultString = ""
- if (mWeight >= 0) {
- resultString += "'${GSFAxes.WEIGHT.tag}' $mWeight"
+
+ if (!isUpdated) {
+ return mCurrentFVar
}
- if (mWidth >= 0) {
- resultString +=
- (if (resultString.isBlank()) "" else ", ") + "'${GSFAxes.WIDTH.tag}' $mWidth"
- }
- if (mOpticalSize >= 0) {
- resultString +=
- (if (resultString.isBlank()) "" else ", ") +
- "'${GSFAxes.OPTICAL_SIZE.tag}' $mOpticalSize"
- }
- if (mRoundness >= 0) {
- resultString +=
- (if (resultString.isBlank()) "" else ", ") + "'${GSFAxes.ROUND.tag}' $mRoundness"
- }
- return if (isUpdated) resultString else ""
+
+ return buildString {
+ if (mWeight >= 0) {
+ if (!isBlank()) append(", ")
+ append("'${GSFAxes.WEIGHT.tag}' $mWeight")
+ }
+
+ if (mWidth >= 0) {
+ if (!isBlank()) append(", ")
+ append("'${GSFAxes.WIDTH.tag}' $mWidth")
+ }
+
+ if (mOpticalSize >= 0) {
+ if (!isBlank()) append(", ")
+ append("'${GSFAxes.OPTICAL_SIZE.tag}' $mOpticalSize")
+ }
+
+ if (mRoundness >= 0) {
+ if (!isBlank()) append(", ")
+ append("'${GSFAxes.ROUND.tag}' $mRoundness")
+ }
+ }
+ .also { mCurrentFVar = it }
}
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
index 5d9c441..a4a96d1 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
@@ -1006,13 +1006,32 @@
Log.d(TAG, "Animation ended")
}
- // TODO(b/330672236): Post this to the main thread instead so that it does not
- // flicker with Flexiglass enabled.
- controller.onTransitionAnimationEnd(isExpandingFullyAbove)
- transitionContainerOverlay.remove(windowBackgroundLayer)
+ val onEnd = {
+ controller.onTransitionAnimationEnd(isExpandingFullyAbove)
+ transitionContainerOverlay.remove(windowBackgroundLayer)
- if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) {
- openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
+ if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) {
+ openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
+ }
+ }
+ // TODO(b/330672236): Post this to the main thread for launches as well, so that they do not
+ // flicker with Flexiglass enabled.
+ if (controller.isLaunching) {
+ onEnd()
+ } else {
+ // onAnimationEnd is called at the end of the animation, on a Choreographer animation
+ // tick. During dialog launches, the following calls will move the animated content from
+ // the dialog overlay back to its original position, and this change must be reflected
+ // in the next frame given that we then sync the next frame of both the content and
+ // dialog ViewRoots. During SysUI activity launches, we will instantly collapse the
+ // shade at the end of the transition. However, if those are rendered by Compose, whose
+ // compositions are also scheduled on a Choreographer frame, any state change made
+ // *right now* won't be reflected in the next frame given that a Choreographer frame
+ // can't schedule another and have it happen in the same frame. So we post the forwarded
+ // calls to [Controller.onLaunchAnimationEnd] in the main executor, leaving this
+ // Choreographer frame, ensuring that any state change applied by
+ // onTransitionAnimationEnd() will be reflected in the same frame.
+ mainExecutor.execute { onEnd() }
}
}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RunBlockingDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RunBlockingDetector.kt
new file mode 100644
index 0000000..fce536a
--- /dev/null
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RunBlockingDetector.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2025 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.systemui.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UFile
+import org.jetbrains.uast.UImportStatement
+
+/** Detects whether [runBlocking] is being imported. */
+class RunBlockingDetector : Detector(), SourceCodeScanner {
+
+ override fun getApplicableUastTypes(): List<Class<out UElement>> {
+ return listOf(UFile::class.java)
+ }
+
+ override fun createUastHandler(context: JavaContext): UElementHandler {
+ return object : UElementHandler() {
+ override fun visitFile(node: UFile) {
+ for (importStatement in node.imports) {
+ visitImportStatement(context, importStatement)
+ }
+ }
+ }
+ }
+
+ private fun visitImportStatement(context: JavaContext, importStatement: UImportStatement) {
+ val importName = importStatement.importReference?.asSourceString()
+ if (FORBIDDEN_IMPORTS.contains(importName)) {
+ context.report(
+ ISSUE,
+ importStatement as UElement,
+ context.getLocation(importStatement),
+ "Importing $importName is not allowed.",
+ )
+ }
+ }
+
+ companion object {
+ @JvmField
+ val ISSUE =
+ Issue.create(
+ id = "RunBlockingUsage",
+ briefDescription = "Discouraged runBlocking call",
+ explanation =
+ """
+ Using `runBlocking` is generally discouraged in Android
+ development as it can lead to UI freezes and ANRs.
+ Consider using `launch` or `async` with coroutine scope
+ instead. If needed from java, consider introducing a method
+ with a callback instead from kotlin.
+ """,
+ category = Category.PERFORMANCE,
+ priority = 8,
+ severity = Severity.WARNING,
+ implementation =
+ Implementation(RunBlockingDetector::class.java, Scope.JAVA_FILE_SCOPE),
+ )
+
+ val FORBIDDEN_IMPORTS = listOf("kotlinx.coroutines.runBlocking")
+ }
+}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
index adb3116..b455c00 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
@@ -50,6 +50,7 @@
ShadeDisplayAwareDialogDetector.ISSUE,
RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING,
RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR,
+ RunBlockingDetector.ISSUE,
)
override val api: Int
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RunBlockingDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RunBlockingDetectorTest.kt
new file mode 100644
index 0000000..4ae429d2
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RunBlockingDetectorTest.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2025 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.systemui.lint
+
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+
+class RunBlockingDetectorTest : SystemUILintDetectorTest() {
+
+ override fun getDetector(): Detector = RunBlockingDetector()
+
+ override fun getIssues(): List<Issue> = listOf(RunBlockingDetector.ISSUE)
+
+ @Test
+ fun testViolation() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package com.example
+
+ import kotlinx.coroutines.runBlocking
+
+ class MyClass {
+ fun myMethod() {
+ runBlocking {
+ // Some code here
+ }
+ }
+ }
+ """
+ ),
+ RUN_BLOCKING_DEFINITION,
+ )
+ .issues(RunBlockingDetector.ISSUE)
+ .run()
+ .expect(
+ """
+src/com/example/MyClass.kt:4: Warning: Importing kotlinx.coroutines.runBlocking is not allowed. [RunBlockingUsage]
+ import kotlinx.coroutines.runBlocking
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+0 errors, 1 warnings
+"""
+ .trimIndent()
+ )
+ }
+
+ // Verifies that the lint check does *not* flag calls to other methods.
+ @Test
+ fun testNotViolation() {
+ lint()
+ .detector(RunBlockingDetector())
+ .issues(RunBlockingDetector.ISSUE)
+ .files(
+ kotlin(
+ """
+ package com.example
+
+ class MyClass {
+ fun myMethod() {
+ myOtherMethod {
+ }
+ }
+
+ fun myOtherMethod(block: () -> Unit) {
+ block()
+ }
+ }
+ """
+ )
+ )
+ .run()
+ .expectClean()
+ }
+
+ private companion object {
+ val RUN_BLOCKING_DEFINITION =
+ kotlin(
+ """
+ package kotlinx.coroutines
+
+ fun runBlocking(block: suspend () -> Unit) {
+ // Implementation details don't matter for this test.
+ }
+ """
+ )
+ }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 4bf0ceb..6e29e69 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -38,8 +38,9 @@
import com.android.systemui.customization.R
import com.android.systemui.log.core.LogLevel
import com.android.systemui.log.core.LogcatOnlyMessageBuffer
-import com.android.systemui.log.core.Logger
import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.plugins.clocks.ClockLogger
+import com.android.systemui.plugins.clocks.ClockLogger.Companion.escapeTime
import java.io.PrintWriter
import java.util.Calendar
import java.util.Locale
@@ -67,7 +68,7 @@
var messageBuffer: MessageBuffer
get() = logger.buffer
set(value) {
- logger = Logger(value, TAG)
+ logger = ClockLogger(this, value, TAG)
}
var hasCustomPositionUpdatedAnimation: Boolean = false
@@ -185,7 +186,9 @@
time.timeInMillis = timeOverrideInMillis ?: System.currentTimeMillis()
contentDescription = DateFormat.format(descFormat, time)
val formattedText = DateFormat.format(format, time)
- logger.d({ "refreshTime: new formattedText=$str1" }) { str1 = formattedText?.toString() }
+ logger.d({ "refreshTime: new formattedText=${escapeTime(str1)}" }) {
+ str1 = formattedText?.toString()
+ }
// Setting text actually triggers a layout pass in TextView (because the text view is set to
// wrap_content width and TextView always relayouts for this). This avoids needless relayout
@@ -195,7 +198,7 @@
}
text = formattedText
- logger.d({ "refreshTime: done setting new time text to: $str1" }) {
+ logger.d({ "refreshTime: done setting new time text to: ${escapeTime(str1)}" }) {
str1 = formattedText?.toString()
}
@@ -225,7 +228,7 @@
@SuppressLint("DrawAllocation")
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
- logger.d("onMeasure")
+ logger.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (!isSingleLineInternal && MeasureSpec.getMode(heightMeasureSpec) == EXACTLY) {
// Call straight into TextView.setTextSize to avoid setting lastUnconstrainedTextSize
@@ -263,14 +266,14 @@
canvas.translate(parentWidth / 4f, 0f)
}
- logger.d({ "onDraw($str1)" }) { str1 = text.toString() }
+ logger.onDraw("$text")
// intentionally doesn't call super.onDraw here or else the text will be rendered twice
textAnimator?.draw(canvas)
canvas.restore()
}
override fun invalidate() {
- logger.d("invalidate")
+ logger.invalidate()
super.invalidate()
}
@@ -280,7 +283,7 @@
lengthBefore: Int,
lengthAfter: Int,
) {
- logger.d({ "onTextChanged($str1)" }) { str1 = text.toString() }
+ logger.d({ "onTextChanged(${escapeTime(str1)})" }) { str1 = "$text" }
super.onTextChanged(text, start, lengthBefore, lengthAfter)
}
@@ -370,7 +373,7 @@
return
}
- logger.d("animateCharge")
+ logger.animateCharge()
val startAnimPhase2 = Runnable {
setTextStyle(
weight = if (isDozing()) dozingWeight else lockScreenWeight,
@@ -394,7 +397,7 @@
}
fun animateDoze(isDozing: Boolean, animate: Boolean) {
- logger.d("animateDoze")
+ logger.animateDoze(isDozing, animate)
setTextStyle(
weight = if (isDozing) dozingWeight else lockScreenWeight,
color = if (isDozing) dozingColor else lockScreenColor,
@@ -484,7 +487,7 @@
isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12
else -> DOUBLE_LINE_FORMAT_12_HOUR
}
- logger.d({ "refreshFormat($str1)" }) { str1 = format?.toString() }
+ logger.d({ "refreshFormat(${escapeTime(str1)})" }) { str1 = format?.toString() }
descFormat = if (use24HourFormat) Patterns.sClockView24 else Patterns.sClockView12
refreshTime()
@@ -634,7 +637,7 @@
companion object {
private val TAG = AnimatableClockView::class.simpleName!!
- private val DEFAULT_LOGGER = Logger(LogcatOnlyMessageBuffer(LogLevel.WARNING), TAG)
+ private val DEFAULT_LOGGER = ClockLogger(null, LogcatOnlyMessageBuffer(LogLevel.DEBUG), TAG)
const val ANIMATION_DURATION_FOLD_TO_AOD: Int = 600
private const val DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm"
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt
index dd1599e..9857d7f 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt
@@ -17,6 +17,8 @@
package com.android.systemui.shared.clocks
import android.graphics.Canvas
+import com.android.systemui.plugins.clocks.VPoint
+import com.android.systemui.plugins.clocks.VPointF
object CanvasUtil {
fun Canvas.translate(pt: VPointF) = this.translate(pt.x, pt.y)
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt
index f5ccc52..941cebf 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt
@@ -20,7 +20,8 @@
import android.animation.AnimatorListenerAdapter
import android.animation.TimeInterpolator
import android.animation.ValueAnimator
-import com.android.systemui.shared.clocks.VPointF.Companion.times
+import com.android.systemui.plugins.clocks.VPointF
+import com.android.systemui.plugins.clocks.VPointF.Companion.times
class DigitTranslateAnimator(private val updateCallback: (VPointF) -> Unit) {
var currentTranslation = VPointF.ZERO
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt
index 336c66e..9ac9e60 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt
@@ -16,13 +16,13 @@
package com.android.systemui.shared.clocks
-import android.graphics.RectF
import android.view.View
import androidx.annotation.VisibleForTesting
import com.android.systemui.plugins.clocks.ClockAnimations
import com.android.systemui.plugins.clocks.ClockEvents
import com.android.systemui.plugins.clocks.ClockFaceConfig
import com.android.systemui.plugins.clocks.ClockFaceEvents
+import com.android.systemui.plugins.clocks.VRectF
interface SimpleClockLayerController {
val view: View
@@ -32,5 +32,5 @@
val config: ClockFaceConfig
@VisibleForTesting var fakeTimeMills: Long?
- var onViewBoundsChanged: ((RectF) -> Unit)?
+ var onViewBoundsChanged: ((VRectF) -> Unit)?
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt
index 1e90a23..0740b0e 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt
@@ -18,8 +18,9 @@
import android.graphics.Rect
import android.view.View
-import com.android.systemui.shared.clocks.VPoint.Companion.center
-import com.android.systemui.shared.clocks.VPointF.Companion.center
+import com.android.systemui.plugins.clocks.VPoint.Companion.center
+import com.android.systemui.plugins.clocks.VPointF
+import com.android.systemui.plugins.clocks.VPointF.Companion.center
object ViewUtils {
fun View.computeLayoutDiff(targetRegion: Rect, isLargeClock: Boolean): VPointF {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
index 2dc3e2b..ba32ab0 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
@@ -17,7 +17,6 @@
package com.android.systemui.shared.clocks.view
import android.graphics.Canvas
-import android.graphics.RectF
import android.icu.text.NumberFormat
import android.util.MathUtils.constrainedMap
import android.view.View
@@ -29,14 +28,15 @@
import com.android.systemui.customization.R
import com.android.systemui.plugins.clocks.ClockFontAxisSetting
import com.android.systemui.plugins.clocks.ClockLogger
+import com.android.systemui.plugins.clocks.VPoint
+import com.android.systemui.plugins.clocks.VPointF
+import com.android.systemui.plugins.clocks.VPointF.Companion.max
+import com.android.systemui.plugins.clocks.VPointF.Companion.times
+import com.android.systemui.plugins.clocks.VRectF
import com.android.systemui.shared.clocks.CanvasUtil.translate
import com.android.systemui.shared.clocks.CanvasUtil.use
import com.android.systemui.shared.clocks.ClockContext
import com.android.systemui.shared.clocks.DigitTranslateAnimator
-import com.android.systemui.shared.clocks.VPoint
-import com.android.systemui.shared.clocks.VPointF
-import com.android.systemui.shared.clocks.VPointF.Companion.max
-import com.android.systemui.shared.clocks.VPointF.Companion.times
import com.android.systemui.shared.clocks.ViewUtils.measuredSize
import java.util.Locale
import kotlin.collections.filterNotNull
@@ -101,7 +101,7 @@
updateLocale(Locale.getDefault())
}
- var onViewBoundsChanged: ((RectF) -> Unit)? = null
+ var onViewBoundsChanged: ((VRectF) -> Unit)? = null
private val digitOffsets = mutableMapOf<Int, Float>()
protected fun calculateSize(
@@ -189,13 +189,7 @@
fun updateLocation() {
val layoutBounds = this.layoutBounds ?: return
- val bounds =
- RectF(
- layoutBounds.centerX() - measuredWidth / 2f,
- layoutBounds.centerY() - measuredHeight / 2f,
- layoutBounds.centerX() + measuredWidth / 2f,
- layoutBounds.centerY() + measuredHeight / 2f,
- )
+ val bounds = VRectF.fromCenter(layoutBounds.center, this.measuredSize)
setFrame(
bounds.left.roundToInt(),
bounds.top.roundToInt(),
@@ -215,16 +209,11 @@
onAnimateDoze = null
}
- private val layoutBounds = RectF()
+ private var layoutBounds = VRectF.ZERO
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
logger.onLayout(changed, left, top, right, bottom)
-
- layoutBounds.left = left.toFloat()
- layoutBounds.top = top.toFloat()
- layoutBounds.right = right.toFloat()
- layoutBounds.bottom = bottom.toFloat()
-
+ layoutBounds = VRectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
updateChildFrames(isLayout = true)
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
index 0ec2d18..2af25fe 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
@@ -23,7 +23,6 @@
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.Rect
-import android.graphics.RectF
import android.os.VibrationEffect
import android.text.Layout
import android.text.TextPaint
@@ -45,6 +44,10 @@
import com.android.systemui.plugins.clocks.ClockFontAxisSetting.Companion.replace
import com.android.systemui.plugins.clocks.ClockFontAxisSetting.Companion.toFVar
import com.android.systemui.plugins.clocks.ClockLogger
+import com.android.systemui.plugins.clocks.VPoint
+import com.android.systemui.plugins.clocks.VPointF
+import com.android.systemui.plugins.clocks.VPointF.Companion.size
+import com.android.systemui.plugins.clocks.VRectF
import com.android.systemui.shared.Flags.ambientAod
import com.android.systemui.shared.clocks.CanvasUtil.translate
import com.android.systemui.shared.clocks.CanvasUtil.use
@@ -53,9 +56,6 @@
import com.android.systemui.shared.clocks.DimensionParser
import com.android.systemui.shared.clocks.FLEX_CLOCK_ID
import com.android.systemui.shared.clocks.FontTextStyle
-import com.android.systemui.shared.clocks.VPoint
-import com.android.systemui.shared.clocks.VPointF
-import com.android.systemui.shared.clocks.VPointF.Companion.size
import com.android.systemui.shared.clocks.ViewUtils.measuredSize
import com.android.systemui.shared.clocks.ViewUtils.size
import com.android.systemui.shared.clocks.toClockAxisSetting
@@ -66,11 +66,11 @@
private val TAG = SimpleDigitalClockTextView::class.simpleName!!
-private fun Paint.getTextBounds(text: CharSequence, result: RectF = RectF()): RectF {
- val rect = Rect()
- this.getTextBounds(text, 0, text.length, rect)
- result.set(rect)
- return result
+private val tempRect = Rect()
+
+private fun Paint.getTextBounds(text: CharSequence): VRectF {
+ this.getTextBounds(text, 0, text.length, tempRect)
+ return VRectF(tempRect)
}
enum class VerticalAlignment {
@@ -143,7 +143,7 @@
fidgetFontVariation = buildFidgetVariation(lsFontAxes).toFVar()
}
- var onViewBoundsChanged: ((RectF) -> Unit)? = null
+ var onViewBoundsChanged: ((VRectF) -> Unit)? = null
private val parser = DimensionParser(clockCtx.context)
var maxSingleDigitHeight = -1f
var maxSingleDigitWidth = -1f
@@ -159,13 +159,13 @@
private val initThread = Thread.currentThread()
// textBounds is the size of text in LS, which only measures current text in lockscreen style
- var textBounds = RectF()
+ var textBounds = VRectF.ZERO
// prevTextBounds and targetTextBounds are to deal with dozing animation between LS and AOD
// especially for the textView which has different bounds during the animation
// prevTextBounds holds the state we are transitioning from
- private val prevTextBounds = RectF()
+ private var prevTextBounds = VRectF.ZERO
// targetTextBounds holds the state we are interpolating to
- private val targetTextBounds = RectF()
+ private var targetTextBounds = VRectF.ZERO
protected val logger = ClockLogger(this, clockCtx.messageBuffer, this::class.simpleName!!)
get() = field ?: ClockLogger.INIT_LOGGER
@@ -215,8 +215,8 @@
lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation)
typeface = lockScreenPaint.typeface
- lockScreenPaint.getTextBounds(text, textBounds)
- targetTextBounds.set(textBounds)
+ textBounds = lockScreenPaint.getTextBounds(text)
+ targetTextBounds = textBounds
textAnimator.setTextStyle(TextAnimator.Style(fVar = lsFontVariation))
measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
@@ -287,7 +287,7 @@
canvas.use {
digitTranslateAnimator?.apply { canvas.translate(currentTranslation) }
canvas.translate(getDrawTranslation(interpBounds))
- if (isLayoutRtl()) canvas.translate(interpBounds.width() - textBounds.width(), 0f)
+ if (isLayoutRtl()) canvas.translate(interpBounds.width - textBounds.width, 0f)
textAnimator.draw(canvas)
}
}
@@ -302,16 +302,12 @@
super.setAlpha(alpha)
}
- private val layoutBounds = RectF()
+ private var layoutBounds = VRectF.ZERO
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
logger.onLayout(changed, left, top, right, bottom)
-
- layoutBounds.left = left.toFloat()
- layoutBounds.top = top.toFloat()
- layoutBounds.right = right.toFloat()
- layoutBounds.bottom = bottom.toFloat()
+ layoutBounds = VRectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
}
override fun invalidate() {
@@ -327,7 +323,7 @@
fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
if (!this::textAnimator.isInitialized) return
- logger.animateDoze()
+ logger.animateDoze(isDozing, isAnimated)
textAnimator.setTextStyle(
TextAnimator.Style(
fVar = if (isDozing) aodFontVariation else lsFontVariation,
@@ -341,6 +337,11 @@
),
)
updateTextBoundsForTextAnimator()
+
+ if (!isAnimated) {
+ requestLayout()
+ (parent as? FlexClockView)?.requestLayout()
+ }
}
fun animateCharge() {
@@ -408,10 +409,10 @@
}
fun refreshText() {
- lockScreenPaint.getTextBounds(text, textBounds)
- if (this::textAnimator.isInitialized) {
- textAnimator.textInterpolator.targetPaint.getTextBounds(text, targetTextBounds)
- }
+ textBounds = lockScreenPaint.getTextBounds(text)
+ targetTextBounds =
+ if (!this::textAnimator.isInitialized) textBounds
+ else textAnimator.textInterpolator.targetPaint.getTextBounds(text)
if (layout == null) {
requestLayout()
@@ -432,23 +433,23 @@
}
/** Returns the interpolated text bounding rect based on interpolation progress */
- private fun getInterpolatedTextBounds(progress: Float = getInterpolatedProgress()): RectF {
+ private fun getInterpolatedTextBounds(progress: Float = getInterpolatedProgress()): VRectF {
if (progress <= 0f) {
return prevTextBounds
} else if (!textAnimator.isRunning || progress >= 1f) {
return targetTextBounds
}
- return RectF().apply {
- left = lerp(prevTextBounds.left, targetTextBounds.left, progress)
- right = lerp(prevTextBounds.right, targetTextBounds.right, progress)
- top = lerp(prevTextBounds.top, targetTextBounds.top, progress)
- bottom = lerp(prevTextBounds.bottom, targetTextBounds.bottom, progress)
- }
+ return VRectF(
+ left = lerp(prevTextBounds.left, targetTextBounds.left, progress),
+ right = lerp(prevTextBounds.right, targetTextBounds.right, progress),
+ top = lerp(prevTextBounds.top, targetTextBounds.top, progress),
+ bottom = lerp(prevTextBounds.bottom, targetTextBounds.bottom, progress),
+ )
}
private fun computeMeasuredSize(
- interpBounds: RectF,
+ interpBounds: VRectF,
widthMeasureSpec: Int = measuredWidthAndState,
heightMeasureSpec: Int = measuredHeightAndState,
): VPointF {
@@ -461,11 +462,11 @@
return VPointF(
when {
mode.x == EXACTLY -> MeasureSpec.getSize(widthMeasureSpec).toFloat()
- else -> interpBounds.width() + 2 * lockScreenPaint.strokeWidth
+ else -> interpBounds.width + 2 * lockScreenPaint.strokeWidth
},
when {
mode.y == EXACTLY -> MeasureSpec.getSize(heightMeasureSpec).toFloat()
- else -> interpBounds.height() + 2 * lockScreenPaint.strokeWidth
+ else -> interpBounds.height + 2 * lockScreenPaint.strokeWidth
},
)
}
@@ -489,44 +490,23 @@
}
/** Set the location of the view to match the interpolated text bounds */
- private fun setInterpolatedLocation(measureSize: VPointF): RectF {
- val targetRect = RectF()
- targetRect.apply {
- when (xAlignment) {
- XAlignment.LEFT -> {
- left = layoutBounds.left
- right = layoutBounds.left + measureSize.x
- }
- XAlignment.CENTER -> {
- left = layoutBounds.centerX() - measureSize.x / 2f
- right = layoutBounds.centerX() + measureSize.x / 2f
- }
- XAlignment.RIGHT -> {
- left = layoutBounds.right - measureSize.x
- right = layoutBounds.right
- }
- }
+ private fun setInterpolatedLocation(measureSize: VPointF): VRectF {
+ val pos =
+ VPointF(
+ when (xAlignment) {
+ XAlignment.LEFT -> layoutBounds.left
+ XAlignment.CENTER -> layoutBounds.center.x - measureSize.x / 2f
+ XAlignment.RIGHT -> layoutBounds.right - measureSize.x
+ },
+ when (verticalAlignment) {
+ VerticalAlignment.TOP -> layoutBounds.top
+ VerticalAlignment.CENTER -> layoutBounds.center.y - measureSize.y / 2f
+ VerticalAlignment.BOTTOM -> layoutBounds.bottom - measureSize.y
+ VerticalAlignment.BASELINE -> layoutBounds.center.y - measureSize.y / 2f
+ },
+ )
- when (verticalAlignment) {
- VerticalAlignment.TOP -> {
- top = layoutBounds.top
- bottom = layoutBounds.top + measureSize.y
- }
- VerticalAlignment.CENTER -> {
- top = layoutBounds.centerY() - measureSize.y / 2f
- bottom = layoutBounds.centerY() + measureSize.y / 2f
- }
- VerticalAlignment.BOTTOM -> {
- top = layoutBounds.bottom - measureSize.y
- bottom = layoutBounds.bottom
- }
- VerticalAlignment.BASELINE -> {
- top = layoutBounds.centerY() - measureSize.y / 2f
- bottom = layoutBounds.centerY() + measureSize.y / 2f
- }
- }
- }
-
+ val targetRect = VRectF.fromTopLeft(pos, measureSize)
setFrame(
targetRect.left.roundToInt(),
targetRect.top.roundToInt(),
@@ -537,7 +517,7 @@
return targetRect
}
- private fun getDrawTranslation(interpBounds: RectF): VPointF {
+ private fun getDrawTranslation(interpBounds: VRectF): VPointF {
val sizeDiff = this.measuredSize - interpBounds.size
val alignment =
VPointF(
@@ -586,11 +566,11 @@
if (fontSizePx > 0) {
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx)
lockScreenPaint.textSize = textSize
- lockScreenPaint.getTextBounds(text, textBounds)
- targetTextBounds.set(textBounds)
+ textBounds = lockScreenPaint.getTextBounds(text)
+ targetTextBounds = textBounds
}
if (!constrainedByHeight) {
- val lastUnconstrainedHeight = textBounds.height() + lockScreenPaint.strokeWidth * 2
+ val lastUnconstrainedHeight = textBounds.height + lockScreenPaint.strokeWidth * 2
fontSizeAdjustFactor = lastUnconstrainedHeight / lastUnconstrainedTextSize
}
@@ -608,8 +588,8 @@
for (i in 0..9) {
val rectForCalculate = lockScreenPaint.getTextBounds("$i")
- maxSingleDigitHeight = max(maxSingleDigitHeight, rectForCalculate.height())
- maxSingleDigitWidth = max(maxSingleDigitWidth, rectForCalculate.width())
+ maxSingleDigitHeight = max(maxSingleDigitHeight, rectForCalculate.height)
+ maxSingleDigitWidth = max(maxSingleDigitWidth, rectForCalculate.width)
}
maxSingleDigitWidth += 2 * lockScreenPaint.strokeWidth
maxSingleDigitHeight += 2 * lockScreenPaint.strokeWidth
@@ -637,8 +617,8 @@
* and targetPaint will store the state we transition to
*/
private fun updateTextBoundsForTextAnimator() {
- textAnimator.textInterpolator.basePaint.getTextBounds(text, prevTextBounds)
- textAnimator.textInterpolator.targetPaint.getTextBounds(text, targetTextBounds)
+ prevTextBounds = textAnimator.textInterpolator.basePaint.getTextBounds(text)
+ targetTextBounds = textAnimator.textInterpolator.targetPaint.getTextBounds(text)
}
/**
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt
index 8d3640d..53b364c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt
@@ -41,21 +41,9 @@
@Test
fun testStyleValueUnchange_getBlankStr() {
val fontVariationUtils = FontVariationUtils()
- fontVariationUtils.updateFontVariation(
- weight = 100,
- width = 100,
- opticalSize = 0,
- roundness = 100,
- )
- val updatedFvar1 =
- fontVariationUtils.updateFontVariation(
- weight = 100,
- width = 100,
- opticalSize = 0,
- roundness = 100,
- )
- Assert.assertEquals("", updatedFvar1)
- val updatedFvar2 = fontVariationUtils.updateFontVariation()
- Assert.assertEquals("", updatedFvar2)
+ Assert.assertEquals("", fontVariationUtils.updateFontVariation())
+ val fVar = fontVariationUtils.updateFontVariation(weight = 100)
+ Assert.assertEquals(fVar, fontVariationUtils.updateFontVariation())
+ Assert.assertEquals(fVar, fontVariationUtils.updateFontVariation(weight = 100))
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt
index 0f40089..56b06de 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt
@@ -17,14 +17,12 @@
package com.android.systemui.common.ui.view
+import android.testing.TestableLooper
+import android.view.MotionEvent
import android.view.ViewConfiguration
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel
-import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel.Down
-import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel.Move
-import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel.Up
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
@@ -33,18 +31,22 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.never
+import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
class TouchHandlingViewInteractionHandlerTest : SysuiTestCase() {
@Mock private lateinit var postDelayed: (Runnable, Long) -> DisposableHandle
@Mock private lateinit var onLongPressDetected: (Int, Int) -> Unit
@Mock private lateinit var onSingleTapDetected: (Int, Int) -> Unit
+ @Mock private lateinit var onDoubleTapDetected: () -> Unit
private lateinit var underTest: TouchHandlingViewInteractionHandler
@@ -61,14 +63,17 @@
underTest =
TouchHandlingViewInteractionHandler(
+ context = context,
postDelayed = postDelayed,
isAttachedToWindow = { isAttachedToWindow },
onLongPressDetected = onLongPressDetected,
onSingleTapDetected = onSingleTapDetected,
+ onDoubleTapDetected = onDoubleTapDetected,
longPressDuration = { ViewConfiguration.getLongPressTimeout().toLong() },
allowedTouchSlop = ViewConfiguration.getTouchSlop(),
)
underTest.isLongPressHandlingEnabled = true
+ underTest.isDoubleTapHandlingEnabled = true
}
@Test
@@ -76,63 +81,250 @@
val downX = 123
val downY = 456
dispatchTouchEvents(
- Down(x = downX, y = downY),
- Move(distanceMoved = ViewConfiguration.getTouchSlop() - 0.1f),
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(
+ 0L,
+ 0L,
+ MotionEvent.ACTION_MOVE,
+ 123f + ViewConfiguration.getTouchSlop() - 0.1f,
+ 456f,
+ 0,
+ ),
)
delayedRunnable?.run()
verify(onLongPressDetected).invoke(downX, downY)
- verify(onSingleTapDetected, never()).invoke(any(), any())
+ verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt())
}
@Test
fun longPressButFeatureNotEnabled() = runTest {
underTest.isLongPressHandlingEnabled = false
- dispatchTouchEvents(Down(x = 123, y = 456))
+ dispatchTouchEvents(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0))
assertThat(delayedRunnable).isNull()
- verify(onLongPressDetected, never()).invoke(any(), any())
- verify(onSingleTapDetected, never()).invoke(any(), any())
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt())
}
@Test
fun longPressButViewNotAttached() = runTest {
isAttachedToWindow = false
- dispatchTouchEvents(Down(x = 123, y = 456))
+ dispatchTouchEvents(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0))
delayedRunnable?.run()
- verify(onLongPressDetected, never()).invoke(any(), any())
- verify(onSingleTapDetected, never()).invoke(any(), any())
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt())
}
@Test
fun draggedTooFarToBeConsideredAlongPress() = runTest {
dispatchTouchEvents(
- Down(x = 123, y = 456),
- Move(distanceMoved = ViewConfiguration.getTouchSlop() + 0.1f),
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123F, 456F, 0),
+ // Drag action within touch slop
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 123f, 456f, 0).apply {
+ addBatch(0L, 123f + ViewConfiguration.getTouchSlop() + 0.1f, 456f, 0f, 0f, 0)
+ },
)
assertThat(delayedRunnable).isNull()
- verify(onLongPressDetected, never()).invoke(any(), any())
- verify(onSingleTapDetected, never()).invoke(any(), any())
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt())
}
@Test
fun heldDownTooBrieflyToBeConsideredAlongPress() = runTest {
dispatchTouchEvents(
- Down(x = 123, y = 456),
- Up(
- distanceMoved = ViewConfiguration.getTouchSlop().toFloat(),
- gestureDuration = ViewConfiguration.getLongPressTimeout() - 1L,
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(
+ 0L,
+ ViewConfiguration.getLongPressTimeout() - 1L,
+ MotionEvent.ACTION_UP,
+ 123f,
+ 456F,
+ 0,
),
)
assertThat(delayedRunnable).isNull()
- verify(onLongPressDetected, never()).invoke(any(), any())
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
verify(onSingleTapDetected).invoke(123, 456)
}
- private fun dispatchTouchEvents(vararg models: MotionEventModel) {
- models.forEach { model -> underTest.onTouchEvent(model) }
+ @Test
+ fun doubleTap() = runTest {
+ val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+ dispatchTouchEvents(
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ MotionEvent.obtain(
+ secondTapTime,
+ secondTapTime,
+ MotionEvent.ACTION_DOWN,
+ 123f,
+ 456f,
+ 0,
+ ),
+ MotionEvent.obtain(secondTapTime, secondTapTime, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ )
+
+ verify(onDoubleTapDetected).invoke()
+ assertThat(delayedRunnable).isNull()
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt())
+ }
+
+ @Test
+ fun doubleTapButFeatureNotEnabled() = runTest {
+ underTest.isDoubleTapHandlingEnabled = false
+
+ val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+ dispatchTouchEvents(
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ MotionEvent.obtain(
+ secondTapTime,
+ secondTapTime,
+ MotionEvent.ACTION_DOWN,
+ 123f,
+ 456f,
+ 0,
+ ),
+ MotionEvent.obtain(secondTapTime, secondTapTime, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ )
+
+ verify(onDoubleTapDetected, never()).invoke()
+ assertThat(delayedRunnable).isNull()
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt())
+ }
+
+ @Test
+ fun tapIntoLongPress() = runTest {
+ val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+ dispatchTouchEvents(
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ MotionEvent.obtain(
+ secondTapTime,
+ secondTapTime,
+ MotionEvent.ACTION_DOWN,
+ 123f,
+ 456f,
+ 0,
+ ),
+ MotionEvent.obtain(
+ secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L,
+ secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L,
+ MotionEvent.ACTION_MOVE,
+ 123f + ViewConfiguration.getTouchSlop() - 0.1f,
+ 456f,
+ 0,
+ ),
+ )
+ delayedRunnable?.run()
+
+ verify(onDoubleTapDetected, never()).invoke()
+ verify(onSingleTapDetected).invoke(anyInt(), anyInt())
+ verify(onLongPressDetected).invoke(anyInt(), anyInt())
+ }
+
+ @Test
+ fun tapIntoDownHoldTooBrieflyToBeConsideredLongPress() = runTest {
+ val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+ dispatchTouchEvents(
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ MotionEvent.obtain(
+ secondTapTime,
+ secondTapTime,
+ MotionEvent.ACTION_DOWN,
+ 123f,
+ 456f,
+ 0,
+ ),
+ MotionEvent.obtain(
+ secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L,
+ secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L,
+ MotionEvent.ACTION_UP,
+ 123f,
+ 456f,
+ 0,
+ ),
+ )
+ delayedRunnable?.run()
+
+ verify(onDoubleTapDetected, never()).invoke()
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt())
+ }
+
+ @Test
+ fun tapIntoDrag() = runTest {
+ val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+ dispatchTouchEvents(
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ MotionEvent.obtain(
+ secondTapTime,
+ secondTapTime,
+ MotionEvent.ACTION_DOWN,
+ 123f,
+ 456f,
+ 0,
+ ),
+ // Drag event within touch slop
+ MotionEvent.obtain(secondTapTime, secondTapTime, MotionEvent.ACTION_MOVE, 123f, 456f, 0)
+ .apply {
+ addBatch(
+ secondTapTime,
+ 123f + ViewConfiguration.getTouchSlop() + 0.1f,
+ 456f,
+ 0f,
+ 0f,
+ 0,
+ )
+ },
+ )
+ delayedRunnable?.run()
+
+ verify(onDoubleTapDetected, never()).invoke()
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected).invoke(anyInt(), anyInt())
+ }
+
+ @Test
+ fun doubleTapOutOfAllowableSlop() = runTest {
+ val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+ val scaledDoubleTapSlop = ViewConfiguration.get(context).scaledDoubleTapSlop
+ dispatchTouchEvents(
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ MotionEvent.obtain(
+ secondTapTime,
+ secondTapTime,
+ MotionEvent.ACTION_DOWN,
+ 123f + scaledDoubleTapSlop + 0.1f,
+ 456f + scaledDoubleTapSlop + 0.1f,
+ 0,
+ ),
+ MotionEvent.obtain(
+ secondTapTime,
+ secondTapTime,
+ MotionEvent.ACTION_UP,
+ 123f + scaledDoubleTapSlop + 0.1f,
+ 456f + scaledDoubleTapSlop + 0.1f,
+ 0,
+ ),
+ )
+
+ verify(onDoubleTapDetected, never()).invoke()
+ assertThat(delayedRunnable).isNull()
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt())
+ }
+
+ private fun dispatchTouchEvents(vararg events: MotionEvent) {
+ events.forEach { event -> underTest.onTouchEvent(event) }
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt
index ed73d89..6a25069 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt
@@ -73,12 +73,12 @@
assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
- kosmos.setCommunalEnabled(true)
+ setCommunalEnabled(true)
assertThat(fakeCommunalMediaRepository.isListening()).isTrue()
assertThat(fakeCommunalSmartspaceRepository.isListening()).isTrue()
- kosmos.setCommunalEnabled(false)
+ setCommunalEnabled(false)
assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
@@ -93,13 +93,13 @@
assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
- kosmos.setCommunalEnabled(true)
+ setCommunalEnabled(true)
// Media listening does not start when UMO is disabled.
assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
assertThat(fakeCommunalSmartspaceRepository.isListening()).isTrue()
- kosmos.setCommunalEnabled(false)
+ setCommunalEnabled(false)
assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryImplTest.kt
new file mode 100644
index 0000000..f9b29e9
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryImplTest.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2025 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.communal.data.repository
+
+import android.app.UiModeManager
+import android.app.UiModeManager.OnProjectionStateChangedListener
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.backgroundScope
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CarProjectionRepositoryImplTest : SysuiTestCase() {
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+
+ private val capturedListeners = mutableListOf<OnProjectionStateChangedListener>()
+
+ private val Kosmos.uiModeManager by
+ Kosmos.Fixture<UiModeManager> {
+ mock {
+ on {
+ addOnProjectionStateChangedListener(
+ eq(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE),
+ any(),
+ any(),
+ )
+ } doAnswer
+ {
+ val listener = it.getArgument<OnProjectionStateChangedListener>(2)
+ capturedListeners.add(listener)
+ Unit
+ }
+
+ on { removeOnProjectionStateChangedListener(any()) } doAnswer
+ {
+ val listener = it.getArgument<OnProjectionStateChangedListener>(0)
+ capturedListeners.remove(listener)
+ Unit
+ }
+
+ on { activeProjectionTypes } doReturn UiModeManager.PROJECTION_TYPE_NONE
+ }
+ }
+
+ private val Kosmos.underTest by
+ Kosmos.Fixture {
+ CarProjectionRepositoryImpl(
+ uiModeManager = uiModeManager,
+ bgDispatcher = testDispatcher,
+ )
+ }
+
+ @Test
+ fun testProjectionActiveUpdatesAfterCallback() =
+ kosmos.runTest {
+ val projectionActive by collectLastValue(underTest.projectionActive)
+ assertThat(projectionActive).isFalse()
+
+ setActiveProjectionType(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE)
+ assertThat(projectionActive).isTrue()
+
+ setActiveProjectionType(UiModeManager.PROJECTION_TYPE_NONE)
+ assertThat(projectionActive).isFalse()
+ }
+
+ @Test
+ fun testProjectionInitialValueTrue() =
+ kosmos.runTest {
+ setActiveProjectionType(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE)
+
+ val projectionActive by collectLastValue(underTest.projectionActive)
+ assertThat(projectionActive).isTrue()
+ }
+
+ @Test
+ fun testUnsubscribeWhenCancelled() =
+ kosmos.runTest {
+ val job = underTest.projectionActive.launchIn(backgroundScope)
+ assertThat(capturedListeners).hasSize(1)
+
+ job.cancel()
+ assertThat(capturedListeners).isEmpty()
+ }
+
+ private fun Kosmos.setActiveProjectionType(@UiModeManager.ProjectionType projectionType: Int) {
+ uiModeManager.stub { on { activeProjectionTypes } doReturn projectionType }
+ capturedListeners.forEach { it.onProjectionStateChanged(projectionType, emptySet()) }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt
index 5c98365..09d44a5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt
@@ -34,9 +34,7 @@
import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.broadcastDispatcher
-import com.android.systemui.communal.data.model.DisabledReason
import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryImpl.Companion.GLANCEABLE_HUB_BACKGROUND_SETTING
-import com.android.systemui.communal.domain.interactor.setCommunalV2Enabled
import com.android.systemui.communal.shared.model.CommunalBackgroundType
import com.android.systemui.communal.shared.model.WhenToDream
import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
@@ -202,63 +200,6 @@
@EnableFlags(FLAG_COMMUNAL_HUB)
@Test
- fun secondaryUserIsInvalid() =
- kosmos.runTest {
- val enabledState by collectLastValue(underTest.getEnabledState(SECONDARY_USER))
-
- assertThat(enabledState?.enabled).isFalse()
- assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_INVALID_USER)
- }
-
- @EnableFlags(FLAG_COMMUNAL_HUB, FLAG_GLANCEABLE_HUB_V2)
- @Test
- fun classicFlagIsDisabled() =
- kosmos.runTest {
- setCommunalV2Enabled(false)
- val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER))
- assertThat(enabledState?.enabled).isFalse()
- assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_FLAG)
- }
-
- @DisableFlags(FLAG_COMMUNAL_HUB, FLAG_GLANCEABLE_HUB_V2)
- @Test
- fun communalHubFlagIsDisabled() =
- kosmos.runTest {
- val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER))
- assertThat(enabledState?.enabled).isFalse()
- assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_FLAG)
- }
-
- @EnableFlags(FLAG_COMMUNAL_HUB)
- @Test
- fun hubIsDisabledByUser() =
- kosmos.runTest {
- fakeSettings.putIntForUser(Settings.Secure.GLANCEABLE_HUB_ENABLED, 0, PRIMARY_USER.id)
- val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER))
- assertThat(enabledState?.enabled).isFalse()
- assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_USER_SETTING)
-
- fakeSettings.putIntForUser(Settings.Secure.GLANCEABLE_HUB_ENABLED, 1, SECONDARY_USER.id)
- assertThat(enabledState?.enabled).isFalse()
-
- fakeSettings.putIntForUser(Settings.Secure.GLANCEABLE_HUB_ENABLED, 1, PRIMARY_USER.id)
- assertThat(enabledState?.enabled).isTrue()
- }
-
- @EnableFlags(FLAG_COMMUNAL_HUB)
- @Test
- fun hubIsDisabledByDevicePolicy() =
- kosmos.runTest {
- val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER))
- assertThat(enabledState?.enabled).isTrue()
-
- setKeyguardFeaturesDisabled(PRIMARY_USER, KEYGUARD_DISABLE_WIDGETS_ALL)
- assertThat(enabledState?.enabled).isFalse()
- assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_DEVICE_POLICY)
- }
-
- @EnableFlags(FLAG_COMMUNAL_HUB)
- @Test
fun widgetsAllowedForWorkProfile_isFalse_whenDisallowedByDevicePolicy() =
kosmos.runTest {
val widgetsAllowedForWorkProfile by
@@ -269,36 +210,6 @@
assertThat(widgetsAllowedForWorkProfile).isFalse()
}
- @EnableFlags(FLAG_COMMUNAL_HUB)
- @Test
- fun hubIsEnabled_whenDisallowedByDevicePolicyForWorkProfile() =
- kosmos.runTest {
- val enabledStateForPrimaryUser by
- collectLastValue(underTest.getEnabledState(PRIMARY_USER))
- assertThat(enabledStateForPrimaryUser?.enabled).isTrue()
-
- setKeyguardFeaturesDisabled(WORK_PROFILE, KEYGUARD_DISABLE_WIDGETS_ALL)
- assertThat(enabledStateForPrimaryUser?.enabled).isTrue()
- }
-
- @EnableFlags(FLAG_COMMUNAL_HUB)
- @Test
- fun hubIsDisabledByUserAndDevicePolicy() =
- kosmos.runTest {
- val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER))
- assertThat(enabledState?.enabled).isTrue()
-
- fakeSettings.putIntForUser(Settings.Secure.GLANCEABLE_HUB_ENABLED, 0, PRIMARY_USER.id)
- setKeyguardFeaturesDisabled(PRIMARY_USER, KEYGUARD_DISABLE_WIDGETS_ALL)
-
- assertThat(enabledState?.enabled).isFalse()
- assertThat(enabledState)
- .containsExactly(
- DisabledReason.DISABLED_REASON_DEVICE_POLICY,
- DisabledReason.DISABLED_REASON_USER_SETTING,
- )
- }
-
@Test
@DisableFlags(FLAG_GLANCEABLE_HUB_BLURRED_BACKGROUND)
fun backgroundType_defaultValue() =
@@ -327,26 +238,6 @@
}
@Test
- fun screensaverDisabledByUser() =
- kosmos.runTest {
- val enabledState by collectLastValue(underTest.getScreensaverEnabledState(PRIMARY_USER))
-
- fakeSettings.putIntForUser(Settings.Secure.SCREENSAVER_ENABLED, 0, PRIMARY_USER.id)
-
- assertThat(enabledState).isFalse()
- }
-
- @Test
- fun screensaverEnabledByUser() =
- kosmos.runTest {
- val enabledState by collectLastValue(underTest.getScreensaverEnabledState(PRIMARY_USER))
-
- fakeSettings.putIntForUser(Settings.Secure.SCREENSAVER_ENABLED, 1, PRIMARY_USER.id)
-
- assertThat(enabledState).isTrue()
- }
-
- @Test
fun whenToDream_charging() =
kosmos.runTest {
val whenToDreamState by collectLastValue(underTest.getWhenToDreamState(PRIMARY_USER))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorTest.kt
new file mode 100644
index 0000000..fc4cd43
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorTest.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2025 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.communal.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.repository.carProjectionRepository
+import com.android.systemui.communal.data.repository.fake
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CarProjectionInteractorTest : SysuiTestCase() {
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+
+ private val Kosmos.underTest by Kosmos.Fixture { carProjectionInteractor }
+
+ @Test
+ fun testProjectionActive() =
+ kosmos.runTest {
+ val projectionActive by collectLastValue(underTest.projectionActive)
+ assertThat(projectionActive).isFalse()
+
+ carProjectionRepository.fake.setProjectionActive(true)
+ assertThat(projectionActive).isTrue()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt
new file mode 100644
index 0000000..f4a1c90
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2025 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.communal.domain.interactor
+
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.data.repository.batteryRepository
+import com.android.systemui.common.data.repository.fake
+import com.android.systemui.communal.data.model.FEATURE_AUTO_OPEN
+import com.android.systemui.communal.data.model.FEATURE_MANUAL_OPEN
+import com.android.systemui.communal.data.model.SuppressionReason
+import com.android.systemui.communal.posturing.data.repository.fake
+import com.android.systemui.communal.posturing.data.repository.posturingRepository
+import com.android.systemui.communal.posturing.shared.model.PosturedState
+import com.android.systemui.dock.DockManager
+import com.android.systemui.dock.fakeDockManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.FakeUserRepository.Companion.MAIN_USER_ID
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.util.settings.fakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CommunalAutoOpenInteractorTest : SysuiTestCase() {
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+
+ private val Kosmos.underTest by Kosmos.Fixture { communalAutoOpenInteractor }
+
+ @Before
+ fun setUp() {
+ runBlocking { kosmos.fakeUserRepository.asMainUser() }
+ with(kosmos.fakeSettings) {
+ putBoolForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, false, MAIN_USER_ID)
+ putBoolForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, false, MAIN_USER_ID)
+ putBoolForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, false, MAIN_USER_ID)
+ }
+ }
+
+ @Test
+ fun testStartWhileCharging() =
+ kosmos.runTest {
+ val shouldAutoOpen by collectLastValue(underTest.shouldAutoOpen)
+ val suppressionReason by collectLastValue(underTest.suppressionReason)
+
+ fakeSettings.putBoolForUser(
+ Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP,
+ true,
+ MAIN_USER_ID,
+ )
+
+ batteryRepository.fake.setDevicePluggedIn(false)
+ assertThat(shouldAutoOpen).isFalse()
+ assertThat(suppressionReason)
+ .isEqualTo(
+ SuppressionReason.ReasonWhenToAutoShow(FEATURE_AUTO_OPEN or FEATURE_MANUAL_OPEN)
+ )
+
+ batteryRepository.fake.setDevicePluggedIn(true)
+ assertThat(shouldAutoOpen).isTrue()
+ assertThat(suppressionReason).isNull()
+ }
+
+ @Test
+ fun testStartWhileDocked() =
+ kosmos.runTest {
+ val shouldAutoOpen by collectLastValue(underTest.shouldAutoOpen)
+ val suppressionReason by collectLastValue(underTest.suppressionReason)
+
+ fakeSettings.putBoolForUser(
+ Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK,
+ true,
+ MAIN_USER_ID,
+ )
+
+ batteryRepository.fake.setDevicePluggedIn(true)
+ fakeDockManager.setIsDocked(false)
+
+ assertThat(shouldAutoOpen).isFalse()
+ assertThat(suppressionReason)
+ .isEqualTo(
+ SuppressionReason.ReasonWhenToAutoShow(FEATURE_AUTO_OPEN or FEATURE_MANUAL_OPEN)
+ )
+
+ fakeDockManager.setIsDocked(true)
+ fakeDockManager.setDockEvent(DockManager.STATE_DOCKED)
+ assertThat(shouldAutoOpen).isTrue()
+ assertThat(suppressionReason).isNull()
+ }
+
+ @Test
+ fun testStartWhilePostured() =
+ kosmos.runTest {
+ val shouldAutoOpen by collectLastValue(underTest.shouldAutoOpen)
+ val suppressionReason by collectLastValue(underTest.suppressionReason)
+
+ fakeSettings.putBoolForUser(
+ Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED,
+ true,
+ MAIN_USER_ID,
+ )
+
+ batteryRepository.fake.setDevicePluggedIn(true)
+ posturingRepository.fake.setPosturedState(PosturedState.NotPostured)
+
+ assertThat(shouldAutoOpen).isFalse()
+ assertThat(suppressionReason)
+ .isEqualTo(
+ SuppressionReason.ReasonWhenToAutoShow(FEATURE_AUTO_OPEN or FEATURE_MANUAL_OPEN)
+ )
+
+ posturingRepository.fake.setPosturedState(PosturedState.Postured(1f))
+ assertThat(shouldAutoOpen).isTrue()
+ assertThat(suppressionReason).isNull()
+ }
+
+ @Test
+ fun testStartNever() =
+ kosmos.runTest {
+ val shouldAutoOpen by collectLastValue(underTest.shouldAutoOpen)
+ val suppressionReason by collectLastValue(underTest.suppressionReason)
+
+ fakeSettings.putBoolForUser(
+ Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP,
+ false,
+ MAIN_USER_ID,
+ )
+ fakeSettings.putBoolForUser(
+ Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK,
+ false,
+ MAIN_USER_ID,
+ )
+ fakeSettings.putBoolForUser(
+ Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED,
+ false,
+ MAIN_USER_ID,
+ )
+
+ batteryRepository.fake.setDevicePluggedIn(true)
+ posturingRepository.fake.setPosturedState(PosturedState.Postured(1f))
+ fakeDockManager.setIsDocked(true)
+
+ assertThat(shouldAutoOpen).isFalse()
+ assertThat(suppressionReason)
+ .isEqualTo(
+ SuppressionReason.ReasonWhenToAutoShow(FEATURE_AUTO_OPEN or FEATURE_MANUAL_OPEN)
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt
deleted file mode 100644
index beec184..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.communal.domain.interactor
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository
-import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository
-import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
-import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.testKosmos
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-/**
- * This class is a variation of the [CommunalInteractorTest] for cases where communal is disabled.
- */
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class CommunalInteractorCommunalDisabledTest : SysuiTestCase() {
- private val kosmos = testKosmos()
- private val testScope = kosmos.testScope
-
- private lateinit var communalRepository: FakeCommunalSceneRepository
- private lateinit var widgetRepository: FakeCommunalWidgetRepository
- private lateinit var keyguardRepository: FakeKeyguardRepository
-
- private lateinit var underTest: CommunalInteractor
-
- @Before
- fun setUp() {
- communalRepository = kosmos.fakeCommunalSceneRepository
- widgetRepository = kosmos.fakeCommunalWidgetRepository
- keyguardRepository = kosmos.fakeKeyguardRepository
-
- mSetFlagsRule.disableFlags(FLAG_COMMUNAL_HUB)
-
- underTest = kosmos.communalInteractor
- }
-
- @Test
- fun isCommunalEnabled_false() =
- testScope.runTest { assertThat(underTest.isCommunalEnabled.value).isFalse() }
-
- @Test
- fun isCommunalAvailable_whenStorageUnlock_false() =
- testScope.runTest {
- val isCommunalAvailable by collectLastValue(underTest.isCommunalAvailable)
-
- assertThat(isCommunalAvailable).isFalse()
-
- keyguardRepository.setIsEncryptedOrLockdown(false)
- runCurrent()
-
- assertThat(isCommunalAvailable).isFalse()
- }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index 8424746..b65ecf46 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -21,7 +21,6 @@
import android.app.admin.devicePolicyManager
import android.content.Intent
import android.content.pm.UserInfo
-import android.content.res.mainResources
import android.os.UserHandle
import android.os.UserManager
import android.os.userManager
@@ -39,9 +38,8 @@
import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.broadcastDispatcher
-import com.android.systemui.common.data.repository.batteryRepository
-import com.android.systemui.common.data.repository.fake
import com.android.systemui.communal.data.model.CommunalSmartspaceTimer
+import com.android.systemui.communal.data.model.SuppressionReason
import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository
import com.android.systemui.communal.data.repository.fakeCommunalPrefsRepository
import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
@@ -50,14 +48,9 @@
import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.domain.model.CommunalTransitionProgressModel
-import com.android.systemui.communal.posturing.data.repository.fake
-import com.android.systemui.communal.posturing.data.repository.posturingRepository
-import com.android.systemui.communal.posturing.shared.model.PosturedState
import com.android.systemui.communal.shared.model.CommunalContentSize
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.communal.shared.model.EditModeState
-import com.android.systemui.dock.DockManager
-import com.android.systemui.dock.fakeDockManager
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -75,19 +68,16 @@
import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.statusbar.phone.fakeManagedProfileController
import com.android.systemui.testKosmos
-import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.user.data.repository.fakeUserRepository
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.nullable
import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.advanceTimeBy
-import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -98,10 +88,6 @@
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters
-/**
- * This class of test cases assume that communal is enabled. For disabled cases, see
- * [CommunalInteractorCommunalDisabledTest].
- */
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
@@ -109,10 +95,7 @@
UserInfo(/* id= */ 0, /* name= */ "primary user", /* flags= */ UserInfo.FLAG_MAIN)
private val secondaryUser = UserInfo(/* id= */ 1, /* name= */ "secondary user", /* flags= */ 0)
- private val kosmos =
- testKosmos()
- .apply { mainResources = mContext.orCreateTestableResources.resources }
- .useUnconfinedTestDispatcher()
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
private val Kosmos.underTest by Kosmos.Fixture { communalInteractor }
@@ -128,104 +111,40 @@
kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)
-
- mContext.orCreateTestableResources.addOverride(
- com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault,
- false,
- )
- mContext.orCreateTestableResources.addOverride(
- com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault,
- false,
- )
- mContext.orCreateTestableResources.addOverride(
- com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault,
- false,
- )
- }
-
- @After
- fun tearDown() {
- mContext.orCreateTestableResources.removeOverride(
- com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault
- )
- mContext.orCreateTestableResources.removeOverride(
- com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault
- )
- mContext.orCreateTestableResources.removeOverride(
- com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault
- )
}
@Test
fun communalEnabled_true() =
kosmos.runTest {
- fakeUserRepository.setSelectedUserInfo(mainUser)
+ communalSettingsInteractor.setSuppressionReasons(emptyList())
assertThat(underTest.isCommunalEnabled.value).isTrue()
}
@Test
- fun isCommunalAvailable_mainUserUnlockedAndMainUser_true() =
- kosmos.runTest {
- val isAvailable by collectLastValue(underTest.isCommunalAvailable)
- assertThat(isAvailable).isFalse()
-
- fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
- fakeUserRepository.setSelectedUserInfo(mainUser)
- fakeKeyguardRepository.setKeyguardShowing(true)
-
- assertThat(isAvailable).isTrue()
- }
-
- @Test
- fun isCommunalAvailable_mainUserLockedAndMainUser_false() =
- kosmos.runTest {
- val isAvailable by collectLastValue(underTest.isCommunalAvailable)
- assertThat(isAvailable).isFalse()
-
- fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, false)
- fakeUserRepository.setSelectedUserInfo(mainUser)
- fakeKeyguardRepository.setKeyguardShowing(true)
-
- assertThat(isAvailable).isFalse()
- }
-
- @Test
- fun isCommunalAvailable_mainUserUnlockedAndSecondaryUser_false() =
- kosmos.runTest {
- val isAvailable by collectLastValue(underTest.isCommunalAvailable)
- assertThat(isAvailable).isFalse()
-
- fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
- fakeUserRepository.setSelectedUserInfo(secondaryUser)
- fakeKeyguardRepository.setKeyguardShowing(true)
-
- assertThat(isAvailable).isFalse()
- }
-
- @Test
fun isCommunalAvailable_whenKeyguardShowing_true() =
kosmos.runTest {
+ communalSettingsInteractor.setSuppressionReasons(emptyList())
+ fakeKeyguardRepository.setKeyguardShowing(false)
+
val isAvailable by collectLastValue(underTest.isCommunalAvailable)
assertThat(isAvailable).isFalse()
- fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
- fakeUserRepository.setSelectedUserInfo(mainUser)
fakeKeyguardRepository.setKeyguardShowing(true)
-
assertThat(isAvailable).isTrue()
}
@Test
- fun isCommunalAvailable_communalDisabled_false() =
+ fun isCommunalAvailable_suppressed() =
kosmos.runTest {
- mSetFlagsRule.disableFlags(FLAG_COMMUNAL_HUB, FLAG_GLANCEABLE_HUB_V2)
+ communalSettingsInteractor.setSuppressionReasons(emptyList())
+ fakeKeyguardRepository.setKeyguardShowing(true)
val isAvailable by collectLastValue(underTest.isCommunalAvailable)
- assertThat(isAvailable).isFalse()
+ assertThat(isAvailable).isTrue()
- fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, false)
- fakeUserRepository.setSelectedUserInfo(mainUser)
- fakeKeyguardRepository.setKeyguardShowing(true)
+ communalSettingsInteractor.setSuppressionReasons(
+ listOf(SuppressionReason.ReasonUnknown())
+ )
assertThat(isAvailable).isFalse()
}
@@ -1280,66 +1199,6 @@
.inOrder()
}
- @Test
- fun showCommunalWhileCharging() =
- kosmos.runTest {
- fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
- fakeUserRepository.setSelectedUserInfo(mainUser)
- fakeKeyguardRepository.setKeyguardShowing(true)
- fakeSettings.putIntForUser(
- Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP,
- 1,
- mainUser.id,
- )
-
- val shouldShowCommunal by collectLastValue(underTest.shouldShowCommunal)
- batteryRepository.fake.setDevicePluggedIn(false)
- assertThat(shouldShowCommunal).isFalse()
-
- batteryRepository.fake.setDevicePluggedIn(true)
- assertThat(shouldShowCommunal).isTrue()
- }
-
- @Test
- fun showCommunalWhilePosturedAndCharging() =
- kosmos.runTest {
- fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
- fakeUserRepository.setSelectedUserInfo(mainUser)
- fakeKeyguardRepository.setKeyguardShowing(true)
- fakeSettings.putIntForUser(
- Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED,
- 1,
- mainUser.id,
- )
-
- val shouldShowCommunal by collectLastValue(underTest.shouldShowCommunal)
- batteryRepository.fake.setDevicePluggedIn(true)
- posturingRepository.fake.setPosturedState(PosturedState.NotPostured)
- assertThat(shouldShowCommunal).isFalse()
-
- posturingRepository.fake.setPosturedState(PosturedState.Postured(1f))
- assertThat(shouldShowCommunal).isTrue()
- }
-
- @Test
- fun showCommunalWhileDocked() =
- kosmos.runTest {
- fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
- fakeUserRepository.setSelectedUserInfo(mainUser)
- fakeKeyguardRepository.setKeyguardShowing(true)
- fakeSettings.putIntForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, 1, mainUser.id)
-
- batteryRepository.fake.setDevicePluggedIn(true)
- fakeDockManager.setIsDocked(false)
-
- val shouldShowCommunal by collectLastValue(underTest.shouldShowCommunal)
- assertThat(shouldShowCommunal).isFalse()
-
- fakeDockManager.setIsDocked(true)
- fakeDockManager.setDockEvent(DockManager.STATE_DOCKED)
- assertThat(shouldShowCommunal).isTrue()
- }
-
private fun setKeyguardFeaturesDisabled(user: UserInfo, disabledFlags: Int) {
whenever(kosmos.devicePolicyManager.getKeyguardDisabledFeatures(nullable(), eq(user.id)))
.thenReturn(disabledFlags)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index 8dc7a33..b8dbc9f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -32,9 +32,9 @@
import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
import com.android.systemui.SysuiTestCase
import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
-import com.android.systemui.common.data.repository.batteryRepository
-import com.android.systemui.common.data.repository.fake
import com.android.systemui.communal.data.model.CommunalSmartspaceTimer
+import com.android.systemui.communal.data.model.FEATURE_MANUAL_OPEN
+import com.android.systemui.communal.data.model.SuppressionReason
import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository
import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository
import com.android.systemui.communal.data.repository.FakeCommunalSmartspaceRepository
@@ -50,6 +50,7 @@
import com.android.systemui.communal.domain.interactor.communalSceneInteractor
import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.communal.domain.interactor.communalTutorialInteractor
+import com.android.systemui.communal.domain.interactor.setCommunalEnabled
import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.shared.log.CommunalMetricsLogger
@@ -101,7 +102,6 @@
import com.android.systemui.testKosmos
import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.user.data.repository.fakeUserRepository
-import com.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.advanceTimeBy
@@ -128,7 +128,9 @@
@RunWith(ParameterizedAndroidJunit4::class)
class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
@Mock private lateinit var mediaHost: MediaHost
+
@Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler
+
@Mock private lateinit var metricsLogger: CommunalMetricsLogger
private val kosmos = testKosmos()
@@ -212,11 +214,8 @@
@Test
fun tutorial_tutorialNotCompletedAndKeyguardVisible_showTutorialContent() =
testScope.runTest {
- // Keyguard showing, storage unlocked, main user, and tutorial not started.
keyguardRepository.setKeyguardShowing(true)
- keyguardRepository.setKeyguardOccluded(false)
- userRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
- setIsMainUser(true)
+ kosmos.setCommunalEnabled(true)
tutorialRepository.setTutorialSettingState(
Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED
)
@@ -951,21 +950,16 @@
fun swipeToCommunal() =
kosmos.runTest {
setCommunalV2ConfigEnabled(true)
- val mainUser = fakeUserRepository.asMainUser()
- fakeKeyguardRepository.setKeyguardShowing(true)
- fakeUserRepository.setUserUnlocked(mainUser.id, true)
- fakeUserTracker.set(userInfos = listOf(mainUser), selectedUserIndex = 0)
- fakeSettings.putIntForUser(
- Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP,
- 1,
- mainUser.id,
+ // Suppress manual opening
+ communalSettingsInteractor.setSuppressionReasons(
+ listOf(SuppressionReason.ReasonUnknown(FEATURE_MANUAL_OPEN))
)
val viewModel = createViewModel()
val swipeToHubEnabled by collectLastValue(viewModel.swipeToHubEnabled)
assertThat(swipeToHubEnabled).isFalse()
- batteryRepository.fake.setDevicePluggedIn(true)
+ communalSettingsInteractor.setSuppressionReasons(emptyList())
assertThat(swipeToHubEnabled).isTrue()
keyguardTransitionRepository.sendTransitionStep(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
index c15f797..df10d05 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
@@ -17,7 +17,6 @@
package com.android.systemui.communal.widgets
import android.content.pm.UserInfo
-import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
@@ -25,6 +24,7 @@
import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
+import com.android.systemui.communal.domain.interactor.setCommunalEnabled
import com.android.systemui.communal.shared.model.FakeGlanceableHubMultiUserHelper
import com.android.systemui.communal.shared.model.fakeGlanceableHubMultiUserHelper
import com.android.systemui.coroutines.collectLastValue
@@ -37,11 +37,9 @@
import com.android.systemui.kosmos.testScope
import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.testKosmos
-import com.android.systemui.user.data.repository.FakeUserRepository.Companion.MAIN_USER_ID
import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.user.domain.interactor.userLockedInteractor
import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.test.runCurrent
@@ -282,22 +280,12 @@
}
}
- private suspend fun setCommunalAvailable(
- available: Boolean,
- setKeyguardShowing: Boolean = true,
- ) =
+ private fun setCommunalAvailable(available: Boolean, setKeyguardShowing: Boolean = true) =
with(kosmos) {
- fakeUserRepository.setUserUnlocked(MAIN_USER_ID, true)
- fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO)
+ setCommunalEnabled(available)
if (setKeyguardShowing) {
fakeKeyguardRepository.setKeyguardShowing(true)
}
- val settingsValue = if (available) 1 else 0
- fakeSettings.putIntForUser(
- Settings.Secure.GLANCEABLE_HUB_ENABLED,
- settingsValue,
- MAIN_USER_INFO.id,
- )
}
private companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
index 6c4325a..046d92d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
@@ -35,7 +35,6 @@
import android.os.PowerManager
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
-import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Flags
@@ -43,8 +42,6 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
-import com.android.systemui.common.data.repository.batteryRepository
-import com.android.systemui.common.data.repository.fake
import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
import com.android.systemui.communal.domain.interactor.communalSceneInteractor
import com.android.systemui.communal.domain.interactor.setCommunalV2Available
@@ -72,7 +69,6 @@
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
import com.android.systemui.testKosmos
-import com.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth
import junit.framework.Assert.assertEquals
import kotlinx.coroutines.runBlocking
@@ -433,9 +429,7 @@
@EnableFlags(FLAG_GLANCEABLE_HUB_V2)
fun testTransitionToGlanceableHub_onWakeUpFromAod() =
kosmos.runTest {
- val user = setCommunalV2Available(true)
- fakeSettings.putIntForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, 1, user.id)
- batteryRepository.fake.setDevicePluggedIn(true)
+ setCommunalV2Available(true)
val currentScene by collectLastValue(communalSceneInteractor.currentScene)
fakeCommunalSceneRepository.changeScene(CommunalScenes.Blank)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
index 9be786f..096c3da 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
@@ -193,6 +193,7 @@
@Test
@EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
+ @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
fun testTransitionToLockscreen_onWake_canNotDream_glanceableHubAvailable() =
kosmos.runTest {
whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(false)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
index e203a27..1dddfc1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
@@ -18,11 +18,17 @@
package com.android.systemui.keyguard.domain.interactor
import android.content.Intent
+import android.os.PowerManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.provider.Settings
import android.view.accessibility.accessibilityManagerWrapper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.internal.logging.uiEventLogger
+import com.android.systemui.Flags.FLAG_DOUBLE_TAP_TO_SLEEP
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
@@ -39,6 +45,8 @@
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository
+import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.advanceTimeBy
@@ -46,14 +54,19 @@
import kotlinx.coroutines.test.runTest
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
import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidJUnit4::class)
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
class KeyguardTouchHandlingInteractorTest : SysuiTestCase() {
private val kosmos =
testKosmos().apply {
@@ -61,17 +74,23 @@
this.uiEventLogger = mock<UiEventLoggerFake>()
}
+ @get:Rule val setFlagsRule = SetFlagsRule()
+
private lateinit var underTest: KeyguardTouchHandlingInteractor
private val logger = kosmos.uiEventLogger
private val testScope = kosmos.testScope
private val keyguardRepository = kosmos.fakeKeyguardRepository
private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val secureSettingsRepository = kosmos.userAwareSecureSettingsRepository
+
+ @Mock private lateinit var powerManager: PowerManager
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
overrideResource(R.bool.long_press_keyguard_customize_lockscreen_enabled, true)
+ overrideResource(com.android.internal.R.bool.config_supportDoubleTapSleep, true)
whenever(kosmos.accessibilityManagerWrapper.getRecommendedTimeoutMillis(anyInt(), anyInt()))
.thenAnswer { it.arguments[0] }
@@ -80,13 +99,13 @@
@After
fun tearDown() {
- mContext
- .getOrCreateTestableResources()
- .removeOverride(R.bool.long_press_keyguard_customize_lockscreen_enabled)
+ val testableResource = mContext.getOrCreateTestableResources()
+ testableResource.removeOverride(R.bool.long_press_keyguard_customize_lockscreen_enabled)
+ testableResource.removeOverride(com.android.internal.R.bool.config_supportDoubleTapSleep)
}
@Test
- fun isEnabled() =
+ fun isLongPressEnabled() =
testScope.runTest {
val isEnabled = collectLastValue(underTest.isLongPressHandlingEnabled)
KeyguardState.values().forEach { keyguardState ->
@@ -101,7 +120,7 @@
}
@Test
- fun isEnabled_alwaysFalseWhenQuickSettingsAreVisible() =
+ fun isLongPressEnabled_alwaysFalseWhenQuickSettingsAreVisible() =
testScope.runTest {
val isEnabled = collectLastValue(underTest.isLongPressHandlingEnabled)
KeyguardState.values().forEach { keyguardState ->
@@ -112,7 +131,7 @@
}
@Test
- fun isEnabled_alwaysFalseWhenConfigEnabledBooleanIsFalse() =
+ fun isLongPressEnabled_alwaysFalseWhenConfigEnabledBooleanIsFalse() =
testScope.runTest {
overrideResource(R.bool.long_press_keyguard_customize_lockscreen_enabled, false)
createUnderTest()
@@ -294,6 +313,119 @@
assertThat(isMenuVisible).isFalse()
}
+ @Test
+ @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+ fun isDoubleTapEnabled_flagEnabled_userSettingEnabled_onlyTrueInLockScreenState() {
+ testScope.runTest {
+ secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true)
+
+ val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled)
+ KeyguardState.entries.forEach { keyguardState ->
+ setUpState(keyguardState = keyguardState)
+
+ if (keyguardState == KeyguardState.LOCKSCREEN) {
+ assertThat(isEnabled()).isTrue()
+ } else {
+ assertThat(isEnabled()).isFalse()
+ }
+ }
+ }
+ }
+
+ @Test
+ @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+ fun isDoubleTapEnabled_flagEnabled_userSettingDisabled_alwaysFalse() {
+ testScope.runTest {
+ secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, false)
+
+ val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled)
+ KeyguardState.entries.forEach { keyguardState ->
+ setUpState(keyguardState = keyguardState)
+
+ assertThat(isEnabled()).isFalse()
+ }
+ }
+ }
+
+ @Test
+ @DisableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+ fun isDoubleTapEnabled_flagDisabled_userSettingEnabled_alwaysFalse() {
+ testScope.runTest {
+ secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true)
+
+ val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled)
+ KeyguardState.entries.forEach { keyguardState ->
+ setUpState(keyguardState = keyguardState)
+
+ assertThat(isEnabled()).isFalse()
+ }
+ }
+ }
+
+
+
+ @Test
+ @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+ fun isDoubleTapEnabled_flagEnabledAndConfigDisabled_alwaysFalse() {
+ testScope.runTest {
+ secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true)
+ overrideResource(com.android.internal.R.bool.config_supportDoubleTapSleep, false)
+ createUnderTest()
+
+ val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled)
+ KeyguardState.entries.forEach { keyguardState ->
+ setUpState(keyguardState = keyguardState)
+
+ assertThat(isEnabled()).isFalse()
+ }
+ }
+ }
+
+ @Test
+ @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+ fun isDoubleTapEnabled_quickSettingsVisible_alwaysFalse() {
+ testScope.runTest {
+ secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true)
+
+ val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled)
+ KeyguardState.entries.forEach { keyguardState ->
+ setUpState(keyguardState = keyguardState, isQuickSettingsVisible = true)
+
+ assertThat(isEnabled()).isFalse()
+ }
+ }
+ }
+
+ @Test
+ @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+ fun onDoubleClick_doubleTapEnabled() {
+ testScope.runTest {
+ secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true)
+ val isEnabled by collectLastValue(underTest.isDoubleTapHandlingEnabled)
+ runCurrent()
+
+ underTest.onDoubleClick()
+
+ assertThat(isEnabled).isTrue()
+ verify(powerManager).goToSleep(anyLong())
+ }
+ }
+
+ @Test
+ @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+ fun onDoubleClick_doubleTapDisabled() {
+ testScope.runTest {
+ secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, false)
+ val isEnabled by collectLastValue(underTest.isDoubleTapHandlingEnabled)
+ runCurrent()
+
+ underTest.onDoubleClick()
+
+ assertThat(isEnabled).isFalse()
+ verify(powerManager, never()).goToSleep(anyLong())
+ }
+ }
+
private suspend fun createUnderTest(isRevampedWppFeatureEnabled: Boolean = true) {
// This needs to be re-created for each test outside of kosmos since the flag values are
// read during initialization to set up flows. Maybe there is a better way to handle that.
@@ -309,6 +441,9 @@
accessibilityManager = kosmos.accessibilityManagerWrapper,
pulsingGestureListener = kosmos.pulsingGestureListener,
faceAuthInteractor = kosmos.deviceEntryFaceAuthInteractor,
+ secureSettingsRepository = secureSettingsRepository,
+ powerManager = powerManager,
+ systemClock = kosmos.fakeSystemClock,
)
setUpState()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index 8df70ef..7d5e9a5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -33,7 +33,7 @@
import com.android.systemui.communal.domain.interactor.CommunalSceneTransitionInteractor
import com.android.systemui.communal.domain.interactor.communalSceneInteractor
import com.android.systemui.communal.domain.interactor.communalSceneTransitionInteractor
-import com.android.systemui.communal.domain.interactor.setCommunalAvailable
+import com.android.systemui.communal.domain.interactor.setCommunalV2Available
import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled
import com.android.systemui.communal.domain.interactor.setCommunalV2Enabled
import com.android.systemui.communal.shared.model.CommunalScenes
@@ -1004,16 +1004,15 @@
@BrokenWithSceneContainer(339465026)
fun occludedToGlanceableHub_communalKtfRefactor() =
testScope.runTest {
- // GIVEN a device on lockscreen and communal is available
- keyguardRepository.setKeyguardShowing(true)
- kosmos.setCommunalAvailable(true)
- runCurrent()
-
// GIVEN a prior transition has run to OCCLUDED from GLANCEABLE_HUB
runTransitionAndSetWakefulness(KeyguardState.GLANCEABLE_HUB, KeyguardState.OCCLUDED)
keyguardRepository.setKeyguardOccluded(true)
runCurrent()
+ // GIVEN a device on lockscreen and communal is available
+ kosmos.setCommunalV2Available(true)
+ runCurrent()
+
// WHEN occlusion ends
keyguardRepository.setKeyguardOccluded(false)
runCurrent()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt
index 68a591d..1d42424 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt
@@ -16,11 +16,9 @@
package com.android.systemui.qs.panels.ui.viewmodel
-
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.qs.FakeQSTile
import com.android.systemui.qs.pipeline.data.repository.tileSpecRepository
@@ -48,45 +46,43 @@
}
@Test
- fun changeTileDetailsViewModel() = with(kosmos) {
- testScope.runTest {
- val specs = listOf(
- spec,
- specNoDetails,
- )
- tileSpecRepository.setTiles(0, specs)
- runCurrent()
+ fun changeTileDetailsViewModel() =
+ with(kosmos) {
+ testScope.runTest {
+ val specs = listOf(spec, specNoDetails)
+ tileSpecRepository.setTiles(0, specs)
+ runCurrent()
- val tiles = currentTilesInteractor.currentTiles.value
+ val tiles = currentTilesInteractor.currentTiles.value
- assertThat(currentTilesInteractor.currentTilesSpecs.size).isEqualTo(2)
- assertThat(tiles[1].spec).isEqualTo(specNoDetails)
- (tiles[1].tile as FakeQSTile).hasDetailsViewModel = false
+ assertThat(currentTilesInteractor.currentTilesSpecs.size).isEqualTo(2)
+ assertThat(tiles[1].spec).isEqualTo(specNoDetails)
+ (tiles[1].tile as FakeQSTile).hasDetailsViewModel = false
- assertThat(underTest.activeTileDetails).isNull()
+ assertThat(underTest.activeTileDetails).isNull()
- // Click on the tile who has the `spec`.
- assertThat(underTest.onTileClicked(spec)).isTrue()
- assertThat(underTest.activeTileDetails).isNotNull()
- assertThat(underTest.activeTileDetails?.getTitle()).isEqualTo("internet")
+ // Click on the tile who has the `spec`.
+ assertThat(underTest.onTileClicked(spec)).isTrue()
+ assertThat(underTest.activeTileDetails).isNotNull()
+ assertThat(underTest.activeTileDetails?.title).isEqualTo("internet")
- // Click on a tile who dose not have a valid spec.
- assertThat(underTest.onTileClicked(null)).isFalse()
- assertThat(underTest.activeTileDetails).isNull()
+ // Click on a tile who dose not have a valid spec.
+ assertThat(underTest.onTileClicked(null)).isFalse()
+ assertThat(underTest.activeTileDetails).isNull()
- // Click again on the tile who has the `spec`.
- assertThat(underTest.onTileClicked(spec)).isTrue()
- assertThat(underTest.activeTileDetails).isNotNull()
- assertThat(underTest.activeTileDetails?.getTitle()).isEqualTo("internet")
+ // Click again on the tile who has the `spec`.
+ assertThat(underTest.onTileClicked(spec)).isTrue()
+ assertThat(underTest.activeTileDetails).isNotNull()
+ assertThat(underTest.activeTileDetails?.title).isEqualTo("internet")
- // Click on a tile who dose not have a detailed view.
- assertThat(underTest.onTileClicked(specNoDetails)).isFalse()
- assertThat(underTest.activeTileDetails).isNull()
+ // Click on a tile who dose not have a detailed view.
+ assertThat(underTest.onTileClicked(specNoDetails)).isFalse()
+ assertThat(underTest.activeTileDetails).isNull()
- underTest.closeDetailedView()
- assertThat(underTest.activeTileDetails).isNull()
+ underTest.closeDetailedView()
+ assertThat(underTest.activeTileDetails).isNull()
- assertThat(underTest.onTileClicked(null)).isFalse()
+ assertThat(underTest.onTileClicked(null)).isFalse()
+ }
}
- }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
index c308976..5bde7ad 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
@@ -691,11 +691,11 @@
var currentModel: TileDetailsViewModel? = null
val setCurrentModel = { model: TileDetailsViewModel? -> currentModel = model }
tiles!![0].tile.getDetailsViewModel(setCurrentModel)
- assertThat(currentModel?.getTitle()).isEqualTo("a")
+ assertThat(currentModel?.title).isEqualTo("a")
currentModel = null
tiles!![1].tile.getDetailsViewModel(setCurrentModel)
- assertThat(currentModel?.getTitle()).isEqualTo("b")
+ assertThat(currentModel?.title).isEqualTo("b")
currentModel = null
tiles!![2].tile.getDetailsViewModel(setCurrentModel)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java
index c5752691d..65763a3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java
@@ -45,6 +45,7 @@
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.RankingBuilder;
import com.android.systemui.statusbar.SbnBuilder;
+import com.android.systemui.statusbar.notification.collection.BundleEntry;
import com.android.systemui.statusbar.notification.collection.ListEntry;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -273,6 +274,18 @@
}
@Test
+ public void testSilentSectioner_acceptsBundle() {
+ BundleEntry bundleEntry = new BundleEntry("testBundleKey");
+ assertTrue(mSilentSectioner.isInSection(bundleEntry));
+ }
+
+ @Test
+ public void testMinimizedSectioner_rejectsBundle() {
+ BundleEntry bundleEntry = new BundleEntry("testBundleKey");
+ assertFalse(mMinimizedSectioner.isInSection(bundleEntry));
+ }
+
+ @Test
public void testMinSection() {
when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false);
setRankingAmbient(true);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index 7847437..e6b2c25 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -364,6 +364,7 @@
.setUid(UID)
.setInitialPid(2000)
.setNotification(summary)
+ .setUser(USER_HANDLE)
.setParent(GroupEntry.ROOT_ENTRY)
.build();
GroupEntryBuilder groupEntry = new GroupEntryBuilder()
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt
index b52db83..7657a21 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt
@@ -13,7 +13,6 @@
*/
package com.android.systemui.plugins.clocks
-import android.graphics.RectF
import com.android.systemui.plugins.annotations.ProtectedInterface
import com.android.systemui.plugins.annotations.SimpleProperty
import java.io.PrintWriter
@@ -50,5 +49,5 @@
}
interface ClockEventListener {
- fun onBoundsChanged(bounds: RectF)
+ fun onBoundsChanged(bounds: VRectF)
}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt
index 20ee6c1..a658c15 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt
@@ -45,6 +45,7 @@
* render within the centered targetRect to avoid obstructing other elements. The specified
* targetRegion is relative to the parent view.
*/
+ @Deprecated("No longer necessary, pending removal")
fun onTargetRegionChanged(targetRegion: Rect?)
/** Called to notify the clock about its display. */
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt
index 02a3902..f9ff75d 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt
@@ -16,7 +16,6 @@
package com.android.systemui.plugins.clocks
-import android.graphics.Rect
import android.view.View
import android.view.View.MeasureSpec
import com.android.systemui.log.core.LogLevel
@@ -56,12 +55,9 @@
}
fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
- d({ "onLayout($bool1, ${Rect(int1, int2, long1.toInt(), long2.toInt())})" }) {
+ d({ "onLayout($bool1, ${VRect(long1.toULong())})" }) {
bool1 = changed
- int1 = left
- int2 = top
- long1 = right.toLong()
- long2 = bottom.toLong()
+ long1 = VRect(left, top, right, bottom).data.toLong()
}
}
@@ -108,8 +104,11 @@
}
}
- fun animateDoze() {
- d("animateDoze()")
+ fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
+ d({ "animateDoze(isDozing=$bool1, isAnimated=$bool2)" }) {
+ bool1 = isDozing
+ bool2 = isAnimated
+ }
}
fun animateCharge() {
@@ -117,10 +116,7 @@
}
fun animateFidget(x: Float, y: Float) {
- d({ "animateFidget($str1, $str2)" }) {
- str1 = x.toString()
- str2 = y.toString()
- }
+ d({ "animateFidget(${VPointF(long1.toULong())})" }) { long1 = VPointF(x, y).data.toLong() }
}
companion object {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/VPoint.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VPoint.kt
similarity index 94%
rename from packages/SystemUI/customization/src/com/android/systemui/shared/clocks/VPoint.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VPoint.kt
index 3dae530..1fb37ec 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/VPoint.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VPoint.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.shared.clocks
+package com.android.systemui.plugins.clocks
import android.graphics.Point
import android.graphics.PointF
@@ -30,22 +30,20 @@
private fun unpackX(data: ULong): Int = ((data and X_MASK) shr 32).toInt()
-private fun unpackY(data: ULong): Int = (data and Y_MASK).toInt()
+private fun unpackY(data: ULong): Int = ((data and Y_MASK) shr 0).toInt()
private fun pack(x: Int, y: Int): ULong {
- return ((x.toULong() shl 32) and X_MASK) or (y.toULong() and Y_MASK)
+ return ((x.toULong() shl 32) and X_MASK) or ((y.toULong() shl 0) and Y_MASK)
}
@JvmInline
-value class VPointF(private val data: ULong) {
+value class VPointF(val data: ULong) {
val x: Float
get() = Float.fromBits(unpackX(data))
val y: Float
get() = Float.fromBits(unpackY(data))
- constructor() : this(0f, 0f)
-
constructor(pt: PointF) : this(pt.x, pt.y)
constructor(x: Int, y: Int) : this(x.toFloat(), y.toFloat())
@@ -139,15 +137,13 @@
}
@JvmInline
-value class VPoint(private val data: ULong) {
+value class VPoint(val data: ULong) {
val x: Int
get() = unpackX(data)
val y: Int
get() = unpackY(data)
- constructor() : this(0, 0)
-
constructor(x: Int, y: Int) : this(pack(x, y))
fun toPoint() = Point(x, y)
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VRect.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VRect.kt
new file mode 100644
index 0000000..3c1adf2
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VRect.kt
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2025 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.plugins.clocks
+
+import android.graphics.Rect
+import android.graphics.RectF
+import android.util.Half
+
+private val LEFT_MASK: ULong = 0xFFFF000000000000U
+private val TOP_MASK: ULong = 0x0000FFFF00000000U
+private val RIGHT_MASK: ULong = 0x00000000FFFF0000U
+private val BOTTOM_MASK: ULong = 0x000000000000FFFFU
+
+private fun unpackLeft(data: ULong): Short = ((data and LEFT_MASK) shr 48).toShort()
+
+private fun unpackTop(data: ULong): Short = ((data and TOP_MASK) shr 32).toShort()
+
+private fun unpackRight(data: ULong): Short = ((data and RIGHT_MASK) shr 16).toShort()
+
+private fun unpackBottom(data: ULong): Short = ((data and BOTTOM_MASK) shr 0).toShort()
+
+private fun pack(left: Short, top: Short, right: Short, bottom: Short): ULong {
+ return ((left.toULong() shl 48) and LEFT_MASK) or
+ ((top.toULong() shl 32) and TOP_MASK) or
+ ((right.toULong() shl 16) and RIGHT_MASK) or
+ ((bottom.toULong() shl 0) and BOTTOM_MASK)
+}
+
+@JvmInline
+value class VRectF(val data: ULong) {
+ val left: Float
+ get() = fromBits(unpackLeft(data))
+
+ val top: Float
+ get() = fromBits(unpackTop(data))
+
+ val right: Float
+ get() = fromBits(unpackRight(data))
+
+ val bottom: Float
+ get() = fromBits(unpackBottom(data))
+
+ val width: Float
+ get() = right - left
+
+ val height: Float
+ get() = bottom - top
+
+ constructor(rect: RectF) : this(rect.left, rect.top, rect.right, rect.bottom)
+
+ constructor(
+ rect: Rect
+ ) : this(
+ left = rect.left.toFloat(),
+ top = rect.top.toFloat(),
+ right = rect.right.toFloat(),
+ bottom = rect.bottom.toFloat(),
+ )
+
+ constructor(
+ left: Float,
+ top: Float,
+ right: Float,
+ bottom: Float,
+ ) : this(pack(toBits(left), toBits(top), toBits(right), toBits(bottom)))
+
+ val center: VPointF
+ get() = VPointF(left, top) + size / 2f
+
+ val size: VPointF
+ get() = VPointF(width, height)
+
+ override fun toString() = "($left, $top) -> ($right, $bottom)"
+
+ companion object {
+ private fun toBits(value: Float): Short = Half.halfToShortBits(Half.toHalf(value))
+
+ private fun fromBits(value: Short): Float = Half.toFloat(Half.intBitsToHalf(value.toInt()))
+
+ fun fromCenter(center: VPointF, size: VPointF): VRectF {
+ return VRectF(
+ center.x - size.x / 2,
+ center.y - size.y / 2,
+ center.x + size.x / 2,
+ center.y + size.y / 2,
+ )
+ }
+
+ fun fromTopLeft(pos: VPointF, size: VPointF): VRectF {
+ return VRectF(pos.x, pos.y, pos.x + size.x, pos.y + size.y)
+ }
+
+ val ZERO = VRectF(0f, 0f, 0f, 0f)
+ }
+}
+
+@JvmInline
+value class VRect(val data: ULong) {
+ val left: Int
+ get() = unpackLeft(data).toInt()
+
+ val top: Int
+ get() = unpackTop(data).toInt()
+
+ val right: Int
+ get() = unpackRight(data).toInt()
+
+ val bottom: Int
+ get() = unpackBottom(data).toInt()
+
+ val width: Int
+ get() = right - left
+
+ val height: Int
+ get() = bottom - top
+
+ constructor(
+ rect: Rect
+ ) : this(
+ left = rect.left.toShort(),
+ top = rect.top.toShort(),
+ right = rect.right.toShort(),
+ bottom = rect.bottom.toShort(),
+ )
+
+ constructor(
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int,
+ ) : this(
+ left = left.toShort(),
+ top = top.toShort(),
+ right = right.toShort(),
+ bottom = bottom.toShort(),
+ )
+
+ constructor(
+ left: Short,
+ top: Short,
+ right: Short,
+ bottom: Short,
+ ) : this(pack(left, top, right, bottom))
+
+ val center: VPoint
+ get() = VPoint(left, top) + size / 2
+
+ val size: VPoint
+ get() = VPoint(width, height)
+
+ override fun toString() = "($left, $top) -> ($right, $bottom)"
+
+ companion object {
+ val ZERO = VRect(0, 0, 0, 0)
+
+ fun fromCenter(center: VPoint, size: VPoint): VRect {
+ return VRect(
+ (center.x - size.x / 2).toShort(),
+ (center.y - size.y / 2).toShort(),
+ (center.x + size.x / 2).toShort(),
+ (center.y + size.y / 2).toShort(),
+ )
+ }
+
+ fun fromTopLeft(pos: VPoint, size: VPoint): VRect {
+ return VRect(
+ pos.x.toShort(),
+ pos.y.toShort(),
+ (pos.x + size.x).toShort(),
+ (pos.y + size.y).toShort(),
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt
index be0362f..ac7a8574 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt
@@ -16,15 +16,13 @@
package com.android.systemui.plugins.qs
-/**
- * The base view model class for rendering the Tile's TileDetailsView.
- */
-abstract class TileDetailsViewModel {
+/** The view model interface for rendering the Tile's TileDetailsView. */
+interface TileDetailsViewModel {
// The callback when the settings button is clicked. Currently this is the same as the on tile
// long press callback
- abstract fun clickOnSettingsButton()
+ fun clickOnSettingsButton()
- abstract fun getTitle(): String
+ val title: String
- abstract fun getSubTitle(): String
+ val subTitle: String
}
diff --git a/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json b/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json
index b1d6a27..3f03fcf 100644
--- a/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json
+++ b/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json
@@ -1 +1 @@
-{"v":"5.7.13","fr":60,"ip":0,"op":55,"w":80,"h":80,"nm":"unlocked_to_checkmark_success","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".colorAccentPrimary","cl":"colorAccentPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[47.143,32,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[2.761,0],[0,-2.7],[0,0]],"o":[[0,0],[0,-2.7],[-2.761,0],[0,0],[0,0]],"v":[[5,5],[5,-0.111],[0,-5],[-5,-0.111],[-5.01,4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".colorAccentPrimary","cl":"colorAccentPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[38,45,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[18,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".colorAccentPrimary","cl":"colorAccentPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[37.999,44.999,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.42,0],[0,1.42],[1.42,0],[0,-1.42]],"o":[[1.42,0],[0,-1.42],[-1.42,0],[0,1.42]],"v":[[0,2.571],[2.571,0],[0,-2.571],[-2.571,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40.5,40.75,0],"ix":2,"l":2},"a":{"a":0,"k":[12.5,-6.25,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[60,60,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":19,"s":[112,112,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-10.556,-9.889],[7.444,6.555],[34.597,-20.486]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":10,"op":910,"st":10,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[93.5,93.5,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[4]}],"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":10,"op":77,"st":10,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".grey700","cl":"grey700","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.278431385756,0.278431385756,0.278431385756,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":20,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
+{"v":"5.7.13","fr":60,"ip":0,"op":55,"w":80,"h":80,"nm":"unlocked_to_checkmark_success","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".onPrimary","cl":"onPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[47.143,32,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[2.761,0],[0,-2.7],[0,0]],"o":[[0,0],[0,-2.7],[-2.761,0],[0,0],[0,0]],"v":[[5,5],[5,-0.111],[0,-5],[-5,-0.111],[-5.01,4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onPrimary","cl":"onPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[38,45,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[18,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".onPrimary","cl":"onPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[37.999,44.999,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.42,0],[0,1.42],[1.42,0],[0,-1.42]],"o":[[1.42,0],[0,-1.42],[-1.42,0],[0,1.42]],"v":[[0,2.571],[2.571,0],[0,-2.571],[-2.571,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40.5,40.75,0],"ix":2,"l":2},"a":{"a":0,"k":[12.5,-6.25,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[60,60,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":19,"s":[112,112,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-10.556,-9.889],[7.444,6.555],[34.597,-20.486]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":10,"op":910,"st":10,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[93.5,93.5,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[4]}],"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":10,"op":77,"st":10,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".primary","cl":"primary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.278431385756,0.278431385756,0.278431385756,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":20,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 1549b69..763b107 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -21,7 +21,6 @@
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Resources
-import android.graphics.RectF
import android.os.Trace
import android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
import android.provider.Settings.Global.ZEN_MODE_OFF
@@ -60,6 +59,7 @@
import com.android.systemui.plugins.clocks.ClockFaceController
import com.android.systemui.plugins.clocks.ClockMessageBuffers
import com.android.systemui.plugins.clocks.ClockTickRate
+import com.android.systemui.plugins.clocks.VRectF
import com.android.systemui.plugins.clocks.WeatherData
import com.android.systemui.plugins.clocks.ZenData
import com.android.systemui.plugins.clocks.ZenData.ZenMode
@@ -250,7 +250,7 @@
private var largeClockOnSecondaryDisplay = false
val dozeAmount = MutableStateFlow(0f)
- val onClockBoundsChanged = MutableStateFlow<RectF?>(null)
+ val onClockBoundsChanged = MutableStateFlow<VRectF>(VRectF.ZERO)
private fun isDarkTheme(): Boolean {
val isLightTheme = TypedValue()
@@ -315,7 +315,7 @@
private val clockListener =
object : ClockEventListener {
- override fun onBoundsChanged(bounds: RectF) {
+ override fun onBoundsChanged(bounds: VRectF) {
onClockBoundsChanged.value = bounds
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt
index 5863a93..7d8752e 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt
@@ -21,18 +21,14 @@
class BluetoothDetailsViewModel(
private val onSettingsClick: () -> Unit,
val detailsContentViewModel: BluetoothDetailsContentViewModel,
-) : TileDetailsViewModel() {
+) : TileDetailsViewModel {
override fun clickOnSettingsButton() {
onSettingsClick()
}
- override fun getTitle(): String {
- // TODO: b/378513956 Update the placeholder text
- return "Bluetooth"
- }
+ // TODO: b/378513956 Update the placeholder text
+ override val title = "Bluetooth"
- override fun getSubTitle(): String {
- // TODO: b/378513956 Update the placeholder text
- return "Tap to connect or disconnect a device"
- }
+ // TODO: b/378513956 Update the placeholder text
+ override val subTitle = "Tap to connect or disconnect a device"
}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt
index 42f1b73..6c3535a 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt
@@ -27,17 +27,17 @@
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
+import com.android.systemui.Flags.doubleTapToSleep
import com.android.systemui.log.TouchHandlingViewLogger
import com.android.systemui.shade.TouchLogger
-import kotlin.math.pow
-import kotlin.math.sqrt
import kotlinx.coroutines.DisposableHandle
/**
- * View designed to handle long-presses.
+ * View designed to handle long-presses and double taps.
*
- * The view will not handle any long pressed by default. To set it up, set up a listener and, when
- * ready to start consuming long-presses, set [setLongPressHandlingEnabled] to `true`.
+ * The view will not handle any gestures by default. To set it up, set up a listener and, when ready
+ * to start consuming gestures, set the gesture's enable function ([setLongPressHandlingEnabled],
+ * [setDoublePressHandlingEnabled]) to `true`.
*/
class TouchHandlingView(
context: Context,
@@ -62,6 +62,9 @@
/** Notifies that the gesture was too short for a long press, it is actually a click. */
fun onSingleTapDetected(view: View, x: Int, y: Int) = Unit
+
+ /** Notifies that a double tap has been detected by the given view. */
+ fun onDoubleTapDetected(view: View) = Unit
}
var listener: Listener? = null
@@ -70,6 +73,7 @@
private val interactionHandler: TouchHandlingViewInteractionHandler by lazy {
TouchHandlingViewInteractionHandler(
+ context = context,
postDelayed = { block, timeoutMs ->
val dispatchToken = Any()
@@ -84,6 +88,9 @@
onSingleTapDetected = { x, y ->
listener?.onSingleTapDetected(this@TouchHandlingView, x = x, y = y)
},
+ onDoubleTapDetected = {
+ if (doubleTapToSleep()) listener?.onDoubleTapDetected(this@TouchHandlingView)
+ },
longPressDuration = longPressDuration,
allowedTouchSlop = allowedTouchSlop,
logger = logger,
@@ -100,13 +107,17 @@
interactionHandler.isLongPressHandlingEnabled = isEnabled
}
+ fun setDoublePressHandlingEnabled(isEnabled: Boolean) {
+ interactionHandler.isDoubleTapHandlingEnabled = isEnabled
+ }
+
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
return TouchLogger.logDispatchTouch("long_press", event, super.dispatchTouchEvent(event))
}
@SuppressLint("ClickableViewAccessibility")
- override fun onTouchEvent(event: MotionEvent?): Boolean {
- return interactionHandler.onTouchEvent(event?.toModel())
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ return interactionHandler.onTouchEvent(event)
}
private fun setupAccessibilityDelegate() {
@@ -154,33 +165,3 @@
}
}
}
-
-private fun MotionEvent.toModel(): TouchHandlingViewInteractionHandler.MotionEventModel {
- return when (actionMasked) {
- MotionEvent.ACTION_DOWN ->
- TouchHandlingViewInteractionHandler.MotionEventModel.Down(x = x.toInt(), y = y.toInt())
- MotionEvent.ACTION_MOVE ->
- TouchHandlingViewInteractionHandler.MotionEventModel.Move(
- distanceMoved = distanceMoved()
- )
- MotionEvent.ACTION_UP ->
- TouchHandlingViewInteractionHandler.MotionEventModel.Up(
- distanceMoved = distanceMoved(),
- gestureDuration = gestureDuration(),
- )
- MotionEvent.ACTION_CANCEL -> TouchHandlingViewInteractionHandler.MotionEventModel.Cancel
- else -> TouchHandlingViewInteractionHandler.MotionEventModel.Other
- }
-}
-
-private fun MotionEvent.distanceMoved(): Float {
- return if (historySize > 0) {
- sqrt((x - getHistoricalX(0)).pow(2) + (y - getHistoricalY(0)).pow(2))
- } else {
- 0f
- }
-}
-
-private fun MotionEvent.gestureDuration(): Long {
- return eventTime - downTime
-}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt
index 5863fc6..fe509d7 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt
@@ -17,12 +17,20 @@
package com.android.systemui.common.ui.view
+import android.content.Context
import android.graphics.Point
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.ViewConfiguration
import com.android.systemui.log.TouchHandlingViewLogger
+import kotlin.math.pow
+import kotlin.math.sqrt
+import kotlin.properties.Delegates
import kotlinx.coroutines.DisposableHandle
/** Encapsulates logic to handle complex touch interactions with a [TouchHandlingView]. */
class TouchHandlingViewInteractionHandler(
+ context: Context,
/**
* Callback to run the given [Runnable] with the given delay, returning a [DisposableHandle]
* allowing the delayed runnable to be canceled before it is run.
@@ -34,6 +42,8 @@
private val onLongPressDetected: (x: Int, y: Int) -> Unit,
/** Callback reporting the a single tap gesture was detected at the given coordinates. */
private val onSingleTapDetected: (x: Int, y: Int) -> Unit,
+ /** Callback reporting that a double tap gesture was detected. */
+ private val onDoubleTapDetected: () -> Unit,
/** Time for the touch to be considered a long-press in ms */
var longPressDuration: () -> Long,
/**
@@ -58,48 +68,98 @@
}
var isLongPressHandlingEnabled: Boolean = false
+ var isDoubleTapHandlingEnabled: Boolean = false
var scheduledLongPressHandle: DisposableHandle? = null
+ private var doubleTapAwaitingUp: Boolean = false
+ private var lastDoubleTapDownEventTime: Long? = null
+
/** Record coordinate for last DOWN event for single tap */
val lastEventDownCoordinate = Point(-1, -1)
- fun onTouchEvent(event: MotionEventModel?): Boolean {
- if (!isLongPressHandlingEnabled) {
- return false
- }
- return when (event) {
- is MotionEventModel.Down -> {
- scheduleLongPress(event.x, event.y)
- lastEventDownCoordinate.x = event.x
- lastEventDownCoordinate.y = event.y
- true
+ private val gestureDetector =
+ GestureDetector(
+ context,
+ object : GestureDetector.SimpleOnGestureListener() {
+ override fun onDoubleTap(event: MotionEvent): Boolean {
+ if (isDoubleTapHandlingEnabled) {
+ doubleTapAwaitingUp = true
+ lastDoubleTapDownEventTime = event.eventTime
+ return true
+ }
+ return false
+ }
+ },
+ )
+
+ fun onTouchEvent(event: MotionEvent): Boolean {
+ if (isDoubleTapHandlingEnabled) {
+ gestureDetector.onTouchEvent(event)
+ if (event.actionMasked == MotionEvent.ACTION_UP && doubleTapAwaitingUp) {
+ lastDoubleTapDownEventTime?.let { time ->
+ if (
+ event.eventTime - time < ViewConfiguration.getDoubleTapTimeout()
+ ) {
+ cancelScheduledLongPress()
+ onDoubleTapDetected()
+ }
+ }
+ doubleTapAwaitingUp = false
+ } else if (event.actionMasked == MotionEvent.ACTION_CANCEL && doubleTapAwaitingUp) {
+ doubleTapAwaitingUp = false
}
- is MotionEventModel.Move -> {
- if (event.distanceMoved > allowedTouchSlop) {
- logger?.cancelingLongPressDueToTouchSlop(event.distanceMoved, allowedTouchSlop)
+ }
+
+ if (isLongPressHandlingEnabled) {
+ val motionEventModel = event.toModel()
+
+ return when (motionEventModel) {
+ is MotionEventModel.Down -> {
+ scheduleLongPress(motionEventModel.x, motionEventModel.y)
+ lastEventDownCoordinate.x = motionEventModel.x
+ lastEventDownCoordinate.y = motionEventModel.y
+ true
+ }
+
+ is MotionEventModel.Move -> {
+ if (motionEventModel.distanceMoved > allowedTouchSlop) {
+ logger?.cancelingLongPressDueToTouchSlop(
+ motionEventModel.distanceMoved,
+ allowedTouchSlop,
+ )
+ cancelScheduledLongPress()
+ }
+ false
+ }
+
+ is MotionEventModel.Up -> {
+ logger?.onUpEvent(
+ motionEventModel.distanceMoved,
+ allowedTouchSlop,
+ motionEventModel.gestureDuration,
+ )
cancelScheduledLongPress()
+ if (
+ motionEventModel.distanceMoved <= allowedTouchSlop &&
+ motionEventModel.gestureDuration < longPressDuration()
+ ) {
+ logger?.dispatchingSingleTap()
+ dispatchSingleTap(lastEventDownCoordinate.x, lastEventDownCoordinate.y)
+ }
+ false
}
- false
- }
- is MotionEventModel.Up -> {
- logger?.onUpEvent(event.distanceMoved, allowedTouchSlop, event.gestureDuration)
- cancelScheduledLongPress()
- if (
- event.distanceMoved <= allowedTouchSlop &&
- event.gestureDuration < longPressDuration()
- ) {
- logger?.dispatchingSingleTap()
- dispatchSingleTap(lastEventDownCoordinate.x, lastEventDownCoordinate.y)
+
+ is MotionEventModel.Cancel -> {
+ logger?.motionEventCancelled()
+ cancelScheduledLongPress()
+ false
}
- false
+
+ else -> false
}
- is MotionEventModel.Cancel -> {
- logger?.motionEventCancelled()
- cancelScheduledLongPress()
- false
- }
- else -> false
}
+
+ return false
}
private fun scheduleLongPress(x: Int, y: Int) {
@@ -134,4 +194,30 @@
onSingleTapDetected(x, y)
}
+
+ private fun MotionEvent.toModel(): MotionEventModel {
+ return when (actionMasked) {
+ MotionEvent.ACTION_DOWN -> MotionEventModel.Down(x = x.toInt(), y = y.toInt())
+ MotionEvent.ACTION_MOVE -> MotionEventModel.Move(distanceMoved = distanceMoved())
+ MotionEvent.ACTION_UP ->
+ MotionEventModel.Up(
+ distanceMoved = distanceMoved(),
+ gestureDuration = gestureDuration(),
+ )
+ MotionEvent.ACTION_CANCEL -> MotionEventModel.Cancel
+ else -> MotionEventModel.Other
+ }
+ }
+
+ private fun MotionEvent.distanceMoved(): Float {
+ return if (historySize > 0) {
+ sqrt((x - getHistoricalX(0)).pow(2) + (y - getHistoricalY(0)).pow(2))
+ } else {
+ 0f
+ }
+ }
+
+ private fun MotionEvent.gestureDuration(): Long {
+ return eventTime - downTime
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSuppressionStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSuppressionStartable.kt
new file mode 100644
index 0000000..6a611ec
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSuppressionStartable.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2025 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.communal
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.communal.data.model.SuppressionReason
+import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.log.dagger.CommunalTableLog
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logDiffsForTable
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+
+@SysUISingleton
+class CommunalSuppressionStartable
+@Inject
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ @Background private val bgDispatcher: CoroutineDispatcher,
+ private val suppressionFlows: Set<@JvmSuppressWildcards Flow<SuppressionReason?>>,
+ private val communalSettingsInteractor: CommunalSettingsInteractor,
+ @CommunalTableLog private val tableLogBuffer: TableLogBuffer,
+) : CoreStartable {
+ override fun start() {
+ getSuppressionReasons()
+ .onEach { reasons -> communalSettingsInteractor.setSuppressionReasons(reasons) }
+ .logDiffsForTable(
+ tableLogBuffer = tableLogBuffer,
+ columnName = "suppressionReasons",
+ initialValue = emptyList(),
+ )
+ .flowOn(bgDispatcher)
+ .launchIn(applicationScope)
+ }
+
+ private fun getSuppressionReasons(): Flow<List<SuppressionReason>> {
+ if (!communalSettingsInteractor.isCommunalFlagEnabled()) {
+ return flowOf(listOf(SuppressionReason.ReasonFlagDisabled))
+ }
+ return combine(suppressionFlows) { reasons -> reasons.filterNotNull() }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
index bb3be53..a31c0bd 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
@@ -29,6 +29,7 @@
import com.android.systemui.communal.data.repository.CommunalTutorialRepositoryModule
import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule
import com.android.systemui.communal.domain.interactor.CommunalSceneTransitionInteractor
+import com.android.systemui.communal.domain.suppression.dagger.CommunalSuppressionModule
import com.android.systemui.communal.shared.log.CommunalMetricsLogger
import com.android.systemui.communal.shared.log.CommunalStatsLogProxyImpl
import com.android.systemui.communal.shared.model.CommunalScenes
@@ -70,6 +71,7 @@
CommunalSmartspaceRepositoryModule::class,
CommunalStartableModule::class,
GlanceableHubWidgetManagerModule::class,
+ CommunalSuppressionModule::class,
]
)
interface CommunalModule {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt
index 7358aa7..a4f75e8 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt
@@ -22,6 +22,7 @@
import com.android.systemui.communal.CommunalMetricsStartable
import com.android.systemui.communal.CommunalOngoingContentStartable
import com.android.systemui.communal.CommunalSceneStartable
+import com.android.systemui.communal.CommunalSuppressionStartable
import com.android.systemui.communal.DevicePosturingListener
import com.android.systemui.communal.log.CommunalLoggerStartable
import com.android.systemui.communal.widgets.CommunalAppWidgetHostStartable
@@ -73,4 +74,9 @@
@IntoMap
@ClassKey(DevicePosturingListener::class)
fun bindDevicePosturingistener(impl: DevicePosturingListener): CoreStartable
+
+ @Binds
+ @IntoMap
+ @ClassKey(CommunalSuppressionStartable::class)
+ fun bindCommunalSuppressionStartable(impl: CommunalSuppressionStartable): CoreStartable
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalEnabledState.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalEnabledState.kt
deleted file mode 100644
index 83a5bdb..0000000
--- a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalEnabledState.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.communal.data.model
-
-import com.android.systemui.log.table.Diffable
-import com.android.systemui.log.table.TableRowLogger
-import java.util.EnumSet
-
-/** Reasons that communal is disabled, primarily for logging. */
-enum class DisabledReason(val loggingString: String) {
- /** Communal should be disabled due to invalid current user */
- DISABLED_REASON_INVALID_USER("invalidUser"),
- /** Communal should be disabled due to the flag being off */
- DISABLED_REASON_FLAG("flag"),
- /** Communal should be disabled because the user has turned off the setting */
- DISABLED_REASON_USER_SETTING("userSetting"),
- /** Communal is disabled by the device policy app */
- DISABLED_REASON_DEVICE_POLICY("devicePolicy"),
-}
-
-/**
- * Model representing the reasons communal hub should be disabled. Allows logging reasons separately
- * for debugging.
- */
-@JvmInline
-value class CommunalEnabledState(
- private val disabledReasons: EnumSet<DisabledReason> =
- EnumSet.noneOf(DisabledReason::class.java)
-) : Diffable<CommunalEnabledState>, Set<DisabledReason> by disabledReasons {
-
- /** Creates [CommunalEnabledState] with a single reason for being disabled */
- constructor(reason: DisabledReason) : this(EnumSet.of(reason))
-
- /** Checks if there are any reasons communal should be disabled. If none, returns true. */
- val enabled: Boolean
- get() = isEmpty()
-
- override fun logDiffs(prevVal: CommunalEnabledState, row: TableRowLogger) {
- for (reason in DisabledReason.entries) {
- val newVal = contains(reason)
- if (newVal != prevVal.contains(reason)) {
- row.logChange(
- columnName = reason.loggingString,
- value = newVal,
- )
- }
- }
- }
-
- override fun logFull(row: TableRowLogger) {
- for (reason in DisabledReason.entries) {
- row.logChange(columnName = reason.loggingString, value = contains(reason))
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalFeature.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalFeature.kt
new file mode 100644
index 0000000..5fb1c4e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalFeature.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2025 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.communal.data.model
+
+import android.annotation.IntDef
+
+@Retention(AnnotationRetention.SOURCE)
+@IntDef(
+ flag = true,
+ prefix = ["FEATURE_"],
+ value = [FEATURE_AUTO_OPEN, FEATURE_MANUAL_OPEN, FEATURE_ENABLED, FEATURE_ALL],
+)
+annotation class CommunalFeature
+
+/** If we should automatically open the hub */
+const val FEATURE_AUTO_OPEN: Int = 1
+
+/** If the user is allowed to manually open the hub */
+const val FEATURE_MANUAL_OPEN: Int = 1 shl 1
+
+/**
+ * If the hub should be considered enabled. If not, it may be cleaned up entirely to reduce memory
+ * footprint.
+ */
+const val FEATURE_ENABLED: Int = 1 shl 2
+
+const val FEATURE_ALL: Int = FEATURE_ENABLED or FEATURE_MANUAL_OPEN or FEATURE_AUTO_OPEN
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/SuppressionReason.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/SuppressionReason.kt
new file mode 100644
index 0000000..de05bed
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/model/SuppressionReason.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2025 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.communal.data.model
+
+sealed interface SuppressionReason {
+ @CommunalFeature val suppressedFeatures: Int
+
+ /** Whether this reason suppresses a particular feature. */
+ fun isSuppressed(@CommunalFeature feature: Int): Boolean {
+ return (suppressedFeatures and feature) != 0
+ }
+
+ /** Suppress hub automatically opening due to Android Auto projection */
+ data object ReasonCarProjection : SuppressionReason {
+ override val suppressedFeatures: Int = FEATURE_AUTO_OPEN
+ }
+
+ /** Suppress hub due to the "When to dream" conditions not being met */
+ data class ReasonWhenToAutoShow(override val suppressedFeatures: Int) : SuppressionReason
+
+ /** Suppress hub due to device policy */
+ data object ReasonDevicePolicy : SuppressionReason {
+ override val suppressedFeatures: Int = FEATURE_ALL
+ }
+
+ /** Suppress hub due to the user disabling the setting */
+ data object ReasonSettingDisabled : SuppressionReason {
+ override val suppressedFeatures: Int = FEATURE_ALL
+ }
+
+ /** Suppress hub due to the user being locked */
+ data object ReasonUserLocked : SuppressionReason {
+ override val suppressedFeatures: Int = FEATURE_ALL
+ }
+
+ /** Suppress hub due the a secondary user being active */
+ data object ReasonSecondaryUser : SuppressionReason {
+ override val suppressedFeatures: Int = FEATURE_ALL
+ }
+
+ /** Suppress hub due to the flag being disabled */
+ data object ReasonFlagDisabled : SuppressionReason {
+ override val suppressedFeatures: Int = FEATURE_ALL
+ }
+
+ /** Suppress hub due to an unknown reason, used as initial state and in tests */
+ data class ReasonUnknown(override val suppressedFeatures: Int = FEATURE_ALL) :
+ SuppressionReason
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CarProjectionRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CarProjectionRepository.kt
new file mode 100644
index 0000000..4fe641a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CarProjectionRepository.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2025 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.communal.data.repository
+
+import android.app.UiModeManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.kotlin.emitOnStart
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.withContext
+
+interface CarProjectionRepository {
+ /** Whether car projection is active. */
+ val projectionActive: Flow<Boolean>
+
+ /**
+ * Checks the system for the current car projection state.
+ *
+ * @return True if projection is active, false otherwise.
+ */
+ suspend fun isProjectionActive(): Boolean
+}
+
+@SysUISingleton
+class CarProjectionRepositoryImpl
+@Inject
+constructor(
+ private val uiModeManager: UiModeManager,
+ @Background private val bgDispatcher: CoroutineDispatcher,
+) : CarProjectionRepository {
+ override val projectionActive: Flow<Boolean> =
+ conflatedCallbackFlow {
+ val listener =
+ UiModeManager.OnProjectionStateChangedListener { _, _ -> trySend(Unit) }
+ uiModeManager.addOnProjectionStateChangedListener(
+ UiModeManager.PROJECTION_TYPE_AUTOMOTIVE,
+ bgDispatcher.asExecutor(),
+ listener,
+ )
+ awaitClose { uiModeManager.removeOnProjectionStateChangedListener(listener) }
+ }
+ .emitOnStart()
+ .map { isProjectionActive() }
+ .flowOn(bgDispatcher)
+
+ override suspend fun isProjectionActive(): Boolean =
+ withContext(bgDispatcher) {
+ (uiModeManager.activeProjectionTypes and UiModeManager.PROJECTION_TYPE_AUTOMOTIVE) != 0
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt
index 7f137f3..0d590db 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt
@@ -22,4 +22,6 @@
@Module
interface CommunalRepositoryModule {
@Binds fun communalRepository(impl: CommunalSceneRepositoryImpl): CommunalSceneRepository
+
+ @Binds fun carProjectionRepository(impl: CarProjectionRepositoryImpl): CarProjectionRepository
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt
index 4c291a0..6f688d1 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt
@@ -26,12 +26,9 @@
import com.android.systemui.Flags.communalHub
import com.android.systemui.Flags.glanceableHubV2
import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.communal.data.model.CommunalEnabledState
-import com.android.systemui.communal.data.model.DisabledReason
-import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_DEVICE_POLICY
-import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_FLAG
-import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_INVALID_USER
-import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_USER_SETTING
+import com.android.systemui.communal.data.model.CommunalFeature
+import com.android.systemui.communal.data.model.FEATURE_ALL
+import com.android.systemui.communal.data.model.SuppressionReason
import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryModule.Companion.DEFAULT_BACKGROUND_TYPE
import com.android.systemui.communal.shared.model.CommunalBackgroundType
import com.android.systemui.communal.shared.model.WhenToDream
@@ -43,22 +40,23 @@
import com.android.systemui.util.kotlin.emitOnStart
import com.android.systemui.util.settings.SecureSettings
import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
-import java.util.EnumSet
import javax.inject.Inject
import javax.inject.Named
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onStart
interface CommunalSettingsRepository {
- /** A [CommunalEnabledState] for the specified user. */
- fun getEnabledState(user: UserInfo): Flow<CommunalEnabledState>
+ /** Whether a particular feature is enabled */
+ fun isEnabled(@CommunalFeature feature: Int): Flow<Boolean>
- fun getScreensaverEnabledState(user: UserInfo): Flow<Boolean>
+ /**
+ * Suppresses the hub with the given reasons. If there are no reasons, the hub will not be
+ * suppressed.
+ */
+ fun setSuppressionReasons(reasons: List<SuppressionReason>)
/**
* Returns a [WhenToDream] for the specified user, indicating what state the device should be in
@@ -66,6 +64,9 @@
*/
fun getWhenToDreamState(user: UserInfo): Flow<WhenToDream>
+ /** Returns whether glanceable hub is enabled by the current user. */
+ fun getSettingEnabledByUser(user: UserInfo): Flow<Boolean>
+
/**
* Returns true if any glanceable hub functionality should be enabled via configs and flags.
*
@@ -123,6 +124,19 @@
resources.getBoolean(com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault)
}
+ private val _suppressionReasons =
+ MutableStateFlow<List<SuppressionReason>>(
+ // Suppress hub by default until we get an initial update.
+ listOf(SuppressionReason.ReasonUnknown(FEATURE_ALL))
+ )
+
+ override fun isEnabled(@CommunalFeature feature: Int): Flow<Boolean> =
+ _suppressionReasons.map { reasons -> reasons.none { it.isSuppressed(feature) } }
+
+ override fun setSuppressionReasons(reasons: List<SuppressionReason>) {
+ _suppressionReasons.value = reasons
+ }
+
override fun getFlagEnabled(): Boolean {
return if (getV2FlagEnabled()) {
true
@@ -138,44 +152,6 @@
glanceableHubV2()
}
- override fun getEnabledState(user: UserInfo): Flow<CommunalEnabledState> {
- if (!user.isMain) {
- return flowOf(CommunalEnabledState(DISABLED_REASON_INVALID_USER))
- }
- if (!getFlagEnabled()) {
- return flowOf(CommunalEnabledState(DISABLED_REASON_FLAG))
- }
- return combine(
- getEnabledByUser(user).mapToReason(DISABLED_REASON_USER_SETTING),
- getAllowedByDevicePolicy(user).mapToReason(DISABLED_REASON_DEVICE_POLICY),
- ) { reasons ->
- reasons.filterNotNull()
- }
- .map { reasons ->
- if (reasons.isEmpty()) {
- EnumSet.noneOf(DisabledReason::class.java)
- } else {
- EnumSet.copyOf(reasons)
- }
- }
- .map { reasons -> CommunalEnabledState(reasons) }
- .flowOn(bgDispatcher)
- }
-
- override fun getScreensaverEnabledState(user: UserInfo): Flow<Boolean> =
- secureSettings
- .observerFlow(userId = user.id, names = arrayOf(Settings.Secure.SCREENSAVER_ENABLED))
- // Force an update
- .onStart { emit(Unit) }
- .map {
- secureSettings.getIntForUser(
- Settings.Secure.SCREENSAVER_ENABLED,
- SCREENSAVER_ENABLED_SETTING_DEFAULT,
- user.id,
- ) == 1
- }
- .flowOn(bgDispatcher)
-
override fun getWhenToDreamState(user: UserInfo): Flow<WhenToDream> =
secureSettings
.observerFlow(
@@ -247,11 +223,11 @@
?: defaultBackgroundType
}
- private fun getEnabledByUser(user: UserInfo): Flow<Boolean> =
+ override fun getSettingEnabledByUser(user: UserInfo): Flow<Boolean> =
secureSettings
.observerFlow(userId = user.id, names = arrayOf(Settings.Secure.GLANCEABLE_HUB_ENABLED))
// Force an update
- .onStart { emit(Unit) }
+ .emitOnStart()
.map {
secureSettings.getIntForUser(
Settings.Secure.GLANCEABLE_HUB_ENABLED,
@@ -259,17 +235,13 @@
user.id,
) == 1
}
+ .flowOn(bgDispatcher)
companion object {
const val GLANCEABLE_HUB_BACKGROUND_SETTING = "glanceable_hub_background"
private const val ENABLED_SETTING_DEFAULT = 1
- private const val SCREENSAVER_ENABLED_SETTING_DEFAULT = 0
}
}
private fun DevicePolicyManager.areKeyguardWidgetsAllowed(userId: Int): Boolean =
(getKeyguardDisabledFeatures(null, userId) and KEYGUARD_DISABLE_WIDGETS_ALL) == 0
-
-private fun Flow<Boolean>.mapToReason(reason: DisabledReason) = map { enabled ->
- if (enabled) null else reason
-}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractor.kt
new file mode 100644
index 0000000..17b61e1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractor.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2025 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.communal.domain.interactor
+
+import com.android.systemui.communal.data.repository.CarProjectionRepository
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+
+@SysUISingleton
+class CarProjectionInteractor @Inject constructor(repository: CarProjectionRepository) {
+ /** Whether car projection is active. */
+ val projectionActive = repository.projectionActive
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractor.kt
new file mode 100644
index 0000000..51df333
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractor.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2025 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.communal.domain.interactor
+
+import com.android.systemui.common.domain.interactor.BatteryInteractor
+import com.android.systemui.communal.dagger.CommunalModule.Companion.SWIPE_TO_HUB
+import com.android.systemui.communal.data.model.FEATURE_AUTO_OPEN
+import com.android.systemui.communal.data.model.FEATURE_MANUAL_OPEN
+import com.android.systemui.communal.data.model.SuppressionReason
+import com.android.systemui.communal.posturing.domain.interactor.PosturingInteractor
+import com.android.systemui.communal.shared.model.WhenToDream
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dock.DockManager
+import com.android.systemui.dock.retrieveIsDocked
+import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
+import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
+import javax.inject.Inject
+import javax.inject.Named
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+@SysUISingleton
+class CommunalAutoOpenInteractor
+@Inject
+constructor(
+ communalSettingsInteractor: CommunalSettingsInteractor,
+ @Background private val backgroundContext: CoroutineContext,
+ private val batteryInteractor: BatteryInteractor,
+ private val posturingInteractor: PosturingInteractor,
+ private val dockManager: DockManager,
+ @Named(SWIPE_TO_HUB) private val allowSwipeAlways: Boolean,
+) {
+ val shouldAutoOpen: Flow<Boolean> =
+ communalSettingsInteractor.whenToDream
+ .flatMapLatestConflated { whenToDream ->
+ when (whenToDream) {
+ WhenToDream.WHILE_CHARGING -> batteryInteractor.isDevicePluggedIn
+ WhenToDream.WHILE_DOCKED -> {
+ allOf(batteryInteractor.isDevicePluggedIn, dockManager.retrieveIsDocked())
+ }
+ WhenToDream.WHILE_POSTURED -> {
+ allOf(batteryInteractor.isDevicePluggedIn, posturingInteractor.postured)
+ }
+ WhenToDream.NEVER -> flowOf(false)
+ }
+ }
+ .flowOn(backgroundContext)
+
+ val suppressionReason: Flow<SuppressionReason?> =
+ shouldAutoOpen.map { conditionMet ->
+ if (conditionMet) {
+ null
+ } else {
+ var suppressedFeatures = FEATURE_AUTO_OPEN
+ if (!allowSwipeAlways) {
+ suppressedFeatures = suppressedFeatures or FEATURE_MANUAL_OPEN
+ }
+ SuppressionReason.ReasonWhenToAutoShow(suppressedFeatures)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 564628d..684c52a 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -30,13 +30,11 @@
import com.android.systemui.Flags.communalResponsiveGrid
import com.android.systemui.Flags.glanceableHubBlurredBackground
import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.common.domain.interactor.BatteryInteractor
import com.android.systemui.communal.data.repository.CommunalMediaRepository
import com.android.systemui.communal.data.repository.CommunalSmartspaceRepository
import com.android.systemui.communal.data.repository.CommunalWidgetRepository
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.domain.model.CommunalContentModel.WidgetContent
-import com.android.systemui.communal.posturing.domain.interactor.PosturingInteractor
import com.android.systemui.communal.shared.model.CommunalBackgroundType
import com.android.systemui.communal.shared.model.CommunalContentSize
import com.android.systemui.communal.shared.model.CommunalContentSize.FixedSize.FULL
@@ -45,14 +43,11 @@
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
import com.android.systemui.communal.shared.model.EditModeState
-import com.android.systemui.communal.shared.model.WhenToDream
import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.dock.DockManager
-import com.android.systemui.dock.retrieveIsDocked
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.Edge
@@ -69,11 +64,8 @@
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.phone.ManagedProfileController
-import com.android.systemui.user.domain.interactor.UserLockedInteractor
import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
-import com.android.systemui.util.kotlin.BooleanFlowOperators.not
import com.android.systemui.util.kotlin.emitOnStart
-import com.android.systemui.util.kotlin.isDevicePluggedIn
import javax.inject.Inject
import kotlin.time.Duration.Companion.minutes
import kotlinx.coroutines.CoroutineDispatcher
@@ -125,10 +117,6 @@
@CommunalLog logBuffer: LogBuffer,
@CommunalTableLog tableLogBuffer: TableLogBuffer,
private val managedProfileController: ManagedProfileController,
- private val batteryInteractor: BatteryInteractor,
- private val dockManager: DockManager,
- private val posturingInteractor: PosturingInteractor,
- private val userLockedInteractor: UserLockedInteractor,
) {
private val logger = Logger(logBuffer, "CommunalInteractor")
@@ -162,11 +150,7 @@
/** Whether communal features are enabled and available. */
val isCommunalAvailable: Flow<Boolean> =
- allOf(
- communalSettingsInteractor.isCommunalEnabled,
- userLockedInteractor.isUserUnlocked(userManager.mainUser),
- keyguardInteractor.isKeyguardShowing,
- )
+ allOf(communalSettingsInteractor.isCommunalEnabled, keyguardInteractor.isKeyguardShowing)
.distinctUntilChanged()
.onEach { available ->
logger.i({ "Communal is ${if (bool1) "" else "un"}available" }) {
@@ -184,37 +168,6 @@
replay = 1,
)
- /**
- * Whether communal hub should be shown automatically, depending on the user's [WhenToDream]
- * state.
- */
- val shouldShowCommunal: StateFlow<Boolean> =
- allOf(
- isCommunalAvailable,
- communalSettingsInteractor.whenToDream
- .flatMapLatest { whenToDream ->
- when (whenToDream) {
- WhenToDream.NEVER -> flowOf(false)
-
- WhenToDream.WHILE_CHARGING -> batteryInteractor.isDevicePluggedIn
-
- WhenToDream.WHILE_DOCKED ->
- allOf(
- batteryInteractor.isDevicePluggedIn,
- dockManager.retrieveIsDocked(),
- )
-
- WhenToDream.WHILE_POSTURED ->
- allOf(
- batteryInteractor.isDevicePluggedIn,
- posturingInteractor.postured,
- )
- }
- }
- .flowOn(bgDispatcher),
- )
- .stateIn(scope = bgScope, started = SharingStarted.Eagerly, initialValue = false)
-
private val _isDisclaimerDismissed = MutableStateFlow(false)
val isDisclaimerDismissed: Flow<Boolean> = _isDisclaimerDismissed.asStateFlow()
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt
index a0b1261..ae89b39 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt
@@ -18,17 +18,18 @@
import android.content.pm.UserInfo
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
-import com.android.systemui.communal.data.model.CommunalEnabledState
+import com.android.systemui.communal.data.model.FEATURE_AUTO_OPEN
+import com.android.systemui.communal.data.model.FEATURE_ENABLED
+import com.android.systemui.communal.data.model.FEATURE_MANUAL_OPEN
+import com.android.systemui.communal.data.model.SuppressionReason
import com.android.systemui.communal.data.repository.CommunalSettingsRepository
import com.android.systemui.communal.shared.model.CommunalBackgroundType
import com.android.systemui.communal.shared.model.WhenToDream
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.log.dagger.CommunalTableLog
-import com.android.systemui.log.table.TableLogBuffer
-import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.settings.UserTracker
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
import java.util.concurrent.Executor
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@@ -53,33 +54,43 @@
private val repository: CommunalSettingsRepository,
userInteractor: SelectedUserInteractor,
private val userTracker: UserTracker,
- @CommunalTableLog tableLogBuffer: TableLogBuffer,
) {
- /** Whether or not communal is enabled for the currently selected user. */
+ /** Whether communal is enabled at all. */
val isCommunalEnabled: StateFlow<Boolean> =
- userInteractor.selectedUserInfo
- .flatMapLatest { user -> repository.getEnabledState(user) }
- .logDiffsForTable(
- tableLogBuffer = tableLogBuffer,
- columnPrefix = "disabledReason",
- initialValue = CommunalEnabledState(),
- )
- .map { model -> model.enabled }
- // Start this eagerly since the value is accessed synchronously in many places.
+ repository
+ .isEnabled(FEATURE_ENABLED)
.stateIn(scope = bgScope, started = SharingStarted.Eagerly, initialValue = false)
- /** Whether or not screensaver (dreams) is enabled for the currently selected user. */
- val isScreensaverEnabled: Flow<Boolean> =
- userInteractor.selectedUserInfo.flatMapLatest { user ->
- repository.getScreensaverEnabledState(user)
- }
+ /** Whether manually opening the hub is enabled */
+ val manualOpenEnabled: StateFlow<Boolean> =
+ repository
+ .isEnabled(FEATURE_MANUAL_OPEN)
+ .stateIn(scope = bgScope, started = SharingStarted.Eagerly, initialValue = false)
+
+ /** Whether auto-opening the hub is enabled */
+ val autoOpenEnabled: StateFlow<Boolean> =
+ repository
+ .isEnabled(FEATURE_AUTO_OPEN)
+ .stateIn(scope = bgScope, started = SharingStarted.Eagerly, initialValue = false)
/** When to dream for the currently selected user. */
val whenToDream: Flow<WhenToDream> =
- userInteractor.selectedUserInfo.flatMapLatest { user ->
+ userInteractor.selectedUserInfo.flatMapLatestConflated { user ->
repository.getWhenToDreamState(user)
}
+ /** Whether communal hub is allowed by device policy for the current user */
+ val allowedForCurrentUserByDevicePolicy: Flow<Boolean> =
+ userInteractor.selectedUserInfo.flatMapLatestConflated { user ->
+ repository.getAllowedByDevicePolicy(user)
+ }
+
+ /** Whether the hub is enabled for the current user */
+ val settingEnabledForCurrentUser: Flow<Boolean> =
+ userInteractor.selectedUserInfo.flatMapLatestConflated { user ->
+ repository.getSettingEnabledByUser(user)
+ }
+
/**
* Returns true if any glanceable hub functionality should be enabled via configs and flags.
*
@@ -109,6 +120,14 @@
*/
fun isV2FlagEnabled(): Boolean = repository.getV2FlagEnabled()
+ /**
+ * Suppresses the hub with the given reasons. If there are no reasons, the hub will not be
+ * suppressed.
+ */
+ fun setSuppressionReasons(reasons: List<SuppressionReason>) {
+ repository.setSuppressionReasons(reasons)
+ }
+
/** The type of background to use for the hub. Used to experiment with different backgrounds */
val communalBackground: Flow<CommunalBackgroundType> =
userInteractor.selectedUserInfo
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/FlowExt.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/FlowExt.kt
new file mode 100644
index 0000000..a10e90f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/FlowExt.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2025 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.communal.domain.suppression
+
+import com.android.systemui.communal.data.model.SuppressionReason
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+fun Flow<Boolean>.mapToReasonIfNotAllowed(reason: SuppressionReason): Flow<SuppressionReason?> =
+ this.map { allowed -> if (allowed) null else reason }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/dagger/CommunalSuppressionModule.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/dagger/CommunalSuppressionModule.kt
new file mode 100644
index 0000000..c62d77e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/dagger/CommunalSuppressionModule.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2025 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.communal.domain.suppression.dagger
+
+import com.android.systemui.Flags.glanceableHubV2
+import com.android.systemui.communal.data.model.SuppressionReason
+import com.android.systemui.communal.domain.interactor.CarProjectionInteractor
+import com.android.systemui.communal.domain.interactor.CommunalAutoOpenInteractor
+import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
+import com.android.systemui.communal.domain.suppression.mapToReasonIfNotAllowed
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.user.domain.interactor.UserLockedInteractor
+import com.android.systemui.util.kotlin.BooleanFlowOperators.not
+import dagger.Module
+import dagger.Provides
+import dagger.multibindings.IntoSet
+import dagger.multibindings.Multibinds
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+@Module
+interface CommunalSuppressionModule {
+ /**
+ * A set of reasons why communal may be suppressed. Ensures that this can be injected even if
+ * it's empty.
+ */
+ @Multibinds fun suppressorSet(): Set<Flow<SuppressionReason?>>
+
+ companion object {
+ @Provides
+ @IntoSet
+ fun provideCarProjectionSuppressor(
+ interactor: CarProjectionInteractor
+ ): Flow<SuppressionReason?> {
+ if (!glanceableHubV2()) {
+ return flowOf(null)
+ }
+ return not(interactor.projectionActive)
+ .mapToReasonIfNotAllowed(SuppressionReason.ReasonCarProjection)
+ }
+
+ @Provides
+ @IntoSet
+ fun provideDevicePolicySuppressor(
+ interactor: CommunalSettingsInteractor
+ ): Flow<SuppressionReason?> {
+ return interactor.allowedForCurrentUserByDevicePolicy.mapToReasonIfNotAllowed(
+ SuppressionReason.ReasonDevicePolicy
+ )
+ }
+
+ @Provides
+ @IntoSet
+ fun provideSettingDisabledSuppressor(
+ interactor: CommunalSettingsInteractor
+ ): Flow<SuppressionReason?> {
+ return interactor.settingEnabledForCurrentUser.mapToReasonIfNotAllowed(
+ SuppressionReason.ReasonSettingDisabled
+ )
+ }
+
+ @Provides
+ @IntoSet
+ fun bindUserLockedSuppressor(interactor: UserLockedInteractor): Flow<SuppressionReason?> {
+ return interactor.currentUserUnlocked.mapToReasonIfNotAllowed(
+ SuppressionReason.ReasonUserLocked
+ )
+ }
+
+ @Provides
+ @IntoSet
+ fun provideAutoOpenSuppressor(
+ interactor: CommunalAutoOpenInteractor
+ ): Flow<SuppressionReason?> {
+ return interactor.suppressionReason
+ }
+
+ @Provides
+ @IntoSet
+ fun provideMainUserSuppressor(
+ interactor: SelectedUserInteractor
+ ): Flow<SuppressionReason?> {
+ return interactor.selectedUserInfo
+ .map { it.isMain }
+ .mapToReasonIfNotAllowed(SuppressionReason.ReasonSecondaryUser)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index 62a98d7..857fa5c 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -369,12 +369,10 @@
val swipeToHubEnabled: Flow<Boolean> by lazy {
val inAllowedDeviceState =
- if (swipeToHub) {
- MutableStateFlow(true)
- } else if (v2FlagEnabled()) {
- communalInteractor.shouldShowCommunal
+ if (v2FlagEnabled()) {
+ communalSettingsInteractor.manualOpenEnabled
} else {
- MutableStateFlow(false)
+ MutableStateFlow(swipeToHub)
}
if (v2FlagEnabled()) {
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt
index a7c078f..36b75c6 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt
@@ -61,7 +61,7 @@
fun startTransitionFromDream() {
val showGlanceableHub =
if (communalSettingsInteractor.isV2FlagEnabled()) {
- communalInteractor.shouldShowCommunal.value
+ communalSettingsInteractor.autoOpenEnabled.value
} else {
communalInteractor.isCommunalEnabled.value &&
!keyguardUpdateMonitor.isEncryptedOrLockdown(userTracker.userId)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index ef06a85..54af8f5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -20,7 +20,6 @@
import android.util.Log
import com.android.app.animation.Interpolators
import com.android.app.tracing.coroutines.launchTraced as launch
-import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
import com.android.systemui.communal.shared.model.CommunalScenes
@@ -60,7 +59,6 @@
private val wakeToGoneInteractor: KeyguardWakeDirectlyToGoneInteractor,
private val communalSettingsInteractor: CommunalSettingsInteractor,
private val communalSceneInteractor: CommunalSceneInteractor,
- private val communalInteractor: CommunalInteractor,
) :
TransitionInteractor(
fromState = KeyguardState.AOD,
@@ -110,7 +108,7 @@
val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value
val biometricUnlockMode = keyguardInteractor.biometricUnlockState.value.mode
val primaryBouncerShowing = keyguardInteractor.primaryBouncerShowing.value
- val shouldShowCommunal = communalInteractor.shouldShowCommunal.value
+ val shouldShowCommunal = communalSettingsInteractor.autoOpenEnabled.value
if (!maybeHandleInsecurePowerGesture()) {
val shouldTransitionToLockscreen =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index 6f5f662..1fc4108 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -153,7 +153,7 @@
.filterRelevantKeyguardStateAnd { isAwake -> isAwake }
.sample(
communalInteractor.isCommunalAvailable,
- communalInteractor.shouldShowCommunal,
+ communalSettingsInteractor.autoOpenEnabled,
)
.collect { (_, isCommunalAvailable, shouldShowCommunal) ->
val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value
@@ -209,7 +209,7 @@
powerInteractor.detailedWakefulness
.filterRelevantKeyguardStateAnd { it.isAwake() }
.sample(
- communalInteractor.shouldShowCommunal,
+ communalSettingsInteractor.autoOpenEnabled,
communalInteractor.isCommunalAvailable,
keyguardInteractor.biometricUnlockState,
wakeToGoneInteractor.canWakeDirectlyToGone,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
index 0fb98ff..3b1b6fc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
@@ -115,7 +115,7 @@
powerInteractor.isAwake
.debounce(50L)
.filterRelevantKeyguardStateAnd { isAwake -> isAwake }
- .sample(communalInteractor.shouldShowCommunal)
+ .sample(communalSettingsInteractor.autoOpenEnabled)
.collect { shouldShowCommunal ->
if (shouldShowCommunal) {
// This case handles tapping the power button to transition through
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
index a01dc02..f8c7a86 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -20,7 +20,6 @@
import android.util.MathUtils
import com.android.app.animation.Interpolators
import com.android.app.tracing.coroutines.launchTraced as launch
-import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
import com.android.systemui.communal.shared.model.CommunalScenes
@@ -69,7 +68,6 @@
private val shadeRepository: ShadeRepository,
powerInteractor: PowerInteractor,
private val communalSettingsInteractor: CommunalSettingsInteractor,
- private val communalInteractor: CommunalInteractor,
private val communalSceneInteractor: CommunalSceneInteractor,
private val swipeToDismissInteractor: SwipeToDismissInteractor,
keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
@@ -355,7 +353,7 @@
private fun listenForLockscreenToGlanceableHubV2() {
scope.launch {
- communalInteractor.shouldShowCommunal
+ communalSettingsInteractor.autoOpenEnabled
.filterRelevantKeyguardStateAnd { shouldShow -> shouldShow }
.collect {
communalSceneInteractor.changeScene(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
index 705eaa2..55534c4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
@@ -20,11 +20,14 @@
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
+import android.os.PowerManager
+import android.provider.Settings
import android.view.accessibility.AccessibilityManager
import androidx.annotation.VisibleForTesting
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger
+import com.android.systemui.Flags.doubleTapToSleep
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -36,10 +39,13 @@
import com.android.systemui.shade.PulsingGestureListener
import com.android.systemui.shade.ShadeDisplayAware
import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
+import com.android.systemui.util.settings.repository.UserAwareSecureSettingsRepository
+import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -66,10 +72,13 @@
private val accessibilityManager: AccessibilityManagerWrapper,
private val pulsingGestureListener: PulsingGestureListener,
private val faceAuthInteractor: DeviceEntryFaceAuthInteractor,
+ private val secureSettingsRepository: UserAwareSecureSettingsRepository,
+ private val powerManager: PowerManager,
+ private val systemClock: SystemClock,
) {
/** Whether the long-press handling feature should be enabled. */
val isLongPressHandlingEnabled: StateFlow<Boolean> =
- if (isFeatureEnabled()) {
+ if (isLongPressFeatureEnabled()) {
combine(
transitionInteractor.isFinishedIn(KeyguardState.LOCKSCREEN),
repository.isQuickSettingsVisible,
@@ -85,6 +94,30 @@
initialValue = false,
)
+ /** Whether the double tap handling handling feature should be enabled. */
+ val isDoubleTapHandlingEnabled: StateFlow<Boolean> =
+ if (isDoubleTapFeatureEnabled()) {
+ combine(
+ transitionInteractor.transitionValue(KeyguardState.LOCKSCREEN),
+ repository.isQuickSettingsVisible,
+ isDoubleTapSettingEnabled(),
+ ) {
+ isFullyTransitionedToLockScreen,
+ isQuickSettingsVisible,
+ isDoubleTapSettingEnabled ->
+ isFullyTransitionedToLockScreen == 1f &&
+ !isQuickSettingsVisible &&
+ isDoubleTapSettingEnabled
+ }
+ } else {
+ flowOf(false)
+ }
+ .stateIn(
+ scope = scope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = false,
+ )
+
private val _isMenuVisible = MutableStateFlow(false)
/** Model for whether the menu should be shown. */
val isMenuVisible: StateFlow<Boolean> =
@@ -116,7 +149,7 @@
private var delayedHideMenuJob: Job? = null
init {
- if (isFeatureEnabled()) {
+ if (isLongPressFeatureEnabled()) {
broadcastDispatcher
.broadcastFlow(IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
.onEach { hideMenu() }
@@ -175,17 +208,30 @@
/** Notifies that the lockscreen has been double clicked. */
fun onDoubleClick() {
- pulsingGestureListener.onDoubleTapEvent()
+ if (isDoubleTapHandlingEnabled.value) {
+ powerManager.goToSleep(systemClock.uptimeMillis())
+ } else {
+ pulsingGestureListener.onDoubleTapEvent()
+ }
+ }
+
+ private fun isDoubleTapSettingEnabled(): Flow<Boolean> {
+ return secureSettingsRepository.boolSetting(Settings.Secure.DOUBLE_TAP_TO_SLEEP)
}
private fun showSettings() {
_shouldOpenSettings.value = true
}
- private fun isFeatureEnabled(): Boolean {
+ private fun isLongPressFeatureEnabled(): Boolean {
return context.resources.getBoolean(R.bool.long_press_keyguard_customize_lockscreen_enabled)
}
+ private fun isDoubleTapFeatureEnabled(): Boolean {
+ return doubleTapToSleep() &&
+ context.resources.getBoolean(com.android.internal.R.bool.config_supportDoubleTapSleep)
+ }
+
/** Updates application state to ask to show the menu. */
private fun showMenu() {
_isMenuVisible.value = true
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
index 17e14c3..70a827d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
@@ -48,7 +48,7 @@
/**
* Updates UI for:
* - device entry containing view (parent view for the below views)
- * - long-press handling view (transparent, no UI)
+ * - touch handling view (transparent, no UI)
* - foreground icon view (lock/unlock/fingerprint)
* - background view (optional)
*/
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
index e81d535..5ef2d6f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
@@ -29,6 +29,7 @@
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.plugins.clocks.VRectF
import com.android.systemui.res.R
import com.android.systemui.shared.R as sharedR
import kotlinx.coroutines.DisposableHandle
@@ -135,7 +136,7 @@
}
}
- if (clockBounds == null) return@collect
+ if (clockBounds == VRectF.ZERO) return@collect
if (isLargeClock) {
val largeDateHeight =
keyguardRootView
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt
index 195413a..485e1ce 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt
@@ -75,6 +75,13 @@
onSingleTap(x, y)
}
+
+ override fun onDoubleTapDetected(view: View) {
+ if (falsingManager.isFalseDoubleTap()) {
+ return
+ }
+ viewModel.onDoubleClick()
+ }
}
view.repeatWhenAttached {
@@ -90,9 +97,20 @@
}
}
}
+ launch("$TAG#viewModel.isDoubleTapHandlingEnabled") {
+ viewModel.isDoubleTapHandlingEnabled.collect { isEnabled ->
+ view.setDoublePressHandlingEnabled(isEnabled)
+ view.contentDescription =
+ if (isEnabled) {
+ view.resources.getString(R.string.accessibility_desc_lock_screen)
+ } else {
+ null
+ }
+ }
+ }
}
}
}
- private const val TAG = "KeyguardLongPressViewBinder"
+ private const val TAG = "KeyguardTouchViewBinder"
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
index 1d2edc6..d4e7af4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
@@ -33,6 +33,9 @@
/** Whether the long-press handling feature should be enabled. */
val isLongPressHandlingEnabled: Flow<Boolean> = interactor.isLongPressHandlingEnabled
+ /** Whether the double tap handling feature should be enabled. */
+ val isDoubleTapHandlingEnabled: Flow<Boolean> = interactor.isDoubleTapHandlingEnabled
+
/** Notifies that the user has long-pressed on the lock screen.
*
* @param isA11yAction: Whether the action was performed as an a11y action
diff --git a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
index e7c2a45..b71d8c9 100644
--- a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
+++ b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
@@ -17,6 +17,7 @@
package com.android.systemui.media;
import android.annotation.Nullable;
+import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
@@ -126,6 +127,8 @@
Log.d(TAG, "play(token=" + token + ", uri=" + uri + ", uid="
+ Binder.getCallingUid() + ")");
}
+ enforceUriUserId(uri);
+
Client client;
synchronized (mClients) {
client = mClients.get(token);
@@ -207,6 +210,7 @@
@Override
public String getTitle(Uri uri) {
+ enforceUriUserId(uri);
final UserHandle user = Binder.getCallingUserHandle();
return Ringtone.getTitle(getContextForUser(user), uri,
false /*followSettingsUri*/, false /*allowRemote*/);
@@ -214,6 +218,7 @@
@Override
public ParcelFileDescriptor openRingtone(Uri uri) {
+ enforceUriUserId(uri);
final UserHandle user = Binder.getCallingUserHandle();
final ContentResolver resolver = getContextForUser(user).getContentResolver();
@@ -241,6 +246,28 @@
}
};
+ /**
+ * Must be called from the Binder calling thread.
+ * Ensures caller is from the same userId as the content they're trying to access.
+ * @param uri the URI to check
+ * @throws SecurityException when in a non-system call and userId in uri differs from the
+ * caller's userId
+ */
+ private void enforceUriUserId(Uri uri) throws SecurityException {
+ final int uriUserId = ContentProvider.getUserIdFromUri(uri, UserHandle.myUserId());
+ // for a non-system call, verify the URI to play belongs to the same user as the caller
+ if (UserHandle.isApp(Binder.getCallingUid()) && (UserHandle.myUserId() != uriUserId)) {
+ final String errorMessage = "Illegal access to uri=" + uri
+ + " content associated with user=" + uriUserId
+ + ", current userID: " + UserHandle.myUserId();
+ if (android.media.audio.Flags.ringtoneUserUriCheck()) {
+ throw new SecurityException(errorMessage);
+ } else {
+ Log.e(TAG, errorMessage, new Exception());
+ }
+ }
+ }
+
private Context getContextForUser(UserHandle user) {
try {
return mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user);
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt
index 49b53c2..dfb32e6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt
@@ -37,6 +37,7 @@
import com.android.settingslib.media.MediaDevice
import com.android.settingslib.media.PhoneMediaDevice
import com.android.settingslib.media.flags.Flags
+import com.android.systemui.Flags.mediaControlsDeviceManagerBackgroundExecution
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.controls.shared.MediaControlDrawables
@@ -94,36 +95,21 @@
data: MediaData,
immediately: Boolean,
receivedSmartspaceCardLatency: Int,
- isSsReactivated: Boolean
+ isSsReactivated: Boolean,
) {
- if (oldKey != null && oldKey != key) {
- val oldEntry = entries.remove(oldKey)
- oldEntry?.stop()
- }
- var entry = entries[key]
- if (entry == null || entry.token != data.token) {
- entry?.stop()
- if (data.device != null) {
- // If we were already provided device info (e.g. from RCN), keep that and don't
- // listen for updates, but process once to push updates to listeners
- processDevice(key, oldKey, data.device)
- return
- }
- val controller = data.token?.let { controllerFactory.create(it) }
- val localMediaManager =
- localMediaManagerFactory.create(data.packageName, controller?.sessionToken)
- val muteAwaitConnectionManager =
- muteAwaitConnectionManagerFactory.create(localMediaManager)
- entry = Entry(key, oldKey, controller, localMediaManager, muteAwaitConnectionManager)
- entries[key] = entry
- entry.start()
+ if (mediaControlsDeviceManagerBackgroundExecution()) {
+ bgExecutor.execute { onMediaLoaded(key, oldKey, data) }
+ } else {
+ onMediaLoaded(key, oldKey, data)
}
}
override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {
- val token = entries.remove(key)
- token?.stop()
- token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } }
+ if (mediaControlsDeviceManagerBackgroundExecution()) {
+ bgExecutor.execute { onMediaRemoved(key, userInitiated) }
+ } else {
+ onMediaRemoved(key, userInitiated)
+ }
}
fun dump(pw: PrintWriter) {
@@ -141,6 +127,47 @@
listeners.forEach { it.onMediaDeviceChanged(key, oldKey, device) }
}
+ private fun onMediaLoaded(key: String, oldKey: String?, data: MediaData) {
+ if (oldKey != null && oldKey != key) {
+ val oldEntry = entries.remove(oldKey)
+ oldEntry?.stop()
+ }
+ var entry = entries[key]
+ if (entry == null || entry.token != data.token) {
+ entry?.stop()
+ if (data.device != null) {
+ // If we were already provided device info (e.g. from RCN), keep that and
+ // don't listen for updates, but process once to push updates to listeners
+ if (mediaControlsDeviceManagerBackgroundExecution()) {
+ fgExecutor.execute { processDevice(key, oldKey, data.device) }
+ } else {
+ processDevice(key, oldKey, data.device)
+ }
+ return
+ }
+ val controller = data.token?.let { controllerFactory.create(it) }
+ val localMediaManager =
+ localMediaManagerFactory.create(data.packageName, controller?.sessionToken)
+ val muteAwaitConnectionManager =
+ muteAwaitConnectionManagerFactory.create(localMediaManager)
+ entry = Entry(key, oldKey, controller, localMediaManager, muteAwaitConnectionManager)
+ entries[key] = entry
+ entry.start()
+ }
+ }
+
+ private fun onMediaRemoved(key: String, userInitiated: Boolean) {
+ val token = entries.remove(key)
+ token?.stop()
+ if (mediaControlsDeviceManagerBackgroundExecution()) {
+ fgExecutor.execute {
+ token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } }
+ }
+ } else {
+ token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } }
+ }
+ }
+
interface Listener {
/** Called when the route has changed for a given notification. */
fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?)
@@ -260,7 +287,7 @@
override fun onAboutToConnectDeviceAdded(
deviceAddress: String,
deviceName: String,
- deviceIcon: Drawable?
+ deviceIcon: Drawable?,
) {
aboutToConnectDeviceOverride =
AboutToConnectDevice(
@@ -270,8 +297,8 @@
/* enabled */ enabled = true,
/* icon */ deviceIcon,
/* name */ deviceName,
- /* showBroadcastButton */ showBroadcastButton = false
- )
+ /* showBroadcastButton */ showBroadcastButton = false,
+ ),
)
updateCurrent()
}
@@ -292,7 +319,7 @@
override fun onBroadcastMetadataChanged(
broadcastId: Int,
- metadata: BluetoothLeBroadcastMetadata
+ metadata: BluetoothLeBroadcastMetadata,
) {
logger.logBroadcastMetadataChanged(broadcastId, metadata.toString())
updateCurrent()
@@ -352,14 +379,14 @@
// route.
connectedDevice?.copy(
name = it.name ?: connectedDevice.name,
- icon = icon
+ icon = icon,
)
}
?: MediaDeviceData(
enabled = false,
icon = MediaControlDrawables.getHomeDevices(context),
name = context.getString(R.string.media_seamless_other_device),
- showBroadcastButton = false
+ showBroadcastButton = false,
)
logger.logRemoteDevice(routingSession?.name, connectedDevice)
} else {
@@ -398,7 +425,7 @@
device?.iconWithoutBackground,
name,
id = device?.id,
- showBroadcastButton = false
+ showBroadcastButton = false,
)
}
}
@@ -415,7 +442,7 @@
icon = iconWithoutBackground,
name = name,
id = id,
- showBroadcastButton = false
+ showBroadcastButton = false,
)
private fun getLeAudioBroadcastDeviceData(): MediaDeviceData {
@@ -425,7 +452,7 @@
icon = MediaControlDrawables.getLeAudioSharing(context),
name = context.getString(R.string.audio_sharing_description),
intent = null,
- showBroadcastButton = false
+ showBroadcastButton = false,
)
} else {
MediaDeviceData(
@@ -433,7 +460,7 @@
icon = MediaControlDrawables.getAntenna(context),
name = broadcastDescription,
intent = null,
- showBroadcastButton = true
+ showBroadcastButton = true,
)
}
}
@@ -449,7 +476,7 @@
device,
controller,
routingSession?.name,
- selectedRoutes?.firstOrNull()?.name
+ selectedRoutes?.firstOrNull()?.name,
)
if (controller == null) {
@@ -514,7 +541,7 @@
MediaDataUtils.getAppLabel(
context,
localMediaManager.packageName,
- context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name)
+ context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name),
)
val isCurrentBroadcastedApp = TextUtils.equals(mediaApp, currentBroadcastedApp)
if (isCurrentBroadcastedApp) {
@@ -538,5 +565,5 @@
*/
private data class AboutToConnectDevice(
val fullMediaDevice: MediaDevice? = null,
- val backupMediaDeviceData: MediaDeviceData? = null
+ val backupMediaDeviceData: MediaDeviceData? = null,
)
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java
index 795e811..6ab4a52 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java
@@ -68,7 +68,6 @@
private final Executor mMainExecutor;
private final Executor mBackgroundExecutor;
View mHolderView;
- private boolean mIsInitVolumeFirstTime;
public MediaOutputAdapterLegacy(
MediaSwitchingController controller,
@@ -78,7 +77,6 @@
super(controller);
mMainExecutor = mainExecutor;
mBackgroundExecutor = backgroundExecutor;
- mIsInitVolumeFirstTime = true;
}
@Override
@@ -261,10 +259,9 @@
updateSeekbarProgressBackground();
}
}
- boolean isCurrentSeekbarInvisible = mSeekBar.getVisibility() == View.GONE;
mSeekBar.setVisibility(showSeekBar ? View.VISIBLE : View.GONE);
if (showSeekBar) {
- initSeekbar(device, isCurrentSeekbarInvisible);
+ initSeekbar(device);
updateContainerContentA11yImportance(false /* isImportant */);
mSeekBar.setContentDescription(contentDescription);
} else {
@@ -274,9 +271,8 @@
void updateGroupSeekBar(String contentDescription) {
updateSeekbarProgressBackground();
- boolean isCurrentSeekbarInvisible = mSeekBar.getVisibility() == View.GONE;
mSeekBar.setVisibility(View.VISIBLE);
- initGroupSeekbar(isCurrentSeekbarInvisible);
+ initGroupSeekbar();
updateContainerContentA11yImportance(false /* isImportant */);
mSeekBar.setContentDescription(contentDescription);
}
@@ -364,31 +360,21 @@
mActiveRadius, 0, 0});
}
- private void initializeSeekbarVolume(
- @Nullable MediaDevice device, int currentVolume,
- boolean isCurrentSeekbarInvisible) {
+ private void initializeSeekbarVolume(@Nullable MediaDevice device, int currentVolume) {
if (!isDragging()) {
if (mSeekBar.getVolume() != currentVolume && (mLatestUpdateVolume == -1
|| currentVolume == mLatestUpdateVolume)) {
// Update only if volume of device and value of volume bar doesn't match.
// Check if response volume match with the latest request, to ignore obsolete
// response
- if (isCurrentSeekbarInvisible && !mIsInitVolumeFirstTime) {
+ if (!mVolumeAnimator.isStarted()) {
if (currentVolume == 0) {
updateMutedVolumeIcon(device);
} else {
updateUnmutedVolumeIcon(device);
}
- } else {
- if (!mVolumeAnimator.isStarted()) {
- if (currentVolume == 0) {
- updateMutedVolumeIcon(device);
- } else {
- updateUnmutedVolumeIcon(device);
- }
- mSeekBar.setVolume(currentVolume);
- mLatestUpdateVolume = -1;
- }
+ mSeekBar.setVolume(currentVolume);
+ mLatestUpdateVolume = -1;
}
} else if (currentVolume == 0) {
mSeekBar.resetVolume();
@@ -398,12 +384,9 @@
mLatestUpdateVolume = -1;
}
}
- if (mIsInitVolumeFirstTime) {
- mIsInitVolumeFirstTime = false;
- }
}
- void initSeekbar(@NonNull MediaDevice device, boolean isCurrentSeekbarInvisible) {
+ void initSeekbar(@NonNull MediaDevice device) {
SeekBarVolumeControl volumeControl = new SeekBarVolumeControl() {
@Override
public int getVolume() {
@@ -432,7 +415,7 @@
}
mSeekBar.setMaxVolume(device.getMaxVolume());
final int currentVolume = device.getCurrentVolume();
- initializeSeekbarVolume(device, currentVolume, isCurrentSeekbarInvisible);
+ initializeSeekbarVolume(device, currentVolume);
mSeekBar.setOnSeekBarChangeListener(new MediaSeekBarChangedListener(
device, volumeControl) {
@@ -445,7 +428,7 @@
}
// Initializes the seekbar for a group of devices.
- void initGroupSeekbar(boolean isCurrentSeekbarInvisible) {
+ void initGroupSeekbar() {
SeekBarVolumeControl volumeControl = new SeekBarVolumeControl() {
@Override
public int getVolume() {
@@ -472,7 +455,7 @@
mSeekBar.setMaxVolume(mController.getSessionVolumeMax());
final int currentVolume = mController.getSessionVolume();
- initializeSeekbarVolume(null, currentVolume, isCurrentSeekbarInvisible);
+ initializeSeekbarVolume(null, currentVolume);
mSeekBar.setOnSeekBarChangeListener(new MediaSeekBarChangedListener(
null, volumeControl) {
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
index 9e6fa48..f796931 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
@@ -145,7 +145,7 @@
@VisibleForTesting
final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>();
final List<MediaDevice> mCachedMediaDevices = new CopyOnWriteArrayList<>();
- private final List<MediaItem> mOutputMediaItemList = new CopyOnWriteArrayList<>();
+ private final OutputMediaItemListProxy mOutputMediaItemListProxy;
private final List<MediaItem> mInputMediaItemList = new CopyOnWriteArrayList<>();
private final AudioManager mAudioManager;
private final PowerExemptionManager mPowerExemptionManager;
@@ -226,6 +226,7 @@
InfoMediaManager.createInstance(mContext, packageName, userHandle, lbm, token);
mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName);
mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName);
+ mOutputMediaItemListProxy = new OutputMediaItemListProxy();
mDialogTransitionAnimator = dialogTransitionAnimator;
mNearbyMediaDevicesManager = nearbyMediaDevicesManager;
mMediaOutputColorSchemeLegacy = MediaOutputColorSchemeLegacy.fromSystemColors(mContext);
@@ -245,7 +246,7 @@
protected void start(@NonNull Callback cb) {
synchronized (mMediaDevicesLock) {
mCachedMediaDevices.clear();
- mOutputMediaItemList.clear();
+ mOutputMediaItemListProxy.clear();
}
mNearbyDeviceInfoMap.clear();
if (mNearbyMediaDevicesManager != null) {
@@ -291,7 +292,7 @@
mLocalMediaManager.stopScan();
synchronized (mMediaDevicesLock) {
mCachedMediaDevices.clear();
- mOutputMediaItemList.clear();
+ mOutputMediaItemListProxy.clear();
}
if (mNearbyMediaDevicesManager != null) {
mNearbyMediaDevicesManager.unregisterNearbyDevicesCallback(this);
@@ -333,7 +334,7 @@
@Override
public void onDeviceListUpdate(List<MediaDevice> devices) {
- boolean isListEmpty = mOutputMediaItemList.isEmpty();
+ boolean isListEmpty = mOutputMediaItemListProxy.isEmpty();
if (isListEmpty || !mIsRefreshing) {
buildMediaItems(devices);
mCallback.onDeviceListChanged();
@@ -347,11 +348,12 @@
}
@Override
- public void onSelectedDeviceStateChanged(MediaDevice device,
- @LocalMediaManager.MediaDeviceState int state) {
+ public void onSelectedDeviceStateChanged(
+ MediaDevice device, @LocalMediaManager.MediaDeviceState int state) {
mCallback.onRouteChanged();
mMetricLogger.logOutputItemSuccess(
- device.toString(), new ArrayList<>(mOutputMediaItemList));
+ device.toString(),
+ new ArrayList<>(mOutputMediaItemListProxy.getOutputMediaItemList()));
}
@Override
@@ -362,7 +364,8 @@
@Override
public void onRequestFailed(int reason) {
mCallback.onRouteChanged();
- mMetricLogger.logOutputItemFailure(new ArrayList<>(mOutputMediaItemList), reason);
+ mMetricLogger.logOutputItemFailure(
+ new ArrayList<>(mOutputMediaItemListProxy.getOutputMediaItemList()), reason);
}
/**
@@ -381,7 +384,7 @@
}
try {
synchronized (mMediaDevicesLock) {
- mOutputMediaItemList.removeIf((MediaItem::isMutingExpectedDevice));
+ mOutputMediaItemListProxy.removeMutingExpectedDevices();
}
mAudioManager.cancelMuteAwaitConnection(mAudioManager.getMutingExpectedDevice());
} catch (Exception e) {
@@ -574,14 +577,14 @@
private void buildMediaItems(List<MediaDevice> devices) {
synchronized (mMediaDevicesLock) {
- List<MediaItem> updatedMediaItems = buildMediaItems(mOutputMediaItemList, devices);
- mOutputMediaItemList.clear();
- mOutputMediaItemList.addAll(updatedMediaItems);
+ List<MediaItem> updatedMediaItems =
+ buildMediaItems(mOutputMediaItemListProxy.getOutputMediaItemList(), devices);
+ mOutputMediaItemListProxy.clearAndAddAll(updatedMediaItems);
}
}
- protected List<MediaItem> buildMediaItems(List<MediaItem> oldMediaItems,
- List<MediaDevice> devices) {
+ protected List<MediaItem> buildMediaItems(
+ List<MediaItem> oldMediaItems, List<MediaDevice> devices) {
synchronized (mMediaDevicesLock) {
if (!mLocalMediaManager.isPreferenceRouteListingExist()) {
attachRangeInfo(devices);
@@ -689,7 +692,8 @@
* list.
*/
@GuardedBy("mMediaDevicesLock")
- private List<MediaItem> categorizeMediaItemsLocked(MediaDevice connectedMediaDevice,
+ private List<MediaItem> categorizeMediaItemsLocked(
+ MediaDevice connectedMediaDevice,
List<MediaDevice> devices,
boolean needToHandleMutingExpectedDevice) {
List<MediaItem> finalMediaItems = new ArrayList<>();
@@ -748,6 +752,14 @@
}
private void attachConnectNewDeviceItemIfNeeded(List<MediaItem> mediaItems) {
+ MediaItem connectNewDeviceItem = getConnectNewDeviceItem();
+ if (connectNewDeviceItem != null) {
+ mediaItems.add(connectNewDeviceItem);
+ }
+ }
+
+ @Nullable
+ private MediaItem getConnectNewDeviceItem() {
boolean isSelectedDeviceNotAGroup = getSelectedMediaDevice().size() == 1;
if (enableInputRouting()) {
// When input routing is enabled, there are expected to be at least 2 total selected
@@ -756,9 +768,9 @@
}
// Attach "Connect a device" item only when current output is not remote and not a group
- if (!isCurrentConnectedDeviceRemote() && isSelectedDeviceNotAGroup) {
- mediaItems.add(MediaItem.createPairNewDeviceMediaItem());
- }
+ return (!isCurrentConnectedDeviceRemote() && isSelectedDeviceNotAGroup)
+ ? MediaItem.createPairNewDeviceMediaItem()
+ : null;
}
private void attachRangeInfo(List<MediaDevice> devices) {
@@ -847,13 +859,13 @@
mediaItems.add(
MediaItem.createGroupDividerMediaItem(
mContext.getString(R.string.media_output_group_title)));
- mediaItems.addAll(mOutputMediaItemList);
+ mediaItems.addAll(mOutputMediaItemListProxy.getOutputMediaItemList());
}
public List<MediaItem> getMediaItemList() {
// If input routing is not enabled, only return output media items.
if (!enableInputRouting()) {
- return mOutputMediaItemList;
+ return mOutputMediaItemListProxy.getOutputMediaItemList();
}
// If input routing is enabled, return both output and input media items.
@@ -959,7 +971,7 @@
public boolean isAnyDeviceTransferring() {
synchronized (mMediaDevicesLock) {
- for (MediaItem mediaItem : mOutputMediaItemList) {
+ for (MediaItem mediaItem : mOutputMediaItemListProxy.getOutputMediaItemList()) {
if (mediaItem.getMediaDevice().isPresent()
&& mediaItem.getMediaDevice().get().getState()
== LocalMediaManager.MediaDeviceState.STATE_CONNECTING) {
@@ -999,8 +1011,11 @@
startActivity(launchIntent, controller);
}
- void launchLeBroadcastNotifyDialog(View mediaOutputDialog, BroadcastSender broadcastSender,
- BroadcastNotifyDialog action, final DialogInterface.OnClickListener listener) {
+ void launchLeBroadcastNotifyDialog(
+ View mediaOutputDialog,
+ BroadcastSender broadcastSender,
+ BroadcastNotifyDialog action,
+ final DialogInterface.OnClickListener listener) {
final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
switch (action) {
case ACTION_FIRST_LAUNCH:
@@ -1230,8 +1245,8 @@
return !sourceList.isEmpty();
}
- boolean addSourceIntoSinkDeviceWithBluetoothLeAssistant(BluetoothDevice sink,
- BluetoothLeBroadcastMetadata metadata, boolean isGroupOp) {
+ boolean addSourceIntoSinkDeviceWithBluetoothLeAssistant(
+ BluetoothDevice sink, BluetoothLeBroadcastMetadata metadata, boolean isGroupOp) {
LocalBluetoothLeBroadcastAssistant assistant =
mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
if (assistant == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java b/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java
new file mode 100644
index 0000000..1c9c0b1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.dialog;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/** A proxy of holding the list of Output Switcher's output media items. */
+public class OutputMediaItemListProxy {
+ private final List<MediaItem> mOutputMediaItemList;
+
+ public OutputMediaItemListProxy() {
+ mOutputMediaItemList = new CopyOnWriteArrayList<>();
+ }
+
+ /** Returns the list of output media items. */
+ public List<MediaItem> getOutputMediaItemList() {
+ return mOutputMediaItemList;
+ }
+
+ /** Updates the list of output media items with the given list. */
+ public void clearAndAddAll(List<MediaItem> updatedMediaItems) {
+ mOutputMediaItemList.clear();
+ mOutputMediaItemList.addAll(updatedMediaItems);
+ }
+
+ /** Removes the media items with muting expected devices. */
+ public void removeMutingExpectedDevices() {
+ mOutputMediaItemList.removeIf((MediaItem::isMutingExpectedDevice));
+ }
+
+ /** Clears the output media item list. */
+ public void clear() {
+ mOutputMediaItemList.clear();
+ }
+
+ /** Returns whether the output media item list is empty. */
+ public boolean isEmpty() {
+ return mOutputMediaItemList.isEmpty();
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
index d8fc52b..8dc27bf4 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
@@ -162,10 +162,6 @@
): Boolean {
return this@NoteTaskInitializer.handleKeyGestureEvent(event)
}
-
- override fun isKeyGestureSupported(gestureType: Int): Boolean {
- return this@NoteTaskInitializer.isKeyGestureSupported(gestureType)
- }
}
/**
@@ -225,10 +221,6 @@
return true
}
- private fun isKeyGestureSupported(gestureType: Int): Boolean {
- return gestureType == KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES
- }
-
companion object {
val MULTI_PRESS_TIMEOUT = ViewConfiguration.getMultiPressTimeout().toLong()
val LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout().toLong()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt
index 701f44e..d40ecc9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt
@@ -61,6 +61,9 @@
DisposableEffect(Unit) { onDispose { detailsViewModel.closeDetailedView() } }
+ val title = tileDetailedViewModel.title
+ val subTitle = tileDetailedViewModel.subTitle
+
Column(
modifier =
modifier
@@ -90,7 +93,7 @@
)
}
Text(
- text = tileDetailedViewModel.getTitle(),
+ text = title,
modifier = Modifier.align(Alignment.CenterVertically),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleLarge,
@@ -110,7 +113,7 @@
}
}
Text(
- text = tileDetailedViewModel.getSubTitle(),
+ text = subTitle,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleSmall,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt
index 99f52c2..3ae90d2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt
@@ -16,14 +16,20 @@
package com.android.systemui.qs.panels.ui.compose.toolbar
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
import com.android.systemui.compose.modifiers.sysuiResTag
+import com.android.systemui.development.ui.compose.BuildNumber
import com.android.systemui.qs.footer.ui.compose.IconButton
import com.android.systemui.qs.panels.ui.viewmodel.toolbar.ToolbarViewModel
+import com.android.systemui.qs.ui.compose.borderOnFocus
@Composable
fun Toolbar(viewModel: ToolbarViewModel, modifier: Modifier = Modifier) {
@@ -44,7 +50,18 @@
Modifier.sysuiResTag("settings_button_container"),
)
- Spacer(modifier = Modifier.weight(1f))
+ Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.Center) {
+ BuildNumber(
+ viewModelFactory = viewModel.buildNumberViewModelFactory,
+ textColor = MaterialTheme.colorScheme.onSurface,
+ modifier =
+ Modifier.borderOnFocus(
+ color = MaterialTheme.colorScheme.secondary,
+ cornerSize = CornerSize(1.dp),
+ )
+ .wrapContentSize(),
+ )
+ }
IconButton(
{ viewModel.powerButtonViewModel },
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt
index e54bfa2..10d7871 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt
@@ -24,6 +24,7 @@
import com.android.systemui.animation.Expandable
import com.android.systemui.classifier.domain.interactor.FalsingInteractor
import com.android.systemui.classifier.domain.interactor.runIfNotFalseTap
+import com.android.systemui.development.ui.viewmodel.BuildNumberViewModel
import com.android.systemui.globalactions.GlobalActionsDialogLite
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
@@ -46,6 +47,7 @@
@AssistedInject
constructor(
editModeButtonViewModelFactory: EditModeButtonViewModel.Factory,
+ val buildNumberViewModelFactory: BuildNumberViewModel.Factory,
private val footerActionsInteractor: FooterActionsInteractor,
private val globalActionsDialogLiteProvider: Provider<GlobalActionsDialogLite>,
private val falsingInteractor: FalsingInteractor,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt
index 7d396c5..8ffba1e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt
@@ -19,26 +19,14 @@
import android.view.LayoutInflater
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import com.android.systemui.res.R
@Composable
fun InternetDetailsContent(viewModel: InternetDetailsViewModel) {
val coroutineScope = rememberCoroutineScope()
- val context = LocalContext.current
-
- val internetDetailsContentManager = remember {
- viewModel.contentManagerFactory.create(
- canConfigMobileData = viewModel.getCanConfigMobileData(),
- canConfigWifi = viewModel.getCanConfigWifi(),
- coroutineScope = coroutineScope,
- context = context,
- )
- }
AndroidView(
modifier = Modifier.fillMaxSize(),
@@ -46,11 +34,11 @@
// Inflate with the existing dialog xml layout and bind it with the manager
val view =
LayoutInflater.from(context).inflate(R.layout.internet_connectivity_dialog, null)
- internetDetailsContentManager.bind(view)
+ viewModel.internetDetailsContentManager.bind(view, coroutineScope)
view
// TODO: b/377388104 - Polish the internet details view UI
},
- onRelease = { internetDetailsContentManager.unBind() },
+ onRelease = { viewModel.internetDetailsContentManager.unBind() },
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt
index 659488b..d8e1755 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt
@@ -43,6 +43,9 @@
import android.widget.TextView
import androidx.annotation.MainThread
import androidx.annotation.WorkerThread
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
@@ -79,8 +82,6 @@
private val internetDetailsContentController: InternetDetailsContentController,
@Assisted(CAN_CONFIG_MOBILE_DATA) private val canConfigMobileData: Boolean,
@Assisted(CAN_CONFIG_WIFI) private val canConfigWifi: Boolean,
- @Assisted private val coroutineScope: CoroutineScope,
- @Assisted private var context: Context,
private val uiEventLogger: UiEventLogger,
@Main private val handler: Handler,
@Background private val backgroundExecutor: Executor,
@@ -121,26 +122,29 @@
private lateinit var shareWifiButton: Button
private lateinit var airplaneModeButton: Button
private var alertDialog: AlertDialog? = null
-
- private val canChangeWifiState =
- WifiEnterpriseRestrictionUtils.isChangeWifiStateAllowed(context)
+ private var canChangeWifiState = false
private var wifiNetworkHeight = 0
private var backgroundOn: Drawable? = null
private var backgroundOff: Drawable? = null
private var clickJob: Job? = null
private var defaultDataSubId = internetDetailsContentController.defaultDataSubscriptionId
- @VisibleForTesting
- internal var adapter = InternetAdapter(internetDetailsContentController, coroutineScope)
+ @VisibleForTesting internal lateinit var adapter: InternetAdapter
@VisibleForTesting internal var wifiEntriesCount: Int = 0
@VisibleForTesting internal var hasMoreWifiEntries: Boolean = false
+ private lateinit var context: Context
+ private lateinit var coroutineScope: CoroutineScope
+
+ var title by mutableStateOf("")
+ private set
+
+ var subTitle by mutableStateOf("")
+ private set
@AssistedFactory
interface Factory {
fun create(
@Assisted(CAN_CONFIG_MOBILE_DATA) canConfigMobileData: Boolean,
@Assisted(CAN_CONFIG_WIFI) canConfigWifi: Boolean,
- coroutineScope: CoroutineScope,
- context: Context,
): InternetDetailsContentManager
}
@@ -152,12 +156,16 @@
*
* @param contentView The view to which the content manager should be bound.
*/
- fun bind(contentView: View) {
+ fun bind(contentView: View, coroutineScope: CoroutineScope) {
if (DEBUG) {
Log.d(TAG, "Bind InternetDetailsContentManager")
}
this.contentView = contentView
+ context = contentView.context
+ this.coroutineScope = coroutineScope
+ adapter = InternetAdapter(internetDetailsContentController, coroutineScope)
+ canChangeWifiState = WifiEnterpriseRestrictionUtils.isChangeWifiStateAllowed(context)
initializeLifecycle()
initializeViews()
@@ -323,11 +331,11 @@
}
}
- fun getTitleText(): String {
+ private fun getTitleText(): String {
return internetDetailsContentController.getDialogTitleText().toString()
}
- fun getSubtitleText(): String {
+ private fun getSubtitleText(): String {
return internetDetailsContentController.getSubtitleText(isProgressBarVisible).toString()
}
@@ -336,6 +344,13 @@
Log.d(TAG, "updateDetailsUI ")
}
+ if (!::context.isInitialized) {
+ return
+ }
+
+ title = getTitleText()
+ subTitle = getSubtitleText()
+
airplaneModeButton.visibility =
if (internetContent.isAirplaneModeEnabled) View.VISIBLE else View.GONE
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
index 6709fd2..fb63bea4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
@@ -28,9 +28,22 @@
@AssistedInject
constructor(
private val accessPointController: AccessPointController,
- val contentManagerFactory: InternetDetailsContentManager.Factory,
+ private val contentManagerFactory: InternetDetailsContentManager.Factory,
private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
-) : TileDetailsViewModel() {
+) : TileDetailsViewModel {
+ val internetDetailsContentManager by lazy {
+ contentManagerFactory.create(
+ canConfigMobileData = accessPointController.canConfigMobileData(),
+ canConfigWifi = accessPointController.canConfigWifi(),
+ )
+ }
+
+ override val title: String
+ get() = internetDetailsContentManager.title
+
+ override val subTitle: String
+ get() = internetDetailsContentManager.subTitle
+
override fun clickOnSettingsButton() {
qsTileIntentUserActionHandler.handle(
/* expandable= */ null,
@@ -38,30 +51,6 @@
)
}
- override fun getTitle(): String {
- // TODO: b/377388104 make title and sub title mutable states of string
- // by internetDetailsContentManager.getTitleText()
- // TODO: test title change between airplane mode and not airplane mode
- // TODO: b/377388104 Update the placeholder text
- return "Internet"
- }
-
- override fun getSubTitle(): String {
- // TODO: b/377388104 make title and sub title mutable states of string
- // by internetDetailsContentManager.getSubtitleText()
- // TODO: test subtitle change between airplane mode and not airplane mode
- // TODO: b/377388104 Update the placeholder text
- return "Tab a network to connect"
- }
-
- fun getCanConfigMobileData(): Boolean {
- return accessPointController.canConfigMobileData()
- }
-
- fun getCanConfigWifi(): Boolean {
- return accessPointController.canConfigWifi()
- }
-
@AssistedFactory
fun interface Factory {
fun create(): InternetDetailsViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt
index 9a39c3c..4f7e03b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt
@@ -23,18 +23,14 @@
class ModesDetailsViewModel(
private val onSettingsClick: () -> Unit,
val viewModel: ModesDialogViewModel,
-) : TileDetailsViewModel() {
+) : TileDetailsViewModel {
override fun clickOnSettingsButton() {
onSettingsClick()
}
- override fun getTitle(): String {
- // TODO(b/388321032): Replace this string with a string in a translatable xml file.
- return "Modes"
- }
+ // TODO(b/388321032): Replace this string with a string in a translatable xml file.
+ override val title = "Modes"
- override fun getSubTitle(): String {
- // TODO(b/388321032): Replace this string with a string in a translatable xml file.
- return "Silences interruptions from people and apps in different circumstances"
- }
+ // TODO(b/388321032): Replace this string with a string in a translatable xml file.
+ override val subTitle = "Silences interruptions from people and apps in different circumstances"
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt
index c84ddb6..59f209e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt
@@ -23,19 +23,15 @@
class ScreenRecordDetailsViewModel(
val recordingController: RecordingController,
val onStartRecordingClicked: Runnable,
-) : TileDetailsViewModel() {
+) : TileDetailsViewModel {
override fun clickOnSettingsButton() {
// No settings button in this tile.
}
- override fun getTitle(): String {
- // TODO(b/388321032): Replace this string with a string in a translatable xml file,
- return "Screen recording"
- }
+ // TODO(b/388321032): Replace this string with a string in a translatable xml file,
+ override val title = "Screen recording"
- override fun getSubTitle(): String {
- // No sub-title in this tile.
- return ""
- }
+ // No sub-title in this tile.
+ override val subTitle = ""
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index 3be2f1b..362b5db 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -17,6 +17,7 @@
package com.android.systemui.shade
import android.content.Context
+import android.content.res.Configuration
import android.graphics.Rect
import android.os.PowerManager
import android.os.SystemClock
@@ -25,11 +26,13 @@
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
+import android.view.WindowInsets
import android.widget.FrameLayout
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.OnBackPressedDispatcherOwner
import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
import androidx.compose.ui.platform.ComposeView
+import androidx.core.view.updateMargins
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleObserver
@@ -101,7 +104,10 @@
) : LifecycleOwner {
private val logger = Logger(logBuffer, TAG)
- private class CommunalWrapper(context: Context) : FrameLayout(context) {
+ private class CommunalWrapper(
+ context: Context,
+ private val communalSettingsInteractor: CommunalSettingsInteractor,
+ ) : FrameLayout(context) {
private val consumers: MutableSet<Consumer<Boolean>> = ArraySet()
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
@@ -121,6 +127,24 @@
consumers.clear()
}
}
+
+ override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets {
+ if (
+ !communalSettingsInteractor.isV2FlagEnabled() ||
+ resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
+ ) {
+ return super.onApplyWindowInsets(windowInsets)
+ }
+ val type = WindowInsets.Type.displayCutout()
+ val insets = windowInsets.getInsets(type)
+
+ // Reset horizontal margins added by window insets, so hub can be edge to edge.
+ if (insets.left > 0 || insets.right > 0) {
+ val lp = layoutParams as LayoutParams
+ lp.updateMargins(0, lp.topMargin, 0, lp.bottomMargin)
+ }
+ return WindowInsets.CONSUMED
+ }
}
/** The container view for the hub. This will not be initialized until [initView] is called. */
@@ -443,7 +467,8 @@
collectFlow(containerView, keyguardInteractor.isDreaming, { isDreaming = it })
collectFlow(containerView, communalViewModel.swipeToHubEnabled, { swipeToHubEnabled = it })
- communalContainerWrapper = CommunalWrapper(containerView.context)
+ communalContainerWrapper =
+ CommunalWrapper(containerView.context, communalSettingsInteractor)
communalContainerWrapper?.addView(communalContainerView)
logger.d("Hub container initialized")
return communalContainerWrapper!!
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index fa17b4f..dafb1a5 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -498,17 +498,18 @@
}
private boolean isExpanded(NotificationShadeWindowState state) {
+ boolean visForBlur = !Flags.disableShadeVisibleWithBlur() && state.backgroundBlurRadius > 0;
boolean isExpanded = !state.forceWindowCollapsed && (state.isKeyguardShowingAndNotOccluded()
|| state.panelVisible || state.keyguardFadingAway || state.bouncerShowing
|| state.headsUpNotificationShowing
|| state.scrimsVisibility != ScrimController.TRANSPARENT)
- || state.backgroundBlurRadius > 0
+ || visForBlur
|| state.launchingActivityFromNotification;
mLogger.logIsExpanded(isExpanded, state.forceWindowCollapsed,
state.isKeyguardShowingAndNotOccluded(), state.panelVisible,
state.keyguardFadingAway, state.bouncerShowing, state.headsUpNotificationShowing,
state.scrimsVisibility != ScrimController.TRANSPARENT,
- state.backgroundBlurRadius > 0, state.launchingActivityFromNotification);
+ visForBlur, state.launchingActivityFromNotification);
return isExpanded;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
index 4d68f2e..f653573 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
@@ -33,7 +33,8 @@
import com.android.systemui.statusbar.notification.icon.IconPack;
import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
-
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import kotlinx.coroutines.flow.MutableStateFlow;
@@ -51,10 +52,27 @@
// TODO (b/389839319): implement the row
private ExpandableNotificationRow mRow;
+ private final List<ListEntry> mChildren = new ArrayList<>();
+
+ private final List<ListEntry> mUnmodifiableChildren = Collections.unmodifiableList(mChildren);
+
public BundleEntry(String key) {
super(key);
}
+ void addChild(ListEntry child) {
+ mChildren.add(child);
+ }
+
+ @NonNull
+ public List<ListEntry> getChildren() {
+ return mUnmodifiableChildren;
+ }
+
+ /**
+ * @return Null because bundles do not have an associated NotificationEntry.
+ */
+
@Nullable
@Override
public NotificationEntry getRepresentativeEntry() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
index 3fad8f0..1f32b94 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
@@ -20,6 +20,7 @@
import android.annotation.Nullable;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.notification.collection.BundleEntry;
import com.android.systemui.statusbar.notification.collection.PipelineEntry;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -116,6 +117,9 @@
NotificationPriorityBucketKt.BUCKET_SILENT) {
@Override
public boolean isInSection(PipelineEntry entry) {
+ if (entry instanceof BundleEntry) {
+ return true;
+ }
return !mHighPriorityProvider.isHighPriority(entry)
&& entry.getRepresentativeEntry() != null
&& !entry.getRepresentativeEntry().isAmbient();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
index c2f0806..6b32c6a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
@@ -17,7 +17,6 @@
package com.android.systemui.statusbar.notification.collection.inflation;
import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE;
-import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC;
@@ -276,7 +275,7 @@
if (LockscreenOtpRedaction.isSingleLineViewEnabled()) {
if (inflaterParams.isChildInGroup()
- && redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) {
+ && redactionType != REDACTION_TYPE_NONE) {
params.requireContentViews(FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE);
} else {
params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index b481455..8da2f76 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -3054,6 +3054,9 @@
mUserLocked = userLocked;
mPrivateLayout.setUserExpanding(userLocked);
+ if (android.app.Flags.expandingPublicView()) {
+ mPublicLayout.setUserExpanding(userLocked);
+ }
// This is intentionally not guarded with mIsSummaryWithChildren since we might have had
// children but not anymore.
if (mChildrenContainer != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 1932037..676c96e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -293,8 +293,8 @@
useExactly = true;
}
int spec = MeasureSpec.makeMeasureSpec(size, useExactly
- ? MeasureSpec.EXACTLY
- : MeasureSpec.AT_MOST);
+ ? MeasureSpec.EXACTLY
+ : MeasureSpec.AT_MOST);
measureChildWithMargins(mExpandedChild, widthMeasureSpec, 0, spec, 0);
maxChildHeight = Math.max(maxChildHeight, mExpandedChild.getMeasuredHeight());
}
@@ -721,14 +721,14 @@
private int getMinContentHeightHint() {
if (mIsChildInGroup && isVisibleOrTransitioning(VISIBLE_TYPE_SINGLELINE)) {
return mContext.getResources().getDimensionPixelSize(
- com.android.internal.R.dimen.notification_action_list_height);
+ com.android.internal.R.dimen.notification_action_list_height);
}
// Transition between heads-up & expanded, or pinned.
if (mHeadsUpChild != null && mExpandedChild != null) {
boolean transitioningBetweenHunAndExpanded =
isTransitioningFromTo(VISIBLE_TYPE_HEADSUP, VISIBLE_TYPE_EXPANDED) ||
- isTransitioningFromTo(VISIBLE_TYPE_EXPANDED, VISIBLE_TYPE_HEADSUP);
+ isTransitioningFromTo(VISIBLE_TYPE_EXPANDED, VISIBLE_TYPE_HEADSUP);
boolean pinned = !isVisibleOrTransitioning(VISIBLE_TYPE_CONTRACTED)
&& (mIsHeadsUp || mHeadsUpAnimatingAway)
&& mContainingNotification.canShowHeadsUp();
@@ -758,7 +758,7 @@
} else if (mContractedChild != null) {
hint = getViewHeight(VISIBLE_TYPE_CONTRACTED)
+ mContext.getResources().getDimensionPixelSize(
- com.android.internal.R.dimen.notification_action_list_height);
+ com.android.internal.R.dimen.notification_action_list_height);
} else {
hint = getMinHeight();
}
@@ -1053,8 +1053,8 @@
// the original type
final int visibleType = (
isGroupExpanded() || mContainingNotification.isUserLocked())
- ? calculateVisibleType()
- : getVisibleType();
+ ? calculateVisibleType()
+ : getVisibleType();
return getBackgroundColor(visibleType);
}
@@ -1240,7 +1240,14 @@
height = mContentHeight;
}
int expandedVisualType = getVisualTypeForHeight(height);
- int collapsedVisualType = mIsChildInGroup && !isGroupExpanded()
+ final boolean shouldShowSingleLineView = mIsChildInGroup && !isGroupExpanded();
+ final boolean isSingleLineViewPresent = mSingleLineView != null;
+
+ if (shouldShowSingleLineView && !isSingleLineViewPresent) {
+ Log.wtf(TAG, "calculateVisibleType: SingleLineView is not available!");
+ }
+
+ final int collapsedVisualType = shouldShowSingleLineView && isSingleLineViewPresent
? VISIBLE_TYPE_SINGLELINE
: getVisualTypeForHeight(mContainingNotification.getCollapsedHeight());
return mTransformationStartVisibleType == collapsedVisualType
@@ -1261,7 +1268,13 @@
if (!noExpandedChild && viewHeight == getViewHeight(VISIBLE_TYPE_EXPANDED)) {
return VISIBLE_TYPE_EXPANDED;
}
- if (!mUserExpanding && mIsChildInGroup && !isGroupExpanded()) {
+ final boolean shouldShowSingleLineView = mIsChildInGroup && !isGroupExpanded();
+ final boolean isSingleLinePresent = mSingleLineView != null;
+ if (shouldShowSingleLineView && !isSingleLinePresent) {
+ Log.wtf(TAG, "getVisualTypeForHeight: singleLineView is not available.");
+ }
+
+ if (!mUserExpanding && shouldShowSingleLineView && isSingleLinePresent) {
return VISIBLE_TYPE_SINGLELINE;
}
@@ -1276,7 +1289,7 @@
if (noExpandedChild || (mContractedChild != null
&& viewHeight <= getViewHeight(VISIBLE_TYPE_CONTRACTED)
&& (!mIsChildInGroup || isGroupExpanded()
- || !mContainingNotification.isExpanded(true /* allowOnKeyguard */)))) {
+ || !mContainingNotification.isExpanded(true /* allowOnKeyguard */)))) {
return VISIBLE_TYPE_CONTRACTED;
} else if (!noExpandedChild) {
return VISIBLE_TYPE_EXPANDED;
@@ -1687,7 +1700,7 @@
: smartReplies.fromAssistant;
boolean editBeforeSending = smartReplies != null
&& mSmartReplyConstants.getEffectiveEditChoicesBeforeSending(
- smartReplies.remoteInput.getEditChoicesBeforeSending());
+ smartReplies.remoteInput.getEditChoicesBeforeSending());
mSmartReplyController.smartSuggestionsAdded(mNotificationEntry, numSmartReplies,
numSmartActions, fromAssistant, editBeforeSending);
@@ -2114,8 +2127,8 @@
public boolean shouldClipToRounding(boolean topRounded, boolean bottomRounded) {
boolean needsPaddings = shouldClipToRounding(getVisibleType(), topRounded, bottomRounded);
if (mUserExpanding) {
- needsPaddings |= shouldClipToRounding(mTransformationStartVisibleType, topRounded,
- bottomRounded);
+ needsPaddings |= shouldClipToRounding(mTransformationStartVisibleType, topRounded,
+ bottomRounded);
}
return needsPaddings;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index c2c271b..e6bb1b9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -64,7 +64,6 @@
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.MathUtils;
-import android.util.Pair;
import android.view.DisplayCutout;
import android.view.InputDevice;
import android.view.LayoutInflater;
@@ -141,7 +140,6 @@
import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
import com.android.systemui.statusbar.policy.ScrollAdapter;
import com.android.systemui.statusbar.policy.SplitShadeStateController;
-import com.android.systemui.statusbar.ui.SystemBarUtilsProxy;
import com.android.systemui.util.Assert;
import com.android.systemui.util.ColorUtilKt;
import com.android.systemui.util.DumpUtilsKt;
@@ -6004,6 +6002,7 @@
* LockscreenShadeTransitionController resets fraction to 0
* where it remains until the next lockscreen-to-shade transition.
*/
+ @Override
public void setFractionToShade(float fraction) {
mAmbientState.setFractionToShade(fraction);
updateContentHeight(); // Recompute stack height with different section gap.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index bb3abc1..f7f8acf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -1735,6 +1735,7 @@
* they remain until the next lockscreen-to-shade transition.
*/
public void setTransitionToFullShadeAmount(float fraction) {
+ SceneContainerFlag.assertInLegacyMode();
mView.setFractionToShade(fraction);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
index 9c855e9..ac89f3a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
@@ -107,6 +107,9 @@
/** sets the current expand fraction */
fun setExpandFraction(expandFraction: Float)
+ /** Sets the fraction of the LockScreen -> Shade transition. */
+ fun setFractionToShade(fraction: Float)
+
/** sets the current QS expand fraction */
fun setQsExpandFraction(expandFraction: Float)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
index 653344a..40739b3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
@@ -95,6 +95,11 @@
view.setExpandFraction(it.coerceIn(0f, 1f))
}
}
+ launch {
+ viewModel.lockScreenToShadeTransitionProgress.collectTraced {
+ view.setFractionToShade(it.coerceIn(0f, 1f))
+ }
+ }
launch { viewModel.qsExpandFraction.collectTraced { view.setQsExpandFraction(it) } }
launch { viewModel.blurRadius(maxBlurRadius).collect(view::setBlurRadius) }
launch {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index c1aa5f1..940b2e5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
@@ -207,6 +207,44 @@
val qsExpandFraction: Flow<Float> =
shadeInteractor.qsExpansion.dumpWhileCollecting("qsExpandFraction")
+ /**
+ * Fraction of the LockScreen -> Shade transition. 0..1 while the transition in progress, and
+ * snaps back to 0 when it is Idle.
+ */
+ val lockScreenToShadeTransitionProgress: Flow<Float> =
+ combine(
+ shadeInteractor.shadeExpansion,
+ shadeModeInteractor.shadeMode,
+ sceneInteractor.transitionState,
+ ) { shadeExpansion, _, transitionState ->
+ when (transitionState) {
+ is Idle -> 0f
+ is ChangeScene ->
+ if (
+ transitionState.isTransitioning(
+ from = Scenes.Lockscreen,
+ to = Scenes.Shade,
+ )
+ ) {
+ shadeExpansion
+ } else {
+ 0f
+ }
+
+ is Transition.OverlayTransition ->
+ if (
+ transitionState.currentScene == Scenes.Lockscreen &&
+ transitionState.isTransitioning(to = Overlays.NotificationsShade)
+ ) {
+ shadeExpansion
+ } else {
+ 0f
+ }
+ }
+ }
+ .distinctUntilChanged()
+ .dumpWhileCollecting("lockScreenToShadeTransitionProgress")
+
val isOccluded: Flow<Boolean> =
bouncerInteractor.bouncerExpansion
.map { it == 1f }
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt
index 3bd8af6..6657c42 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt
@@ -20,6 +20,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
@@ -31,7 +32,14 @@
constructor(
@Background val backgroundDispatcher: CoroutineDispatcher,
val userRepository: UserRepository,
+ val selectedUserInteractor: SelectedUserInteractor,
) {
+ /** Whether the current user is unlocked */
+ val currentUserUnlocked: Flow<Boolean> =
+ selectedUserInteractor.selectedUserInfo.flatMapLatestConflated { user ->
+ isUserUnlocked(user.userHandle)
+ }
+
fun isUserUnlocked(userHandle: UserHandle?): Flow<Boolean> =
userRepository.isUserUnlocked(userHandle).flowOn(backgroundDispatcher)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
index 845be02..60345a3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
@@ -24,12 +24,15 @@
import android.window.RemoteTransition
import android.window.TransitionFilter
import android.window.WindowAnimationState
+import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.activity.EmptyTestActivity
+import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
-import com.android.systemui.shared.Flags
+import com.android.systemui.shared.Flags as SharedFlags
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
import com.android.wm.shell.shared.ShellTransitions
@@ -43,7 +46,6 @@
import kotlin.test.assertEquals
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
-import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertThrows
import org.junit.Before
@@ -57,8 +59,8 @@
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
-import org.mockito.Spy
import org.mockito.junit.MockitoJUnit
+import org.mockito.kotlin.spy
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -66,21 +68,23 @@
@RunWithLooper
class ActivityTransitionAnimatorTest : SysuiTestCase() {
private val kosmos = testKosmos()
- private val transitionContainer = LinearLayout(mContext)
+
private val mainExecutor = context.mainExecutor
private val testTransitionAnimator = fakeTransitionAnimator(mainExecutor)
private val testShellTransitions = FakeShellTransitions()
+
+ private val Kosmos.underTest by Kosmos.Fixture { activityTransitionAnimator }
+
@Mock lateinit var callback: ActivityTransitionAnimator.Callback
@Mock lateinit var listener: ActivityTransitionAnimator.Listener
- @Spy private val controller = TestTransitionAnimatorController(transitionContainer)
@Mock lateinit var iCallback: IRemoteAnimationFinishedCallback
- private lateinit var underTest: ActivityTransitionAnimator
- @get:Rule val rule = MockitoJUnit.rule()
+ @get:Rule(order = 0) val mockitoRule = MockitoJUnit.rule()
+ @get:Rule(order = 1) val activityRule = ActivityScenarioRule(EmptyTestActivity::class.java)
@Before
fun setup() {
- underTest =
+ kosmos.activityTransitionAnimator =
ActivityTransitionAnimator(
mainExecutor,
ActivityTransitionAnimator.TransitionRegister.fromShellTransitions(
@@ -89,19 +93,20 @@
testTransitionAnimator,
testTransitionAnimator,
disableWmTimeout = true,
+ skipReparentTransaction = true,
)
- underTest.callback = callback
- underTest.addListener(listener)
+ kosmos.activityTransitionAnimator.callback = callback
+ kosmos.activityTransitionAnimator.addListener(listener)
}
@After
fun tearDown() {
- underTest.removeListener(listener)
+ kosmos.activityTransitionAnimator.removeListener(listener)
}
private fun startIntentWithAnimation(
- animator: ActivityTransitionAnimator = underTest,
- controller: ActivityTransitionAnimator.Controller? = this.controller,
+ controller: ActivityTransitionAnimator.Controller?,
+ animator: ActivityTransitionAnimator = kosmos.activityTransitionAnimator,
animate: Boolean = true,
intentStarter: (RemoteAnimationAdapter?) -> Int,
) {
@@ -119,129 +124,152 @@
@Test
fun animationAdapterIsNullIfControllerIsNull() {
- var startedIntent = false
- var animationAdapter: RemoteAnimationAdapter? = null
+ kosmos.runTest {
+ var startedIntent = false
+ var animationAdapter: RemoteAnimationAdapter? = null
- startIntentWithAnimation(controller = null) { adapter ->
- startedIntent = true
- animationAdapter = adapter
+ startIntentWithAnimation(controller = null) { adapter ->
+ startedIntent = true
+ animationAdapter = adapter
- ActivityManager.START_SUCCESS
+ ActivityManager.START_SUCCESS
+ }
+
+ assertTrue(startedIntent)
+ assertNull(animationAdapter)
}
-
- assertTrue(startedIntent)
- assertNull(animationAdapter)
}
@Test
fun animatesIfActivityOpens() {
- val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
- var animationAdapter: RemoteAnimationAdapter? = null
- startIntentWithAnimation { adapter ->
- animationAdapter = adapter
- ActivityManager.START_SUCCESS
- }
+ kosmos.runTest {
+ val controller = createController()
+ val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
+ var animationAdapter: RemoteAnimationAdapter? = null
+ startIntentWithAnimation(controller) { adapter ->
+ animationAdapter = adapter
+ ActivityManager.START_SUCCESS
+ }
- assertNotNull(animationAdapter)
- waitForIdleSync()
- verify(controller).onIntentStarted(willAnimateCaptor.capture())
- assertTrue(willAnimateCaptor.value)
+ assertNotNull(animationAdapter)
+ waitForIdleSync()
+ verify(controller).onIntentStarted(willAnimateCaptor.capture())
+ assertTrue(willAnimateCaptor.value)
+ }
}
@Test
fun doesNotAnimateIfActivityIsAlreadyOpen() {
- val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
- startIntentWithAnimation { ActivityManager.START_DELIVERED_TO_TOP }
+ kosmos.runTest {
+ val controller = createController()
+ val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
+ startIntentWithAnimation(controller) { ActivityManager.START_DELIVERED_TO_TOP }
- waitForIdleSync()
- verify(controller).onIntentStarted(willAnimateCaptor.capture())
- assertFalse(willAnimateCaptor.value)
+ waitForIdleSync()
+ verify(controller).onIntentStarted(willAnimateCaptor.capture())
+ assertFalse(willAnimateCaptor.value)
+ }
}
@Test
fun animatesIfActivityIsAlreadyOpenAndIsOnKeyguard() {
- `when`(callback.isOnKeyguard()).thenReturn(true)
+ kosmos.runTest {
+ `when`(callback.isOnKeyguard()).thenReturn(true)
- val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
- var animationAdapter: RemoteAnimationAdapter? = null
+ val controller = createController()
+ val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
+ var animationAdapter: RemoteAnimationAdapter? = null
- startIntentWithAnimation(underTest) { adapter ->
- animationAdapter = adapter
- ActivityManager.START_DELIVERED_TO_TOP
+ startIntentWithAnimation(controller, underTest) { adapter ->
+ animationAdapter = adapter
+ ActivityManager.START_DELIVERED_TO_TOP
+ }
+
+ waitForIdleSync()
+ verify(controller).onIntentStarted(willAnimateCaptor.capture())
+ verify(callback).hideKeyguardWithAnimation(any())
+
+ assertTrue(willAnimateCaptor.value)
+ assertNull(animationAdapter)
}
-
- waitForIdleSync()
- verify(controller).onIntentStarted(willAnimateCaptor.capture())
- verify(callback).hideKeyguardWithAnimation(any())
-
- assertTrue(willAnimateCaptor.value)
- assertNull(animationAdapter)
}
@Test
fun doesNotAnimateIfAnimateIsFalse() {
- val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
- startIntentWithAnimation(animate = false) { ActivityManager.START_SUCCESS }
+ kosmos.runTest {
+ val controller = createController()
+ val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
+ startIntentWithAnimation(controller, animate = false) { ActivityManager.START_SUCCESS }
- waitForIdleSync()
- verify(controller).onIntentStarted(willAnimateCaptor.capture())
- assertFalse(willAnimateCaptor.value)
+ waitForIdleSync()
+ verify(controller).onIntentStarted(willAnimateCaptor.capture())
+ assertFalse(willAnimateCaptor.value)
+ }
}
- @EnableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY)
+ @EnableFlags(SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY)
@Test
fun registersReturnIffCookieIsPresent() {
- `when`(callback.isOnKeyguard()).thenReturn(false)
+ kosmos.runTest {
+ `when`(callback.isOnKeyguard()).thenReturn(false)
- startIntentWithAnimation(underTest, controller) { ActivityManager.START_DELIVERED_TO_TOP }
-
- waitForIdleSync()
- assertTrue(testShellTransitions.remotes.isEmpty())
- assertTrue(testShellTransitions.remotesForTakeover.isEmpty())
-
- val controller =
- object : DelegateTransitionAnimatorController(controller) {
- override val transitionCookie
- get() = ActivityTransitionAnimator.TransitionCookie("testCookie")
+ val controller = createController()
+ startIntentWithAnimation(controller, underTest) {
+ ActivityManager.START_DELIVERED_TO_TOP
}
- startIntentWithAnimation(underTest, controller) { ActivityManager.START_DELIVERED_TO_TOP }
+ waitForIdleSync()
+ assertTrue(testShellTransitions.remotes.isEmpty())
+ assertTrue(testShellTransitions.remotesForTakeover.isEmpty())
- waitForIdleSync()
- assertEquals(1, testShellTransitions.remotes.size)
- assertTrue(testShellTransitions.remotesForTakeover.isEmpty())
+ val controllerWithCookie =
+ object : DelegateTransitionAnimatorController(controller) {
+ override val transitionCookie
+ get() = ActivityTransitionAnimator.TransitionCookie("testCookie")
+ }
+
+ startIntentWithAnimation(controllerWithCookie, underTest) {
+ ActivityManager.START_DELIVERED_TO_TOP
+ }
+
+ waitForIdleSync()
+ assertEquals(1, testShellTransitions.remotes.size)
+ assertTrue(testShellTransitions.remotesForTakeover.isEmpty())
+ }
}
@EnableFlags(
- Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
- Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
+ SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+ SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
)
@Test
fun registersLongLivedTransition() {
kosmos.runTest {
- var factory = controllerFactory()
+ val controller = createController()
+ var factory = controllerFactory(controller)
underTest.register(factory.cookie, factory, testScope)
assertEquals(2, testShellTransitions.remotes.size)
- factory = controllerFactory()
+ factory = controllerFactory(controller)
underTest.register(factory.cookie, factory, testScope)
assertEquals(4, testShellTransitions.remotes.size)
}
}
@EnableFlags(
- Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
- Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
+ SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+ SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
)
@Test
fun registersLongLivedTransitionOverridingPreviousRegistration() {
kosmos.runTest {
+ val controller = createController()
val cookie = ActivityTransitionAnimator.TransitionCookie("test_cookie")
- var factory = controllerFactory(cookie)
+ var factory = controllerFactory(controller, cookie)
underTest.register(cookie, factory, testScope)
val transitions = testShellTransitions.remotes.values.toList()
- factory = controllerFactory(cookie)
+ factory = controllerFactory(controller, cookie)
underTest.register(cookie, factory, testScope)
assertEquals(2, testShellTransitions.remotes.size)
for (transition in transitions) {
@@ -250,23 +278,25 @@
}
}
- @DisableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED)
+ @DisableFlags(SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED)
@Test
fun doesNotRegisterLongLivedTransitionIfFlagIsDisabled() {
kosmos.runTest {
- val factory = controllerFactory(component = null)
+ val factory = controllerFactory(createController(), component = null)
assertThrows(IllegalStateException::class.java) {
underTest.register(factory.cookie, factory, testScope)
}
}
}
- @EnableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED)
+ @EnableFlags(SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED)
@Test
fun doesNotRegisterLongLivedTransitionIfMissingRequiredProperties() {
kosmos.runTest {
+ val controller = createController()
+
// No ComponentName
- var factory = controllerFactory(component = null)
+ var factory = controllerFactory(controller, component = null)
assertThrows(IllegalStateException::class.java) {
underTest.register(factory.cookie, factory, testScope)
}
@@ -280,7 +310,7 @@
testTransitionAnimator,
disableWmTimeout = true,
)
- factory = controllerFactory()
+ factory = controllerFactory(controller)
assertThrows(IllegalStateException::class.java) {
activityTransitionAnimator.register(factory.cookie, factory, testScope)
}
@@ -288,17 +318,18 @@
}
@EnableFlags(
- Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
- Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
+ SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+ SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
)
@Test
fun unregistersLongLivedTransition() {
kosmos.runTest {
+ val controller = createController()
val cookies = arrayOfNulls<ActivityTransitionAnimator.TransitionCookie>(3)
for (index in 0 until 3) {
cookies[index] = mock(ActivityTransitionAnimator.TransitionCookie::class.java)
- val factory = controllerFactory(cookies[index]!!)
+ val factory = controllerFactory(controller, cookies[index]!!)
underTest.register(factory.cookie, factory, testScope)
}
@@ -315,75 +346,98 @@
@Test
fun doesNotStartIfAnimationIsCancelled() {
- val runner = underTest.createEphemeralRunner(controller)
- runner.onAnimationCancelled()
- runner.onAnimationStart(TRANSIT_NONE, emptyArray(), emptyArray(), emptyArray(), iCallback)
+ kosmos.runTest {
+ val controller = createController()
+ val runner = underTest.createEphemeralRunner(controller)
+ runner.onAnimationCancelled()
+ runner.onAnimationStart(
+ TRANSIT_NONE,
+ emptyArray(),
+ emptyArray(),
+ emptyArray(),
+ iCallback,
+ )
- waitForIdleSync()
- verify(controller).onTransitionAnimationCancelled()
- verify(controller, never()).onTransitionAnimationStart(anyBoolean())
- verify(listener).onTransitionAnimationCancelled()
- verify(listener, never()).onTransitionAnimationStart()
- assertNull(runner.delegate)
+ waitForIdleSync()
+ verify(controller).onTransitionAnimationCancelled()
+ verify(controller, never()).onTransitionAnimationStart(anyBoolean())
+ verify(listener).onTransitionAnimationCancelled()
+ verify(listener, never()).onTransitionAnimationStart()
+ assertNull(runner.delegate)
+ }
}
@Test
fun cancelsIfNoOpeningWindowIsFound() {
- val runner = underTest.createEphemeralRunner(controller)
- runner.onAnimationStart(TRANSIT_NONE, emptyArray(), emptyArray(), emptyArray(), iCallback)
+ kosmos.runTest {
+ val controller = createController()
+ val runner = underTest.createEphemeralRunner(controller)
+ runner.onAnimationStart(
+ TRANSIT_NONE,
+ emptyArray(),
+ emptyArray(),
+ emptyArray(),
+ iCallback,
+ )
- waitForIdleSync()
- verify(controller).onTransitionAnimationCancelled()
- verify(controller, never()).onTransitionAnimationStart(anyBoolean())
- verify(listener).onTransitionAnimationCancelled()
- verify(listener, never()).onTransitionAnimationStart()
- assertNull(runner.delegate)
+ waitForIdleSync()
+ verify(controller).onTransitionAnimationCancelled()
+ verify(controller, never()).onTransitionAnimationStart(anyBoolean())
+ verify(listener).onTransitionAnimationCancelled()
+ verify(listener, never()).onTransitionAnimationStart()
+ assertNull(runner.delegate)
+ }
}
@Test
fun startsAnimationIfWindowIsOpening() {
- val runner = underTest.createEphemeralRunner(controller)
- runner.onAnimationStart(
- TRANSIT_NONE,
- arrayOf(fakeWindow()),
- emptyArray(),
- emptyArray(),
- iCallback,
- )
- waitForIdleSync()
- verify(listener).onTransitionAnimationStart()
- verify(controller).onTransitionAnimationStart(anyBoolean())
+ kosmos.runTest {
+ val controller = createController()
+ val runner = underTest.createEphemeralRunner(controller)
+ runner.onAnimationStart(
+ TRANSIT_NONE,
+ arrayOf(fakeWindow()),
+ emptyArray(),
+ emptyArray(),
+ iCallback,
+ )
+ waitForIdleSync()
+ verify(listener).onTransitionAnimationStart()
+ verify(controller).onTransitionAnimationStart(anyBoolean())
+ }
}
@Test
fun creatingControllerFromNormalViewThrows() {
- assertThrows(IllegalArgumentException::class.java) {
- ActivityTransitionAnimator.Controller.fromView(FrameLayout(mContext))
+ kosmos.runTest {
+ assertThrows(IllegalArgumentException::class.java) {
+ ActivityTransitionAnimator.Controller.fromView(FrameLayout(mContext))
+ }
}
}
@DisableFlags(
- Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
- Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
+ SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+ SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
)
@Test
fun creatingRunnerWithLazyInitializationThrows_whenTheFlagsAreDisabled() {
kosmos.runTest {
assertThrows(IllegalStateException::class.java) {
- val factory = controllerFactory()
+ val factory = controllerFactory(createController())
underTest.createLongLivedRunner(factory, testScope, forLaunch = true)
}
}
}
@EnableFlags(
- Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
- Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
+ SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+ SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
)
@Test
fun runnerCreatesDelegateLazily_onAnimationStart() {
kosmos.runTest {
- val factory = controllerFactory()
+ val factory = controllerFactory(createController())
val runner = underTest.createLongLivedRunner(factory, testScope, forLaunch = true)
assertNull(runner.delegate)
@@ -412,13 +466,13 @@
}
@EnableFlags(
- Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
- Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
+ SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+ SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
)
@Test
fun runnerCreatesDelegateLazily_onAnimationTakeover() {
kosmos.runTest {
- val factory = controllerFactory()
+ val factory = controllerFactory(createController())
val runner = underTest.createLongLivedRunner(factory, testScope, forLaunch = false)
assertNull(runner.delegate)
@@ -446,58 +500,78 @@
}
@DisableFlags(
- Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
- Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
+ SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+ SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
)
@Test
fun animationTakeoverThrows_whenTheFlagsAreDisabled() {
- val runner = underTest.createEphemeralRunner(controller)
- assertThrows(IllegalStateException::class.java) {
- runner.takeOverAnimation(
- arrayOf(fakeWindow()),
- emptyArray(),
- SurfaceControl.Transaction(),
- iCallback,
- )
+ kosmos.runTest {
+ val controller = createController()
+ val runner = underTest.createEphemeralRunner(controller)
+ assertThrows(IllegalStateException::class.java) {
+ runner.takeOverAnimation(
+ arrayOf(fakeWindow()),
+ emptyArray(),
+ SurfaceControl.Transaction(),
+ iCallback,
+ )
+ }
}
}
@DisableFlags(
- Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
- Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
+ SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+ SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
)
@Test
fun disposeRunner_delegateDereferenced() {
- val runner = underTest.createEphemeralRunner(controller)
- assertNotNull(runner.delegate)
- runner.dispose()
- waitForIdleSync()
- assertNull(runner.delegate)
+ kosmos.runTest {
+ val controller = createController()
+ val runner = underTest.createEphemeralRunner(controller)
+ assertNotNull(runner.delegate)
+ runner.dispose()
+ waitForIdleSync()
+ assertNull(runner.delegate)
+ }
}
@Test
fun concurrentListenerModification_doesNotThrow() {
- // Need a second listener to trigger the concurrent modification.
- underTest.addListener(object : ActivityTransitionAnimator.Listener {})
- `when`(listener.onTransitionAnimationStart()).thenAnswer {
- underTest.removeListener(listener)
- listener
+ kosmos.runTest {
+ // Need a second listener to trigger the concurrent modification.
+ underTest.addListener(object : ActivityTransitionAnimator.Listener {})
+ `when`(listener.onTransitionAnimationStart()).thenAnswer {
+ underTest.removeListener(listener)
+ listener
+ }
+
+ val controller = createController()
+ val runner = underTest.createEphemeralRunner(controller)
+ runner.onAnimationStart(
+ TRANSIT_NONE,
+ arrayOf(fakeWindow()),
+ emptyArray(),
+ emptyArray(),
+ iCallback,
+ )
+
+ waitForIdleSync()
+ verify(listener).onTransitionAnimationStart()
}
+ }
- val runner = underTest.createEphemeralRunner(controller)
- runner.onAnimationStart(
- TRANSIT_NONE,
- arrayOf(fakeWindow()),
- emptyArray(),
- emptyArray(),
- iCallback,
- )
-
+ private fun createController(): TestTransitionAnimatorController {
+ lateinit var transitionContainer: ViewGroup
+ activityRule.scenario.onActivity { activity ->
+ transitionContainer = LinearLayout(activity)
+ activity.setContentView(transitionContainer)
+ }
waitForIdleSync()
- verify(listener).onTransitionAnimationStart()
+ return spy(TestTransitionAnimatorController(transitionContainer))
}
private fun controllerFactory(
+ controller: ActivityTransitionAnimator.Controller,
cookie: ActivityTransitionAnimator.TransitionCookie =
mock(ActivityTransitionAnimator.TransitionCookie::class.java),
component: ComponentName? = mock(ComponentName::class.java),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt
index b274f1c..d9c59d3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt
@@ -88,11 +88,6 @@
companion object {
private val INTENT = Intent("some.intent.action")
- private val DRAWABLE =
- mock<Icon> {
- whenever(this.contentDescription)
- .thenReturn(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID))
- }
private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337
@Parameters(
@@ -337,7 +332,17 @@
homeControls.setState(
lockScreenState =
- KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = DRAWABLE)
+ KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+ icon =
+ mock<Icon> {
+ whenever(contentDescription)
+ .thenReturn(
+ ContentDescription.Resource(
+ res = CONTENT_DESCRIPTION_RESOURCE_ID
+ )
+ )
+ }
+ )
)
homeControls.onTriggeredResult =
if (startActivity) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
index 175e8d4..e048d46 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
@@ -33,8 +33,8 @@
import android.platform.test.annotations.RequiresFlagsDisabled
import android.platform.test.annotations.RequiresFlagsEnabled
import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import android.platform.test.flag.junit.FlagsParameterization
import android.testing.TestableLooper
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
import com.android.settingslib.bluetooth.LocalBluetoothManager
@@ -77,6 +77,8 @@
import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
import org.mockito.kotlin.eq
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
private const val KEY = "TEST_KEY"
private const val KEY_OLD = "TEST_KEY_OLD"
@@ -89,12 +91,24 @@
private const val NORMAL_APP_NAME = "NORMAL_APP_NAME"
@SmallTest
-@RunWith(AndroidJUnit4::class)
+@RunWith(ParameterizedAndroidJunit4::class)
@TestableLooper.RunWithLooper
-public class MediaDeviceManagerTest : SysuiTestCase() {
+public class MediaDeviceManagerTest(flags: FlagsParameterization) : SysuiTestCase() {
- private companion object {
+ companion object {
val OTHER_DEVICE_ICON_STUB = TestStubDrawable()
+
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.progressionOf(
+ com.android.systemui.Flags.FLAG_MEDIA_CONTROLS_DEVICE_MANAGER_BACKGROUND_EXECUTION
+ )
+ }
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
}
@get:Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
@@ -187,6 +201,8 @@
@Test
fun loadMediaData() {
manager.onMediaDataLoaded(KEY, null, mediaData)
+ fakeBgExecutor.runAllReady()
+ fakeFgExecutor.runAllReady()
verify(lmmFactory).create(PACKAGE)
}
@@ -195,6 +211,7 @@
manager.onMediaDataLoaded(KEY, null, mediaData)
manager.onMediaDataRemoved(KEY, false)
fakeBgExecutor.runAllReady()
+ fakeFgExecutor.runAllReady()
verify(lmm).unregisterCallback(any())
verify(muteAwaitManager).stopListening()
}
@@ -406,6 +423,8 @@
manager.onMediaDataLoaded(KEY, null, mediaData)
// WHEN the notification is removed
manager.onMediaDataRemoved(KEY, true)
+ fakeBgExecutor.runAllReady()
+ fakeFgExecutor.runAllReady()
// THEN the listener receives key removed event
verify(listener).onKeyRemoved(eq(KEY), eq(true))
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt
index c20a801..a8bfbd1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt
@@ -103,6 +103,8 @@
whenever(internetWifiEntry.hasInternetAccess()).thenReturn(true)
whenever(wifiEntries.size).thenReturn(1)
whenever(internetDetailsContentController.getDialogTitleText()).thenReturn(TITLE)
+ whenever(internetDetailsContentController.getSubtitleText(ArgumentMatchers.anyBoolean()))
+ .thenReturn("")
whenever(internetDetailsContentController.getMobileNetworkTitle(ArgumentMatchers.anyInt()))
.thenReturn(MOBILE_NETWORK_TITLE)
whenever(
@@ -128,15 +130,13 @@
internetDetailsContentController,
canConfigMobileData = true,
canConfigWifi = true,
- coroutineScope = scope,
- context = mContext,
uiEventLogger = mock<UiEventLogger>(),
handler = handler,
backgroundExecutor = bgExecutor,
keyguard = keyguard,
)
- internetDetailsContentManager.bind(contentView)
+ internetDetailsContentManager.bind(contentView, scope)
internetDetailsContentManager.adapter = internetAdapter
internetDetailsContentManager.connectedWifiEntry = internetWifiEntry
internetDetailsContentManager.wifiEntriesCount = wifiEntries.size
@@ -777,6 +777,26 @@
}
}
+ @Test
+ fun updateTitleAndSubtitle() {
+ assertThat(internetDetailsContentManager.title).isEqualTo("Internet")
+ assertThat(internetDetailsContentManager.subTitle).isEqualTo("")
+
+ whenever(internetDetailsContentController.getDialogTitleText()).thenReturn("New title")
+ whenever(internetDetailsContentController.getSubtitleText(ArgumentMatchers.anyBoolean()))
+ .thenReturn("New subtitle")
+
+ internetDetailsContentManager.updateContent(true)
+ bgExecutor.runAllReady()
+
+ internetDetailsContentManager.internetContentData.observe(
+ internetDetailsContentManager.lifecycleOwner!!
+ ) {
+ assertThat(internetDetailsContentManager.title).isEqualTo("New title")
+ assertThat(internetDetailsContentManager.subTitle).isEqualTo("New subtitle")
+ }
+ }
+
companion object {
private const val TITLE = "Internet"
private const val MOBILE_NETWORK_TITLE = "Mobile Title"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ringtone/RingtonePlayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/ringtone/RingtonePlayerTest.java
new file mode 100644
index 0000000..c231be1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/ringtone/RingtonePlayerTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2025 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.ringtone;
+
+import static org.junit.Assert.assertThrows;
+
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.UserHandle;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RingtonePlayerTest extends SysuiTestCase {
+
+ private AudioManager mAudioManager;
+
+ private static final String TAG = "RingtonePlayerTest";
+
+ @Before
+ public void setup() throws Exception {
+ mAudioManager = getContext().getSystemService(AudioManager.class);
+ }
+
+ @Test
+ public void testRingtonePlayerUriUserCheck() {
+ android.media.IRingtonePlayer irp = mAudioManager.getRingtonePlayer();
+ final AudioAttributes aa = new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE).build();
+ // get a UserId that doesn't belong to mine
+ final int otherUserId = UserHandle.myUserId() == 0 ? 10 : 0;
+ // build a URI that I shouldn't have access to
+ final Uri uri = new Uri.Builder()
+ .scheme("content").authority(otherUserId + "@media")
+ .appendPath("external").appendPath("downloads")
+ .appendPath("bogusPathThatDoesNotMatter.mp3")
+ .build();
+ if (android.media.audio.Flags.ringtoneUserUriCheck()) {
+ assertThrows(SecurityException.class, () ->
+ irp.play(new Binder(), uri, aa, 1.0f /*volume*/, false /*looping*/)
+ );
+
+ assertThrows(SecurityException.class, () ->
+ irp.getTitle(uri));
+ }
+ }
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index a978ecd..8de931a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -204,8 +204,6 @@
import com.android.wm.shell.taskview.TaskViewTransitions;
import com.android.wm.shell.transition.Transitions;
-import kotlin.Lazy;
-
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -216,9 +214,6 @@
import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
-import platform.test.runner.parameterized.Parameters;
-
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -227,6 +222,10 @@
import java.util.Optional;
import java.util.concurrent.Executor;
+import kotlin.Lazy;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
@SmallTest
@RunWith(ParameterizedAndroidJunit4.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@@ -602,14 +601,19 @@
// Get a reference to KeyguardStateController.Callback
verify(mKeyguardStateController, atLeastOnce())
.addCallback(mKeyguardStateControllerCallbackCaptor.capture());
+
+ // Make sure mocks are set up for current user
+ switchUser(ActivityManager.getCurrentUser());
}
@After
public void tearDown() throws Exception {
- ArrayList<Bubble> bubbles = new ArrayList<>(mBubbleData.getBubbles());
- for (int i = 0; i < bubbles.size(); i++) {
- mBubbleController.removeBubble(bubbles.get(i).getKey(),
- Bubbles.DISMISS_NO_LONGER_BUBBLE);
+ if (mBubbleData != null) {
+ ArrayList<Bubble> bubbles = new ArrayList<>(mBubbleData.getBubbles());
+ for (int i = 0; i < bubbles.size(); i++) {
+ mBubbleController.removeBubble(bubbles.get(i).getKey(),
+ Bubbles.DISMISS_NO_LONGER_BUBBLE);
+ }
}
mTestableLooper.processAllMessages();
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryKosmos.kt
new file mode 100644
index 0000000..130c298
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2025 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.communal.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.carProjectionRepository by
+ Kosmos.Fixture<CarProjectionRepository> { FakeCarProjectionRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCarProjectionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCarProjectionRepository.kt
new file mode 100644
index 0000000..4042342
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCarProjectionRepository.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2025 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.communal.data.repository
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeCarProjectionRepository : CarProjectionRepository {
+ private val _projectionActive = MutableStateFlow(false)
+ override val projectionActive: Flow<Boolean> = _projectionActive.asStateFlow()
+
+ override suspend fun isProjectionActive(): Boolean {
+ return _projectionActive.value
+ }
+
+ fun setProjectionActive(active: Boolean) {
+ _projectionActive.value = active
+ }
+}
+
+val CarProjectionRepository.fake
+ get() = this as FakeCarProjectionRepository
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorKosmos.kt
new file mode 100644
index 0000000..23bbe36
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2025 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.communal.domain.interactor
+
+import com.android.systemui.communal.data.repository.carProjectionRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+
+val Kosmos.carProjectionInteractor by Fixture { CarProjectionInteractor(carProjectionRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorKosmos.kt
new file mode 100644
index 0000000..5735cf8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorKosmos.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2025 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.communal.domain.interactor
+
+import com.android.systemui.common.domain.interactor.batteryInteractor
+import com.android.systemui.communal.posturing.domain.interactor.posturingInteractor
+import com.android.systemui.dock.dockManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.backgroundCoroutineContext
+
+val Kosmos.communalAutoOpenInteractor by Fixture {
+ CommunalAutoOpenInteractor(
+ communalSettingsInteractor = communalSettingsInteractor,
+ backgroundContext = backgroundCoroutineContext,
+ batteryInteractor = batteryInteractor,
+ posturingInteractor = posturingInteractor,
+ dockManager = dockManager,
+ allowSwipeAlways = false,
+ )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
index b8b2ec5..316fcbb 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
@@ -16,17 +16,14 @@
package com.android.systemui.communal.domain.interactor
-import android.content.pm.UserInfo
import android.content.testableContext
import android.os.userManager
import com.android.systemui.broadcast.broadcastDispatcher
-import com.android.systemui.common.domain.interactor.batteryInteractor
+import com.android.systemui.communal.data.model.SuppressionReason
import com.android.systemui.communal.data.repository.communalMediaRepository
import com.android.systemui.communal.data.repository.communalSmartspaceRepository
import com.android.systemui.communal.data.repository.communalWidgetRepository
-import com.android.systemui.communal.posturing.domain.interactor.posturingInteractor
import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
-import com.android.systemui.dock.dockManager
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
@@ -39,13 +36,9 @@
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.plugins.activityStarter
-import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.settings.userTracker
import com.android.systemui.statusbar.phone.fakeManagedProfileController
-import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.user.data.repository.fakeUserRepository
-import com.android.systemui.user.domain.interactor.userLockedInteractor
import com.android.systemui.util.mockito.mock
val Kosmos.communalInteractor by Fixture {
@@ -70,10 +63,6 @@
logBuffer = logcatLogBuffer("CommunalInteractor"),
tableLogBuffer = mock(),
managedProfileController = fakeManagedProfileController,
- batteryInteractor = batteryInteractor,
- dockManager = dockManager,
- posturingInteractor = posturingInteractor,
- userLockedInteractor = userLockedInteractor,
)
}
@@ -86,28 +75,28 @@
)
}
-suspend fun Kosmos.setCommunalEnabled(enabled: Boolean): UserInfo {
+fun Kosmos.setCommunalEnabled(enabled: Boolean) {
fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, enabled)
- return if (enabled) {
- fakeUserRepository.asMainUser()
- } else {
- fakeUserRepository.asDefaultUser()
- }
+ val suppressionReasons =
+ if (enabled) {
+ emptyList()
+ } else {
+ listOf(SuppressionReason.ReasonUnknown())
+ }
+ communalSettingsInteractor.setSuppressionReasons(suppressionReasons)
}
-suspend fun Kosmos.setCommunalV2Enabled(enabled: Boolean): UserInfo {
+fun Kosmos.setCommunalV2Enabled(enabled: Boolean) {
setCommunalV2ConfigEnabled(enabled)
return setCommunalEnabled(enabled)
}
-suspend fun Kosmos.setCommunalAvailable(available: Boolean): UserInfo {
- val user = setCommunalEnabled(available)
+fun Kosmos.setCommunalAvailable(available: Boolean) {
+ setCommunalEnabled(available)
fakeKeyguardRepository.setKeyguardShowing(available)
- fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, available)
- return user
}
-suspend fun Kosmos.setCommunalV2Available(available: Boolean): UserInfo {
+fun Kosmos.setCommunalV2Available(available: Boolean) {
setCommunalV2ConfigEnabled(available)
- return setCommunalAvailable(available)
+ setCommunalAvailable(available)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorKosmos.kt
index fb983f7..d2fbb51 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorKosmos.kt
@@ -24,7 +24,6 @@
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.settings.userTracker
import com.android.systemui.user.domain.interactor.selectedUserInteractor
-import com.android.systemui.util.mockito.mock
val Kosmos.communalSettingsInteractor by Fixture {
CommunalSettingsInteractor(
@@ -34,6 +33,5 @@
repository = communalSettingsRepository,
userInteractor = selectedUserInteractor,
userTracker = userTracker,
- tableLogBuffer = mock(),
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.java b/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.java
deleted file mode 100644
index b99310b..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (C) 2018 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.dock;
-
-/**
- * A rudimentary fake for DockManager.
- */
-public class DockManagerFake implements DockManager {
- DockEventListener mCallback;
- AlignmentStateListener mAlignmentListener;
- private boolean mDocked;
-
- @Override
- public void addListener(DockEventListener callback) {
- this.mCallback = callback;
- }
-
- @Override
- public void removeListener(DockEventListener callback) {
- this.mCallback = null;
- }
-
- @Override
- public void addAlignmentStateListener(AlignmentStateListener listener) {
- mAlignmentListener = listener;
- }
-
- @Override
- public void removeAlignmentStateListener(AlignmentStateListener listener) {
- mAlignmentListener = listener;
- }
-
- @Override
- public boolean isDocked() {
- return mDocked;
- }
-
- /** Sets the docked state */
- public void setIsDocked(boolean docked) {
- mDocked = docked;
- }
-
- @Override
- public boolean isHidden() {
- return false;
- }
-
- /** Notifies callbacks of dock state change */
- public void setDockEvent(int event) {
- mCallback.onEvent(event);
- }
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.kt
new file mode 100644
index 0000000..6a43c40
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2025 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.dock
+
+import com.android.systemui.dock.DockManager.AlignmentStateListener
+
+/** A rudimentary fake for DockManager. */
+class DockManagerFake : DockManager {
+ private val callbacks = mutableSetOf<DockManager.DockEventListener>()
+ private val alignmentListeners = mutableSetOf<AlignmentStateListener>()
+ private var docked = false
+
+ override fun addListener(callback: DockManager.DockEventListener) {
+ callbacks.add(callback)
+ }
+
+ override fun removeListener(callback: DockManager.DockEventListener) {
+ callbacks.remove(callback)
+ }
+
+ override fun addAlignmentStateListener(listener: AlignmentStateListener) {
+ alignmentListeners.add(listener)
+ }
+
+ override fun removeAlignmentStateListener(listener: AlignmentStateListener) {
+ alignmentListeners.remove(listener)
+ }
+
+ override fun isDocked(): Boolean {
+ return docked
+ }
+
+ /** Sets the docked state */
+ fun setIsDocked(docked: Boolean) {
+ this.docked = docked
+ }
+
+ override fun isHidden(): Boolean {
+ return false
+ }
+
+ /** Notifies callbacks of dock state change */
+ fun setDockEvent(event: Int) {
+ for (callback in callbacks) {
+ callback.onEvent(event)
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt
index bdfa875..9b0a983 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt
@@ -16,7 +16,6 @@
package com.android.systemui.keyguard.domain.interactor
-import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.communal.domain.interactor.communalSceneInteractor
import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.deviceentry.data.repository.deviceEntryRepository
@@ -43,6 +42,5 @@
wakeToGoneInteractor = keyguardWakeDirectlyToGoneInteractor,
communalSettingsInteractor = communalSettingsInteractor,
communalSceneInteractor = communalSceneInteractor,
- communalInteractor = communalInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
index 985044c..511bede 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
@@ -16,7 +16,6 @@
package com.android.systemui.keyguard.domain.interactor
-import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.communal.domain.interactor.communalSceneInteractor
import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
@@ -42,7 +41,6 @@
communalSettingsInteractor = communalSettingsInteractor,
swipeToDismissInteractor = swipeToDismissInteractor,
keyguardOcclusionInteractor = keyguardOcclusionInteractor,
- communalInteractor = communalInteractor,
communalSceneInteractor = communalSceneInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
index 255a780..1130592 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.domain.interactor
import android.content.applicationContext
+import android.os.powerManager
import android.view.accessibility.accessibilityManagerWrapper
import com.android.internal.logging.uiEventLogger
import com.android.systemui.broadcast.broadcastDispatcher
@@ -26,6 +27,8 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.shade.pulsingGestureListener
+import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository
+import com.android.systemui.util.time.fakeSystemClock
val Kosmos.keyguardTouchHandlingInteractor by
Kosmos.Fixture {
@@ -40,5 +43,8 @@
accessibilityManager = accessibilityManagerWrapper,
pulsingGestureListener = pulsingGestureListener,
faceAuthInteractor = deviceEntryFaceAuthInteractor,
+ secureSettingsRepository = userAwareSecureSettingsRepository,
+ powerManager = powerManager,
+ systemClock = fakeSystemClock,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt
index 4f8d5a1..9457de1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt
@@ -18,18 +18,14 @@
import com.android.systemui.plugins.qs.TileDetailsViewModel
-class FakeTileDetailsViewModel(var tileSpec: String?) : TileDetailsViewModel() {
+class FakeTileDetailsViewModel(var tileSpec: String?) : TileDetailsViewModel {
private var _clickOnSettingsButton = 0
override fun clickOnSettingsButton() {
_clickOnSettingsButton++
}
- override fun getTitle(): String {
- return tileSpec ?: " Fake title"
- }
+ override val title = tileSpec ?: " Fake title"
- override fun getSubTitle(): String {
- return tileSpec ?: "Fake sub title"
- }
+ override val subTitle = tileSpec ?: "Fake sub title"
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt
index 75ca311..4aa4a2b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt
@@ -18,6 +18,7 @@
import android.content.applicationContext
import com.android.systemui.classifier.domain.interactor.falsingInteractor
+import com.android.systemui.development.ui.viewmodel.buildNumberViewModelFactory
import com.android.systemui.globalactions.globalActionsDialogLite
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.qs.footerActionsInteractor
@@ -29,6 +30,7 @@
override fun create(): ToolbarViewModel {
return ToolbarViewModel(
editModeButtonViewModelFactory,
+ buildNumberViewModelFactory,
footerActionsInteractor,
{ globalActionsDialogLite },
falsingInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt
index 933c351..6bb908a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt
@@ -22,5 +22,9 @@
val Kosmos.userLockedInteractor by
Kosmos.Fixture {
- UserLockedInteractor(backgroundDispatcher = testDispatcher, userRepository = userRepository)
+ UserLockedInteractor(
+ backgroundDispatcher = testDispatcher,
+ userRepository = userRepository,
+ selectedUserInteractor = selectedUserInteractor,
+ )
}
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index ccbc46f..5424ac3b 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -16,7 +16,7 @@
srcs: [
"texts/ravenwood-common-policies.txt",
],
- visibility: ["//visibility:private"],
+ visibility: [":__subpackages__"],
}
filegroup {
@@ -44,6 +44,22 @@
}
filegroup {
+ name: "ravenwood-standard-annotations",
+ srcs: [
+ "texts/ravenwood-standard-annotations.txt",
+ ],
+ visibility: [":__subpackages__"],
+}
+
+filegroup {
+ name: "ravenizer-standard-options",
+ srcs: [
+ "texts/ravenizer-standard-options.txt",
+ ],
+ visibility: [":__subpackages__"],
+}
+
+filegroup {
name: "ravenwood-annotation-allowed-classes",
srcs: [
"texts/ravenwood-annotation-allowed-classes.txt",
diff --git a/ravenwood/Framework.bp b/ravenwood/Framework.bp
index e366771..f5b075b 100644
--- a/ravenwood/Framework.bp
+++ b/ravenwood/Framework.bp
@@ -33,6 +33,7 @@
":ravenwood-common-policies",
":ravenwood-framework-policies",
":ravenwood-standard-options",
+ ":ravenwood-standard-annotations",
":ravenwood-annotation-allowed-classes",
],
out: [
@@ -44,6 +45,7 @@
framework_minus_apex_cmd = "$(location hoststubgen) " +
"@$(location :ravenwood-standard-options) " +
+ "@$(location :ravenwood-standard-annotations) " +
"--debug-log $(location hoststubgen_framework-minus-apex.log) " +
"--out-jar $(location ravenwood.jar) " +
"--in-jar $(location :framework-minus-apex-for-host) " +
@@ -178,6 +180,7 @@
tools: ["hoststubgen"],
cmd: "$(location hoststubgen) " +
"@$(location :ravenwood-standard-options) " +
+ "@$(location :ravenwood-standard-annotations) " +
"--debug-log $(location hoststubgen_services.core.log) " +
"--stats-file $(location hoststubgen_services.core_stats.csv) " +
@@ -196,6 +199,7 @@
":ravenwood-common-policies",
":ravenwood-services-policies",
":ravenwood-standard-options",
+ ":ravenwood-standard-annotations",
":ravenwood-annotation-allowed-classes",
],
out: [
@@ -247,6 +251,7 @@
tools: ["hoststubgen"],
cmd: "$(location hoststubgen) " +
"@$(location :ravenwood-standard-options) " +
+ "@$(location :ravenwood-standard-annotations) " +
"--debug-log $(location hoststubgen_core-icu4j-for-host.log) " +
"--stats-file $(location hoststubgen_core-icu4j-for-host_stats.csv) " +
@@ -265,6 +270,7 @@
":ravenwood-common-policies",
":icu-ravenwood-policies",
":ravenwood-standard-options",
+ ":ravenwood-standard-annotations",
],
out: [
"ravenwood.jar",
@@ -301,6 +307,7 @@
tools: ["hoststubgen"],
cmd: "$(location hoststubgen) " +
"@$(location :ravenwood-standard-options) " +
+ "@$(location :ravenwood-standard-annotations) " +
"--debug-log $(location framework-configinfrastructure.log) " +
"--stats-file $(location framework-configinfrastructure_stats.csv) " +
@@ -319,6 +326,7 @@
":ravenwood-common-policies",
":framework-configinfrastructure-ravenwood-policies",
":ravenwood-standard-options",
+ ":ravenwood-standard-annotations",
],
out: [
"ravenwood.jar",
@@ -355,6 +363,7 @@
tools: ["hoststubgen"],
cmd: "$(location hoststubgen) " +
"@$(location :ravenwood-standard-options) " +
+ "@$(location :ravenwood-standard-annotations) " +
"--debug-log $(location framework-statsd.log) " +
"--stats-file $(location framework-statsd_stats.csv) " +
@@ -373,6 +382,7 @@
":ravenwood-common-policies",
":framework-statsd-ravenwood-policies",
":ravenwood-standard-options",
+ ":ravenwood-standard-annotations",
],
out: [
"ravenwood.jar",
@@ -409,6 +419,7 @@
tools: ["hoststubgen"],
cmd: "$(location hoststubgen) " +
"@$(location :ravenwood-standard-options) " +
+ "@$(location :ravenwood-standard-annotations) " +
"--debug-log $(location framework-graphics.log) " +
"--stats-file $(location framework-graphics_stats.csv) " +
@@ -427,6 +438,7 @@
":ravenwood-common-policies",
":framework-graphics-ravenwood-policies",
":ravenwood-standard-options",
+ ":ravenwood-standard-annotations",
],
out: [
"ravenwood.jar",
diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING
index df63cb9..1148539 100644
--- a/ravenwood/TEST_MAPPING
+++ b/ravenwood/TEST_MAPPING
@@ -150,6 +150,10 @@
"host": true
},
{
+ "name": "RavenwoodCoreTest-light",
+ "host": true
+ },
+ {
"name": "RavenwoodMinimumTest",
"host": true
},
@@ -168,6 +172,10 @@
{
"name": "RavenwoodServicesTest",
"host": true
+ },
+ {
+ "name": "UinputTestsRavenwood",
+ "host": true
}
// AUTO-GENERATED-END
],
diff --git a/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodJdkPatchTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodJdkPatchTest.java
new file mode 100644
index 0000000..cdfd4a8
--- /dev/null
+++ b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodJdkPatchTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2025 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+
+import org.junit.Test;
+
+import java.io.FileDescriptor;
+import java.util.LinkedHashMap;
+import java.util.regex.Pattern;
+
+public class RavenwoodJdkPatchTest {
+
+ @Test
+ public void testUnicodeRegex() {
+ var pattern = Pattern.compile("\\w+");
+ assertTrue(pattern.matcher("über").matches());
+ }
+
+ @Test
+ public void testLinkedHashMapEldest() {
+ var map = new LinkedHashMap<String, String>();
+ map.put("a", "b");
+ map.put("x", "y");
+ assertEquals(map.entrySet().iterator().next(), map.eldest());
+ }
+
+ @Test
+ public void testFileDescriptorGetSetInt() throws ErrnoException {
+ FileDescriptor fd = Os.open("/dev/zero", OsConstants.O_RDONLY, 0);
+ try {
+ int fdRaw = fd.getInt$();
+ assertNotEquals(-1, fdRaw);
+ fd.setInt$(-1);
+ assertEquals(-1, fd.getInt$());
+ fd.setInt$(fdRaw);
+ Os.close(fd);
+ assertEquals(-1, fd.getInt$());
+ } finally {
+ Os.close(fd);
+ }
+ }
+}
diff --git a/ravenwood/texts/ravenizer-standard-options.txt b/ravenwood/texts/ravenizer-standard-options.txt
new file mode 100644
index 0000000..cef736f
--- /dev/null
+++ b/ravenwood/texts/ravenizer-standard-options.txt
@@ -0,0 +1,13 @@
+# File containing standard options to Ravenizer for Ravenwood
+
+# Keep all classes / methods / fields in tests and its target
+--default-keep
+
+--delete-finals
+
+# Include standard annotations
+@jar:texts/ravenwood-standard-annotations.txt
+
+# Apply common policies
+--policy-override-file
+ jar:texts/ravenwood-common-policies.txt
diff --git a/ravenwood/texts/ravenwood-standard-annotations.txt b/ravenwood/texts/ravenwood-standard-annotations.txt
new file mode 100644
index 0000000..75ec5ca
--- /dev/null
+++ b/ravenwood/texts/ravenwood-standard-annotations.txt
@@ -0,0 +1,37 @@
+# Standard annotations.
+# Note, each line is a single argument, so we need newlines after each `--xxx-annotation`.
+--keep-annotation
+ android.ravenwood.annotation.RavenwoodKeep
+
+--keep-annotation
+ android.ravenwood.annotation.RavenwoodKeepPartialClass
+
+--keep-class-annotation
+ android.ravenwood.annotation.RavenwoodKeepWholeClass
+
+--throw-annotation
+ android.ravenwood.annotation.RavenwoodThrow
+
+--remove-annotation
+ android.ravenwood.annotation.RavenwoodRemove
+
+--ignore-annotation
+ android.ravenwood.annotation.RavenwoodIgnore
+
+--partially-allowed-annotation
+ android.ravenwood.annotation.RavenwoodPartiallyAllowlisted
+
+--substitute-annotation
+ android.ravenwood.annotation.RavenwoodReplace
+
+--redirect-annotation
+ android.ravenwood.annotation.RavenwoodRedirect
+
+--redirection-class-annotation
+ android.ravenwood.annotation.RavenwoodRedirectionClass
+
+--class-load-hook-annotation
+ android.ravenwood.annotation.RavenwoodClassLoadHook
+
+--keep-static-initializer-annotation
+ android.ravenwood.annotation.RavenwoodKeepStaticInitializer
diff --git a/ravenwood/texts/ravenwood-standard-options.txt b/ravenwood/texts/ravenwood-standard-options.txt
index 2336575..0a65025 100644
--- a/ravenwood/texts/ravenwood-standard-options.txt
+++ b/ravenwood/texts/ravenwood-standard-options.txt
@@ -15,41 +15,3 @@
#--default-class-load-hook
# com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
-
-# Standard annotations.
-# Note, each line is a single argument, so we need newlines after each `--xxx-annotation`.
---keep-annotation
- android.ravenwood.annotation.RavenwoodKeep
-
---keep-annotation
- android.ravenwood.annotation.RavenwoodKeepPartialClass
-
---keep-class-annotation
- android.ravenwood.annotation.RavenwoodKeepWholeClass
-
---throw-annotation
- android.ravenwood.annotation.RavenwoodThrow
-
---remove-annotation
- android.ravenwood.annotation.RavenwoodRemove
-
---ignore-annotation
- android.ravenwood.annotation.RavenwoodIgnore
-
---partially-allowed-annotation
- android.ravenwood.annotation.RavenwoodPartiallyAllowlisted
-
---substitute-annotation
- android.ravenwood.annotation.RavenwoodReplace
-
---redirect-annotation
- android.ravenwood.annotation.RavenwoodRedirect
-
---redirection-class-annotation
- android.ravenwood.annotation.RavenwoodRedirectionClass
-
---class-load-hook-annotation
- android.ravenwood.annotation.RavenwoodClassLoadHook
-
---keep-static-initializer-annotation
- android.ravenwood.annotation.RavenwoodKeepStaticInitializer
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt
index f59e143..ae0a008 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt
@@ -15,8 +15,6 @@
*/
package com.android.hoststubgen
-import java.io.File
-
/**
* We will not print the stack trace for exceptions implementing it.
*/
@@ -64,9 +62,6 @@
class InputFileNotFoundException(filename: String) :
ArgumentsException("File '$filename' not found")
-fun String.ensureFileExists(): String {
- if (!File(this).exists()) {
- throw InputFileNotFoundException(this)
- }
- return this
-}
+/** Thrown when a JAR resource does not exist. */
+class JarResourceNotFoundException(path: String) :
+ ArgumentsException("JAR resource '$path' not found")
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt
index 4fe21ea..98f96a8 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt
@@ -16,6 +16,7 @@
package com.android.hoststubgen
import com.android.hoststubgen.asm.ClassNodes
+import com.android.hoststubgen.asm.findAnyAnnotation
import com.android.hoststubgen.filters.AnnotationBasedFilter
import com.android.hoststubgen.filters.ClassWidePolicyPropagatingFilter
import com.android.hoststubgen.filters.ConstantFilter
@@ -26,21 +27,25 @@
import com.android.hoststubgen.filters.OutputFilter
import com.android.hoststubgen.filters.SanitizationFilter
import com.android.hoststubgen.filters.TextFileFilterPolicyBuilder
+import com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
import com.android.hoststubgen.utils.ClassPredicate
import com.android.hoststubgen.visitors.BaseAdapter
+import com.android.hoststubgen.visitors.ImplGeneratingAdapter
import com.android.hoststubgen.visitors.PackageRedirectRemapper
+import java.io.PrintWriter
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.commons.ClassRemapper
import org.objectweb.asm.util.CheckClassAdapter
+import org.objectweb.asm.util.TraceClassVisitor
/**
* This class implements bytecode transformation of HostStubGen.
*/
class HostStubGenClassProcessor(
private val options: HostStubGenClassProcessorOptions,
- private val allClasses: ClassNodes,
+ val allClasses: ClassNodes,
private val errors: HostStubGenErrors = HostStubGenErrors(),
private val stats: HostStubGenStats? = null,
) {
@@ -48,6 +53,7 @@
val remapper = FilterRemapper(filter)
private val packageRedirector = PackageRedirectRemapper(options.packageRedirects)
+ private val processedAnnotation = setOf(HostStubGenProcessedAsKeep.CLASS_DESCRIPTOR)
/**
* Build the filter, which decides what classes/methods/fields should be put in stub or impl
@@ -130,15 +136,10 @@
return filter
}
- fun processClassBytecode(bytecode: ByteArray): ByteArray {
- val cr = ClassReader(bytecode)
+ private fun buildVisitor(base: ClassVisitor, className: String): ClassVisitor {
+ // Connect to the base visitor
+ var outVisitor: ClassVisitor = base
- // COMPUTE_FRAMES wouldn't be happy if code uses
- val flags = ClassWriter.COMPUTE_MAXS // or ClassWriter.COMPUTE_FRAMES
- val cw = ClassWriter(flags)
-
- // Connect to the class writer
- var outVisitor: ClassVisitor = cw
if (options.enableClassChecker.get) {
outVisitor = CheckClassAdapter(outVisitor)
}
@@ -149,15 +150,59 @@
val visitorOptions = BaseAdapter.Options(
errors = errors,
stats = stats,
- enablePreTrace = options.enablePreTrace.get,
- enablePostTrace = options.enablePostTrace.get,
deleteClassFinals = options.deleteFinals.get,
deleteMethodFinals = options.deleteFinals.get,
)
- outVisitor = BaseAdapter.getVisitor(
- cr.className, allClasses, outVisitor, filter,
- packageRedirector, visitorOptions
- )
+
+ val verbosePrinter = PrintWriter(log.getWriter(LogLevel.Verbose))
+
+ // Inject TraceClassVisitor for debugging.
+ if (options.enablePostTrace.get) {
+ outVisitor = TraceClassVisitor(outVisitor, verbosePrinter)
+ }
+
+ // Handle --package-redirect
+ if (!packageRedirector.isEmpty) {
+ // Don't apply the remapper on redirect-from classes.
+ // Otherwise, if the target jar actually contains the "from" classes (which
+ // may or may not be the case) they'd be renamed.
+ // But we update all references in other places, so, a method call to a "from" class
+ // would be replaced with the "to" class. All type references (e.g. variable types)
+ // will be updated too.
+ if (!packageRedirector.isTarget(className)) {
+ outVisitor = ClassRemapper(outVisitor, packageRedirector)
+ } else {
+ log.v(
+ "Class $className is a redirect-from class, not applying" +
+ " --package-redirect"
+ )
+ }
+ }
+
+ outVisitor = ImplGeneratingAdapter(allClasses, outVisitor, filter, visitorOptions)
+
+ // Inject TraceClassVisitor for debugging.
+ if (options.enablePreTrace.get) {
+ outVisitor = TraceClassVisitor(outVisitor, verbosePrinter)
+ }
+
+ return outVisitor
+ }
+
+ fun processClassBytecode(bytecode: ByteArray): ByteArray {
+ val cr = ClassReader(bytecode)
+
+ // If the class was already processed previously, skip
+ val clz = allClasses.getClass(cr.className)
+ if (clz.findAnyAnnotation(processedAnnotation) != null) {
+ return bytecode
+ }
+
+ // COMPUTE_FRAMES wouldn't be happy if code uses
+ val flags = ClassWriter.COMPUTE_MAXS // or ClassWriter.COMPUTE_FRAMES
+ val cw = ClassWriter(flags)
+
+ val outVisitor = buildVisitor(cw, cr.className)
cr.accept(outVisitor, ClassReader.EXPAND_FRAMES)
return cw.toByteArray()
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt
index c7c45e6..e7166f1 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt
@@ -18,6 +18,7 @@
import com.android.hoststubgen.filters.FilterPolicy
import com.android.hoststubgen.utils.ArgIterator
import com.android.hoststubgen.utils.BaseOptions
+import com.android.hoststubgen.utils.FileOrResource
import com.android.hoststubgen.utils.SetOnce
private fun parsePackageRedirect(fromColonTo: String): Pair<String, String> {
@@ -53,7 +54,7 @@
var defaultClassLoadHook: SetOnce<String?> = SetOnce(null),
var defaultMethodCallHook: SetOnce<String?> = SetOnce(null),
- var policyOverrideFiles: MutableList<String> = mutableListOf(),
+ var policyOverrideFiles: MutableList<FileOrResource> = mutableListOf(),
var defaultPolicy: SetOnce<FilterPolicy> = SetOnce(FilterPolicy.Remove),
@@ -73,15 +74,14 @@
return name
}
- override fun parseOption(option: String, ai: ArgIterator): Boolean {
+ override fun parseOption(option: String, args: ArgIterator): Boolean {
// Define some shorthands...
- fun nextArg(): String = ai.nextArgRequired(option)
+ fun nextArg(): String = args.nextArgRequired(option)
fun MutableSet<String>.addUniqueAnnotationArg(): String =
nextArg().also { this += ensureUniqueAnnotation(it) }
when (option) {
- "--policy-override-file" ->
- policyOverrideFiles.add(nextArg().ensureFileExists())
+ "--policy-override-file" -> policyOverrideFiles.add(FileOrResource(nextArg()))
"--default-remove" -> defaultPolicy.set(FilterPolicy.Remove)
"--default-throw" -> defaultPolicy.set(FilterPolicy.Throw)
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt
index b41ce0f..112ef01e 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt
@@ -217,7 +217,7 @@
fun isAnonymousInnerClass(cn: ClassNode): Boolean {
// TODO: Is there a better way?
- return cn.name.matches(numericalInnerClassName)
+ return cn.outerClass != null && cn.name.matches(numericalInnerClassName)
}
/**
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt
index b8b0d8a..7f36aca 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt
@@ -19,9 +19,9 @@
* Base class for an [OutputFilter] that uses another filter as a fallback.
*/
abstract class DelegatingFilter(
- // fallback shouldn't be used by subclasses directly, so make it private.
- // They should instead be calling into `super` or `outermostFilter`.
- private val fallback: OutputFilter
+ // fallback shouldn't be used by subclasses directly, so make it private.
+ // They should instead be calling into `super` or `outermostFilter`.
+ private val fallback: OutputFilter
) : OutputFilter() {
init {
fallback.outermostFilter = this
@@ -50,24 +50,24 @@
}
override fun getPolicyForField(
- className: String,
- fieldName: String
+ className: String,
+ fieldName: String
): FilterPolicyWithReason {
return fallback.getPolicyForField(className, fieldName)
}
override fun getPolicyForMethod(
- className: String,
- methodName: String,
- descriptor: String
+ className: String,
+ methodName: String,
+ descriptor: String
): FilterPolicyWithReason {
return fallback.getPolicyForMethod(className, methodName, descriptor)
}
override fun getRenameTo(
- className: String,
- methodName: String,
- descriptor: String
+ className: String,
+ methodName: String,
+ descriptor: String
): String? {
return fallback.getRenameTo(className, methodName, descriptor)
}
@@ -97,13 +97,12 @@
}
override fun getMethodCallReplaceTo(
- callerClassName: String,
- callerMethodName: String,
className: String,
methodName: String,
descriptor: String,
): MethodReplaceTarget? {
return fallback.getMethodCallReplaceTo(
- callerClassName, callerMethodName, className, methodName, descriptor)
+ className, methodName, descriptor
+ )
}
}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
index 474da6d..d44d016 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
@@ -16,7 +16,6 @@
package com.android.hoststubgen.filters
import com.android.hoststubgen.HostStubGenErrors
-import com.android.hoststubgen.HostStubGenInternalException
import com.android.hoststubgen.asm.CLASS_INITIALIZER_DESC
import com.android.hoststubgen.asm.CLASS_INITIALIZER_NAME
import com.android.hoststubgen.asm.ClassNodes
@@ -37,19 +36,15 @@
* TODO: Do we need a way to make anonymous class methods and lambdas "throw"?
*/
class ImplicitOutputFilter(
- private val errors: HostStubGenErrors,
- private val classes: ClassNodes,
- fallback: OutputFilter
+ private val errors: HostStubGenErrors,
+ private val classes: ClassNodes,
+ fallback: OutputFilter
) : DelegatingFilter(fallback) {
- private fun getClassImplicitPolicy(className: String, cn: ClassNode): FilterPolicyWithReason? {
+ private fun getClassImplicitPolicy(cn: ClassNode): FilterPolicyWithReason? {
if (isAnonymousInnerClass(cn)) {
log.forDebug {
// log.d(" anon-inner class: ${className} outer: ${cn.outerClass} ")
}
- if (cn.outerClass == null) {
- throw HostStubGenInternalException(
- "outerClass is null for anonymous inner class")
- }
// If the outer class needs to be in impl, it should be in impl too.
val outerPolicy = outermostFilter.getPolicyForClass(cn.outerClass)
if (outerPolicy.policy.needsInOutput) {
@@ -65,15 +60,15 @@
val cn = classes.getClass(className)
// Use the implicit policy, if any.
- getClassImplicitPolicy(className, cn)?.let { return it }
+ getClassImplicitPolicy(cn)?.let { return it }
return fallback
}
override fun getPolicyForMethod(
- className: String,
- methodName: String,
- descriptor: String
+ className: String,
+ methodName: String,
+ descriptor: String
): FilterPolicyWithReason {
val fallback = super.getPolicyForMethod(className, methodName, descriptor)
val classPolicy = outermostFilter.getPolicyForClass(className)
@@ -84,12 +79,14 @@
// "keep" instead.
// Unless it's an enum -- in that case, the below code would handle it.
if (!cn.isEnum() &&
- fallback.policy == FilterPolicy.Throw &&
- methodName == CLASS_INITIALIZER_NAME && descriptor == CLASS_INITIALIZER_DESC) {
+ fallback.policy == FilterPolicy.Throw &&
+ methodName == CLASS_INITIALIZER_NAME && descriptor == CLASS_INITIALIZER_DESC
+ ) {
// TODO Maybe show a warning?? But that'd be too noisy with --default-throw.
return FilterPolicy.Ignore.withReason(
"'throw' on static initializer is handled as 'ignore'" +
- " [original throw reason: ${fallback.reason}]")
+ " [original throw reason: ${fallback.reason}]"
+ )
}
log.d("Class ${cn.name} Class policy: $classPolicy")
@@ -120,7 +117,8 @@
// For synthetic methods (such as lambdas), let's just inherit the class's
// policy.
return memberPolicy.withReason(classPolicy.reason).wrapReason(
- "is-synthetic-method")
+ "is-synthetic-method"
+ )
}
}
}
@@ -129,8 +127,8 @@
}
override fun getPolicyForField(
- className: String,
- fieldName: String
+ className: String,
+ fieldName: String
): FilterPolicyWithReason {
val fallback = super.getPolicyForField(className, fieldName)
@@ -161,4 +159,4 @@
return fallback
}
-}
\ No newline at end of file
+}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt
index fc885d6..59da3da 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt
@@ -28,10 +28,12 @@
private val classes: ClassNodes,
fallback: OutputFilter,
) : DelegatingFilter(fallback) {
- private val mPolicies: MutableMap<String, FilterPolicyWithReason> = mutableMapOf()
- private val mRenames: MutableMap<String, String> = mutableMapOf()
- private val mRedirectionClasses: MutableMap<String, String> = mutableMapOf()
- private val mClassLoadHooks: MutableMap<String, String> = mutableMapOf()
+ private val mPolicies = mutableMapOf<String, FilterPolicyWithReason>()
+ private val mRenames = mutableMapOf<String, String>()
+ private val mRedirectionClasses = mutableMapOf<String, String>()
+ private val mClassLoadHooks = mutableMapOf<String, String>()
+ private val mMethodCallReplaceSpecs = mutableListOf<MethodCallReplaceSpec>()
+ private val mTypeRenameSpecs = mutableListOf<TypeRenameSpec>()
private fun getClassKey(className: String): String {
return className.toHumanReadableClassName()
@@ -45,10 +47,6 @@
return getClassKey(className) + "." + methodName + ";" + signature
}
- override fun getPolicyForClass(className: String): FilterPolicyWithReason {
- return mPolicies[getClassKey(className)] ?: super.getPolicyForClass(className)
- }
-
private fun checkClass(className: String) {
if (classes.findClass(className) == null) {
log.w("Unknown class $className")
@@ -74,6 +72,10 @@
}
}
+ override fun getPolicyForClass(className: String): FilterPolicyWithReason {
+ return mPolicies[getClassKey(className)] ?: super.getPolicyForClass(className)
+ }
+
fun setPolicyForClass(className: String, policy: FilterPolicyWithReason) {
checkClass(className)
mPolicies[getClassKey(className)] = policy
@@ -81,7 +83,7 @@
override fun getPolicyForField(className: String, fieldName: String): FilterPolicyWithReason {
return mPolicies[getFieldKey(className, fieldName)]
- ?: super.getPolicyForField(className, fieldName)
+ ?: super.getPolicyForField(className, fieldName)
}
fun setPolicyForField(className: String, fieldName: String, policy: FilterPolicyWithReason) {
@@ -90,21 +92,21 @@
}
override fun getPolicyForMethod(
- className: String,
- methodName: String,
- descriptor: String,
- ): FilterPolicyWithReason {
+ className: String,
+ methodName: String,
+ descriptor: String,
+ ): FilterPolicyWithReason {
return mPolicies[getMethodKey(className, methodName, descriptor)]
?: mPolicies[getMethodKey(className, methodName, "*")]
?: super.getPolicyForMethod(className, methodName, descriptor)
}
fun setPolicyForMethod(
- className: String,
- methodName: String,
- descriptor: String,
- policy: FilterPolicyWithReason,
- ) {
+ className: String,
+ methodName: String,
+ descriptor: String,
+ policy: FilterPolicyWithReason,
+ ) {
checkMethod(className, methodName, descriptor)
mPolicies[getMethodKey(className, methodName, descriptor)] = policy
}
@@ -123,7 +125,7 @@
override fun getRedirectionClass(className: String): String? {
return mRedirectionClasses[getClassKey(className)]
- ?: super.getRedirectionClass(className)
+ ?: super.getRedirectionClass(className)
}
fun setRedirectionClass(from: String, to: String) {
@@ -135,11 +137,52 @@
}
override fun getClassLoadHooks(className: String): List<String> {
- return addNonNullElement(super.getClassLoadHooks(className),
- mClassLoadHooks[getClassKey(className)])
+ return addNonNullElement(
+ super.getClassLoadHooks(className),
+ mClassLoadHooks[getClassKey(className)]
+ )
}
fun setClassLoadHook(className: String, methodName: String) {
mClassLoadHooks[getClassKey(className)] = methodName.toHumanReadableMethodName()
}
+
+ override fun hasAnyMethodCallReplace(): Boolean {
+ return mMethodCallReplaceSpecs.isNotEmpty() || super.hasAnyMethodCallReplace()
+ }
+
+ override fun getMethodCallReplaceTo(
+ className: String,
+ methodName: String,
+ descriptor: String,
+ ): MethodReplaceTarget? {
+ // Maybe use 'Tri' if we end up having too many replacements.
+ mMethodCallReplaceSpecs.forEach {
+ if (className == it.fromClass &&
+ methodName == it.fromMethod
+ ) {
+ if (it.fromDescriptor == "*" || descriptor == it.fromDescriptor) {
+ return MethodReplaceTarget(it.toClass, it.toMethod)
+ }
+ }
+ }
+ return super.getMethodCallReplaceTo(className, methodName, descriptor)
+ }
+
+ fun setMethodCallReplaceSpec(spec: MethodCallReplaceSpec) {
+ mMethodCallReplaceSpecs.add(spec)
+ }
+
+ override fun remapType(className: String): String? {
+ mTypeRenameSpecs.forEach {
+ if (it.typeInternalNamePattern.matcher(className).matches()) {
+ return it.typeInternalNamePrefix + className
+ }
+ }
+ return super.remapType(className)
+ }
+
+ fun setRemapTypeSpec(spec: TypeRenameSpec) {
+ mTypeRenameSpecs.add(spec)
+ }
}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt
index f99ce90..c47bb30 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt
@@ -41,10 +41,10 @@
abstract fun getPolicyForField(className: String, fieldName: String): FilterPolicyWithReason
abstract fun getPolicyForMethod(
- className: String,
- methodName: String,
- descriptor: String,
- ): FilterPolicyWithReason
+ className: String,
+ methodName: String,
+ descriptor: String,
+ ): FilterPolicyWithReason
/**
* If a given method is a substitute-from method, return the substitute-to method name.
@@ -108,8 +108,6 @@
* If a method call should be forwarded to another method, return the target's class / method.
*/
open fun getMethodCallReplaceTo(
- callerClassName: String,
- callerMethodName: String,
className: String,
methodName: String,
descriptor: String,
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
index dd353e9..97fc353 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
@@ -22,13 +22,13 @@
import com.android.hoststubgen.asm.toJvmClassName
import com.android.hoststubgen.log
import com.android.hoststubgen.normalizeTextLine
+import com.android.hoststubgen.utils.FileOrResource
import com.android.hoststubgen.whitespaceRegex
-import org.objectweb.asm.tree.ClassNode
import java.io.BufferedReader
-import java.io.FileReader
import java.io.PrintWriter
import java.io.Reader
import java.util.regex.Pattern
+import org.objectweb.asm.tree.ClassNode
/**
* Print a class node as a "keep" policy.
@@ -58,6 +58,23 @@
RFile,
}
+data class MethodCallReplaceSpec(
+ val fromClass: String,
+ val fromMethod: String,
+ val fromDescriptor: String,
+ val toClass: String,
+ val toMethod: String,
+)
+
+/**
+ * When a package name matches [typeInternalNamePattern], we prepend [typeInternalNamePrefix]
+ * to it.
+ */
+data class TypeRenameSpec(
+ val typeInternalNamePattern: Pattern,
+ val typeInternalNamePrefix: String,
+)
+
/**
* This receives [TextFileFilterPolicyBuilder] parsing result.
*/
@@ -99,7 +116,7 @@
className: String,
methodName: String,
methodDesc: String,
- replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec,
+ replaceSpec: MethodCallReplaceSpec,
)
}
@@ -116,9 +133,6 @@
private var featureFlagsPolicy: FilterPolicyWithReason? = null
private var syspropsPolicy: FilterPolicyWithReason? = null
private var rFilePolicy: FilterPolicyWithReason? = null
- private val typeRenameSpec = mutableListOf<TextFilePolicyRemapperFilter.TypeRenameSpec>()
- private val methodReplaceSpec =
- mutableListOf<TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec>()
/**
* Fields for a filter chain used for "partial allowlisting", which are used by
@@ -126,47 +140,34 @@
*/
private val annotationAllowedInMemoryFilter: InMemoryOutputFilter
val annotationAllowedMembersFilter: OutputFilter
+ get() = annotationAllowedInMemoryFilter
private val annotationAllowedPolicy = FilterPolicy.AnnotationAllowed.withReason(FILTER_REASON)
init {
// Create a filter that checks "partial allowlisting".
- var aaf: OutputFilter = ConstantFilter(FilterPolicy.Remove, "default disallowed")
-
- aaf = InMemoryOutputFilter(classes, aaf)
- annotationAllowedInMemoryFilter = aaf
-
- annotationAllowedMembersFilter = annotationAllowedInMemoryFilter
+ val filter = ConstantFilter(FilterPolicy.Remove, "default disallowed")
+ annotationAllowedInMemoryFilter = InMemoryOutputFilter(classes, filter)
}
/**
* Parse a given policy file. This method can be called multiple times to read from
* multiple files. To get the resulting filter, use [createOutputFilter]
*/
- fun parse(file: String) {
+ fun parse(file: FileOrResource) {
// We may parse multiple files, but we reuse the same parser, because the parser
// will make sure there'll be no dupplicating "special class" policies.
- parser.parse(FileReader(file), file, Processor())
+ parser.parse(file.open(), file.path, Processor())
}
/**
* Generate the resulting [OutputFilter].
*/
fun createOutputFilter(): OutputFilter {
- var ret: OutputFilter = imf
- if (typeRenameSpec.isNotEmpty()) {
- ret = TextFilePolicyRemapperFilter(typeRenameSpec, ret)
- }
- if (methodReplaceSpec.isNotEmpty()) {
- ret = TextFilePolicyMethodReplaceFilter(methodReplaceSpec, classes, ret)
- }
-
// Wrap the in-memory-filter with AHF.
- ret = AndroidHeuristicsFilter(
- classes, aidlPolicy, featureFlagsPolicy, syspropsPolicy, rFilePolicy, ret
+ return AndroidHeuristicsFilter(
+ classes, aidlPolicy, featureFlagsPolicy, syspropsPolicy, rFilePolicy, imf
)
-
- return ret
}
private inner class Processor : PolicyFileProcessor {
@@ -180,9 +181,7 @@
}
override fun onRename(pattern: Pattern, prefix: String) {
- typeRenameSpec += TextFilePolicyRemapperFilter.TypeRenameSpec(
- pattern, prefix
- )
+ imf.setRemapTypeSpec(TypeRenameSpec(pattern, prefix))
}
override fun onClassStart(className: String) {
@@ -284,12 +283,12 @@
className: String,
methodName: String,
methodDesc: String,
- replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec,
+ replaceSpec: MethodCallReplaceSpec,
) {
// Keep the source method, because the target method may call it.
imf.setPolicyForMethod(className, methodName, methodDesc,
FilterPolicy.Keep.withReason(FILTER_REASON))
- methodReplaceSpec.add(replaceSpec)
+ imf.setMethodCallReplaceSpec(replaceSpec)
}
}
}
@@ -630,13 +629,13 @@
if (classAndMethod != null) {
// If the substitution target contains a ".", then
// it's a method call redirect.
- val spec = TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec(
- currentClassName!!.toJvmClassName(),
- methodName,
- signature,
- classAndMethod.first.toJvmClassName(),
- classAndMethod.second,
- )
+ val spec = MethodCallReplaceSpec(
+ className.toJvmClassName(),
+ methodName,
+ signature,
+ classAndMethod.first.toJvmClassName(),
+ classAndMethod.second,
+ )
processor.onMethodOutClassReplace(
className,
methodName,
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyMethodReplaceFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyMethodReplaceFilter.kt
deleted file mode 100644
index a3f934c..0000000
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyMethodReplaceFilter.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.hoststubgen.filters
-
-import com.android.hoststubgen.asm.ClassNodes
-
-/**
- * Filter used by TextFileFilterPolicyParser for "method call relacement".
- */
-class TextFilePolicyMethodReplaceFilter(
- val spec: List<MethodCallReplaceSpec>,
- val classes: ClassNodes,
- val fallback: OutputFilter,
-) : DelegatingFilter(fallback) {
-
- data class MethodCallReplaceSpec(
- val fromClass: String,
- val fromMethod: String,
- val fromDescriptor: String,
- val toClass: String,
- val toMethod: String,
- )
-
- override fun hasAnyMethodCallReplace(): Boolean {
- return true
- }
-
- override fun getMethodCallReplaceTo(
- callerClassName: String,
- callerMethodName: String,
- className: String,
- methodName: String,
- descriptor: String,
- ): MethodReplaceTarget? {
- // Maybe use 'Tri' if we end up having too many replacements.
- spec.forEach {
- if (className == it.fromClass &&
- methodName == it.fromMethod
- ) {
- if (it.fromDescriptor == "*" || descriptor == it.fromDescriptor) {
- return MethodReplaceTarget(it.toClass, it.toMethod)
- }
- }
- }
- return null
- }
-}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyRemapperFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyRemapperFilter.kt
deleted file mode 100644
index bc90d12..0000000
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyRemapperFilter.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.hoststubgen.filters
-
-import java.util.regex.Pattern
-
-/**
- * A filter that provides a simple "jarjar" functionality via [mapType]
- */
-class TextFilePolicyRemapperFilter(
- val typeRenameSpecs: List<TypeRenameSpec>,
- fallback: OutputFilter,
-) : DelegatingFilter(fallback) {
- /**
- * When a package name matches [typeInternalNamePattern], we prepend [typeInternalNamePrefix]
- * to it.
- */
- data class TypeRenameSpec(
- val typeInternalNamePattern: Pattern,
- val typeInternalNamePrefix: String,
- )
-
- override fun remapType(className: String): String? {
- typeRenameSpecs.forEach {
- if (it.typeInternalNamePattern.matcher(className).matches()) {
- return it.typeInternalNamePrefix + className
- }
- }
- return null
- }
-}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt
index 0b17879..d086992 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt
@@ -16,11 +16,16 @@
package com.android.hoststubgen.utils
import com.android.hoststubgen.ArgumentsException
-import com.android.hoststubgen.ensureFileExists
+import com.android.hoststubgen.InputFileNotFoundException
+import com.android.hoststubgen.JarResourceNotFoundException
import com.android.hoststubgen.log
import com.android.hoststubgen.normalizeTextLine
-import java.io.BufferedReader
+import java.io.File
import java.io.FileReader
+import java.io.InputStreamReader
+import java.io.Reader
+
+const val JAR_RESOURCE_PREFIX = "jar:"
/**
* Base class for parsing arguments from commandline.
@@ -73,7 +78,7 @@
*
* Subclasses override/extend this method to support more options.
*/
- abstract fun parseOption(option: String, ai: ArgIterator): Boolean
+ abstract fun parseOption(option: String, args: ArgIterator): Boolean
abstract fun dumpFields(): String
}
@@ -112,7 +117,9 @@
companion object {
fun withAtFiles(args: List<String>): ArgIterator {
- return ArgIterator(expandAtFiles(args))
+ val expanded = mutableListOf<String>()
+ expandAtFiles(args.asSequence(), expanded)
+ return ArgIterator(expanded)
}
/**
@@ -125,34 +132,30 @@
*
* The file can contain '#' as comments.
*/
- private fun expandAtFiles(args: List<String>): List<String> {
- val ret = mutableListOf<String>()
-
+ private fun expandAtFiles(args: Sequence<String>, out: MutableList<String>) {
args.forEach { arg ->
if (arg.startsWith("@@")) {
- ret += arg.substring(1)
+ out.add(arg.substring(1))
return@forEach
} else if (!arg.startsWith('@')) {
- ret += arg
+ out.add(arg)
return@forEach
}
+
// Read from the file, and add each line to the result.
- val filename = arg.substring(1).ensureFileExists()
+ val file = FileOrResource(arg.substring(1))
- log.v("Expanding options file $filename")
+ log.v("Expanding options file ${file.path}")
- BufferedReader(FileReader(filename)).use { reader ->
- while (true) {
- var line = reader.readLine() ?: break // EOF
+ val fileArgs = file
+ .open()
+ .buffered()
+ .lineSequence()
+ .map(::normalizeTextLine)
+ .filter(CharSequence::isNotEmpty)
- line = normalizeTextLine(line)
- if (line.isNotEmpty()) {
- ret += line
- }
- }
- }
+ expandAtFiles(fileArgs, out)
}
- return ret
}
}
}
@@ -204,3 +207,37 @@
}
}
}
+
+/**
+ * A path either points to a file in filesystem, or an entry in the JAR.
+ */
+class FileOrResource(val path: String) {
+ init {
+ path.ensureFileExists()
+ }
+
+ /**
+ * Either read from filesystem, or read from JAR resources.
+ */
+ fun open(): Reader {
+ return if (path.startsWith(JAR_RESOURCE_PREFIX)) {
+ val path = path.removePrefix(JAR_RESOURCE_PREFIX)
+ InputStreamReader(this::class.java.classLoader.getResourceAsStream(path)!!)
+ } else {
+ FileReader(path)
+ }
+ }
+}
+
+fun String.ensureFileExists(): String {
+ if (this.startsWith(JAR_RESOURCE_PREFIX)) {
+ val cl = FileOrResource::class.java.classLoader
+ val path = this.removePrefix(JAR_RESOURCE_PREFIX)
+ if (cl.getResource(path) == null) {
+ throw JarResourceNotFoundException(path)
+ }
+ } else if (!File(this).exists()) {
+ throw InputFileNotFoundException(this)
+ }
+ return this
+}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt
index a08d1d6..769b769 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt
@@ -17,7 +17,6 @@
import com.android.hoststubgen.HostStubGenErrors
import com.android.hoststubgen.HostStubGenStats
-import com.android.hoststubgen.LogLevel
import com.android.hoststubgen.asm.ClassNodes
import com.android.hoststubgen.asm.UnifiedVisitor
import com.android.hoststubgen.asm.getPackageNameFromFullClassName
@@ -26,13 +25,10 @@
import com.android.hoststubgen.filters.OutputFilter
import com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
import com.android.hoststubgen.log
-import java.io.PrintWriter
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.FieldVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
-import org.objectweb.asm.commons.ClassRemapper
-import org.objectweb.asm.util.TraceClassVisitor
const val OPCODE_VERSION = Opcodes.ASM9
@@ -49,8 +45,6 @@
data class Options(
val errors: HostStubGenErrors,
val stats: HostStubGenStats?,
- val enablePreTrace: Boolean,
- val enablePostTrace: Boolean,
val deleteClassFinals: Boolean,
val deleteMethodFinals: Boolean,
// We don't remove finals from fields, because final fields have a stronger memory
@@ -253,50 +247,4 @@
substituted: Boolean,
superVisitor: MethodVisitor?,
): MethodVisitor?
-
- companion object {
- fun getVisitor(
- classInternalName: String,
- classes: ClassNodes,
- nextVisitor: ClassVisitor,
- filter: OutputFilter,
- packageRedirector: PackageRedirectRemapper,
- options: Options,
- ): ClassVisitor {
- var next = nextVisitor
-
- val verbosePrinter = PrintWriter(log.getWriter(LogLevel.Verbose))
-
- // Inject TraceClassVisitor for debugging.
- if (options.enablePostTrace) {
- next = TraceClassVisitor(next, verbosePrinter)
- }
-
- // Handle --package-redirect
- if (!packageRedirector.isEmpty) {
- // Don't apply the remapper on redirect-from classes.
- // Otherwise, if the target jar actually contains the "from" classes (which
- // may or may not be the case) they'd be renamed.
- // But we update all references in other places, so, a method call to a "from" class
- // would be replaced with the "to" class. All type references (e.g. variable types)
- // will be updated too.
- if (!packageRedirector.isTarget(classInternalName)) {
- next = ClassRemapper(next, packageRedirector)
- } else {
- log.v(
- "Class $classInternalName is a redirect-from class, not applying" +
- " --package-redirect"
- )
- }
- }
-
- next = ImplGeneratingAdapter(classes, next, filter, options)
-
- // Inject TraceClassVisitor for debugging.
- if (options.enablePreTrace) {
- next = TraceClassVisitor(next, verbosePrinter)
- }
- return next
- }
- }
}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
index b8a3576..617385a 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
@@ -396,7 +396,7 @@
}
val to = filter.getMethodCallReplaceTo(
- currentClassName, callerMethodName, owner, name, descriptor
+ owner, name, descriptor
)
if (to == null
diff --git a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
index 8bb454f..d9cc54a 100644
--- a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
+++ b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
@@ -18,6 +18,7 @@
import com.android.hoststubgen.utils.ArgIterator
import com.android.hoststubgen.utils.IntSetOnce
import com.android.hoststubgen.utils.SetOnce
+import com.android.hoststubgen.utils.ensureFileExists
/**
* Options that can be set from command line arguments.
@@ -61,9 +62,9 @@
}
}
- override fun parseOption(option: String, ai: ArgIterator): Boolean {
+ override fun parseOption(option: String, args: ArgIterator): Boolean {
// Define some shorthands...
- fun nextArg(): String = ai.nextArgRequired(option)
+ fun nextArg(): String = args.nextArgRequired(option)
when (option) {
// TODO: Write help
@@ -94,7 +95,7 @@
}
}
- else -> return super.parseOption(option, ai)
+ else -> return super.parseOption(option, args)
}
return true
diff --git a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt
index f7fd080..58bd9e9 100644
--- a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt
+++ b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt
@@ -16,10 +16,10 @@
package com.android.platform.test.ravenwood.ravenhelper.policytoannot
import com.android.hoststubgen.ArgumentsException
-import com.android.hoststubgen.ensureFileExists
import com.android.hoststubgen.utils.ArgIterator
import com.android.hoststubgen.utils.BaseOptions
import com.android.hoststubgen.utils.SetOnce
+import com.android.hoststubgen.utils.ensureFileExists
/**
* Options for the "ravenhelper pta" subcommand.
@@ -41,8 +41,8 @@
var dumpOperations: SetOnce<Boolean> = SetOnce(false),
) : BaseOptions() {
- override fun parseOption(option: String, ai: ArgIterator): Boolean {
- fun nextArg(): String = ai.nextArgRequired(option)
+ override fun parseOption(option: String, args: ArgIterator): Boolean {
+ fun nextArg(): String = args.nextArgRequired(option)
when (option) {
// TODO: Write help
diff --git a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt
index fd6f732..5ce9a23 100644
--- a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt
+++ b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt
@@ -19,10 +19,10 @@
import com.android.hoststubgen.asm.CLASS_INITIALIZER_NAME
import com.android.hoststubgen.asm.toJvmClassName
import com.android.hoststubgen.filters.FilterPolicyWithReason
+import com.android.hoststubgen.filters.MethodCallReplaceSpec
import com.android.hoststubgen.filters.PolicyFileProcessor
import com.android.hoststubgen.filters.SpecialClass
import com.android.hoststubgen.filters.TextFileFilterPolicyParser
-import com.android.hoststubgen.filters.TextFilePolicyMethodReplaceFilter
import com.android.hoststubgen.log
import com.android.hoststubgen.utils.ClassPredicate
import com.android.platform.test.ravenwood.ravenhelper.SubcommandHandler
@@ -448,7 +448,7 @@
className: String,
methodName: String,
methodDesc: String,
- replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec,
+ replaceSpec: MethodCallReplaceSpec,
) {
// This can't be converted to an annotation.
classHasMember = true
diff --git a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt
index 8b95843..6e0b7b8 100644
--- a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt
+++ b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt
@@ -16,10 +16,10 @@
package com.android.platform.test.ravenwood.ravenhelper.sourcemap
import com.android.hoststubgen.ArgumentsException
-import com.android.hoststubgen.ensureFileExists
import com.android.hoststubgen.utils.ArgIterator
import com.android.hoststubgen.utils.BaseOptions
import com.android.hoststubgen.utils.SetOnce
+import com.android.hoststubgen.utils.ensureFileExists
/**
* Options for the "ravenhelper map" subcommand.
@@ -38,8 +38,8 @@
var text: SetOnce<String?> = SetOnce(null),
) : BaseOptions() {
- override fun parseOption(option: String, ai: ArgIterator): Boolean {
- fun nextArg(): String = ai.nextArgRequired(option)
+ override fun parseOption(option: String, args: ArgIterator): Boolean {
+ fun nextArg(): String = args.nextArgRequired(option)
when (option) {
// TODO: Write help
diff --git a/ravenwood/tools/ravenizer/Android.bp b/ravenwood/tools/ravenizer/Android.bp
index 957e206..93cda4e 100644
--- a/ravenwood/tools/ravenizer/Android.bp
+++ b/ravenwood/tools/ravenizer/Android.bp
@@ -15,5 +15,10 @@
"hoststubgen-lib",
"ravenwood-junit-for-ravenizer",
],
+ java_resources: [
+ ":ravenizer-standard-options",
+ ":ravenwood-standard-annotations",
+ ":ravenwood-common-policies",
+ ],
visibility: ["//visibility:public"],
}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
index e67c730..04e3bda2 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
@@ -16,23 +16,22 @@
package com.android.platform.test.ravenwood.ravenizer
import com.android.hoststubgen.GeneralUserErrorException
+import com.android.hoststubgen.HostStubGenClassProcessor
import com.android.hoststubgen.asm.ClassNodes
import com.android.hoststubgen.asm.zipEntryNameToClassName
import com.android.hoststubgen.executableName
import com.android.hoststubgen.log
import com.android.platform.test.ravenwood.ravenizer.adapter.RunnerRewritingAdapter
+import java.io.BufferedInputStream
+import java.io.BufferedOutputStream
+import java.io.FileOutputStream
+import java.util.zip.ZipEntry
+import java.util.zip.ZipFile
+import java.util.zip.ZipOutputStream
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.util.CheckClassAdapter
-import java.io.BufferedInputStream
-import java.io.BufferedOutputStream
-import java.io.FileOutputStream
-import java.io.InputStream
-import java.io.OutputStream
-import java.util.zip.ZipEntry
-import java.util.zip.ZipFile
-import java.util.zip.ZipOutputStream
/**
* Various stats on Ravenizer.
@@ -41,7 +40,7 @@
/** Total end-to-end time. */
var totalTime: Double = .0,
- /** Time took to build [ClasNodes] */
+ /** Time took to build [ClassNodes] */
var loadStructureTime: Double = .0,
/** Time took to validate the classes */
@@ -50,14 +49,17 @@
/** Total real time spent for converting the jar file */
var totalProcessTime: Double = .0,
- /** Total real time spent for converting class files (except for I/O time). */
- var totalConversionTime: Double = .0,
+ /** Total real time spent for ravenizing class files (excluding I/O time). */
+ var totalRavenizeTime: Double = .0,
+
+ /** Total real time spent for processing class files HSG style (excluding I/O time). */
+ var totalHostStubGenTime: Double = .0,
/** Total real time spent for copying class files without modification. */
var totalCopyTime: Double = .0,
/** # of entries in the input jar file */
- var totalEntiries: Int = 0,
+ var totalEntries: Int = 0,
/** # of *.class files in the input jar file */
var totalClasses: Int = 0,
@@ -67,14 +69,15 @@
) {
override fun toString(): String {
return """
- RavenizerStats{
+ RavenizerStats {
totalTime=$totalTime,
loadStructureTime=$loadStructureTime,
validationTime=$validationTime,
totalProcessTime=$totalProcessTime,
- totalConversionTime=$totalConversionTime,
+ totalRavenizeTime=$totalRavenizeTime,
+ totalHostStubGenTime=$totalHostStubGenTime,
totalCopyTime=$totalCopyTime,
- totalEntiries=$totalEntiries,
+ totalEntries=$totalEntries,
totalClasses=$totalClasses,
processedClasses=$processedClasses,
}
@@ -90,12 +93,18 @@
val stats = RavenizerStats()
stats.totalTime = log.nTime {
+ val allClasses = ClassNodes.loadClassStructures(options.inJar.get) {
+ stats.loadStructureTime = it
+ }
+ val processor = HostStubGenClassProcessor(options, allClasses)
+
process(
options.inJar.get,
options.outJar.get,
options.enableValidation.get,
options.fatalValidation.get,
options.stripMockito.get,
+ processor,
stats,
)
}
@@ -108,15 +117,13 @@
enableValidation: Boolean,
fatalValidation: Boolean,
stripMockito: Boolean,
+ processor: HostStubGenClassProcessor,
stats: RavenizerStats,
) {
- var allClasses = ClassNodes.loadClassStructures(inJar) {
- time -> stats.loadStructureTime = time
- }
if (enableValidation) {
stats.validationTime = log.iTime("Validating classes") {
- if (!validateClasses(allClasses)) {
- var message = "Invalid test class(es) detected." +
+ if (!validateClasses(processor.allClasses)) {
+ val message = "Invalid test class(es) detected." +
" See error log for details."
if (fatalValidation) {
throw RavenizerInvalidTestException(message)
@@ -126,7 +133,7 @@
}
}
}
- if (includeUnsupportedMockito(allClasses)) {
+ if (includeUnsupportedMockito(processor.allClasses)) {
log.w("Unsupported Mockito detected in $inJar!")
}
@@ -134,7 +141,7 @@
ZipFile(inJar).use { inZip ->
val inEntries = inZip.entries()
- stats.totalEntiries = inZip.size()
+ stats.totalEntries = inZip.size()
ZipOutputStream(BufferedOutputStream(FileOutputStream(outJar))).use { outZip ->
while (inEntries.hasMoreElements()) {
@@ -159,9 +166,9 @@
stats.totalClasses += 1
}
- if (className != null && shouldProcessClass(allClasses, className)) {
- stats.processedClasses += 1
- processSingleClass(inZip, entry, outZip, allClasses, stats)
+ if (className != null &&
+ shouldProcessClass(processor.allClasses, className)) {
+ processSingleClass(inZip, entry, outZip, processor, stats)
} else {
// Too slow, let's use merge_zips to bring back the original classes.
copyZipEntry(inZip, entry, outZip, stats)
@@ -201,14 +208,22 @@
inZip: ZipFile,
entry: ZipEntry,
outZip: ZipOutputStream,
- allClasses: ClassNodes,
+ processor: HostStubGenClassProcessor,
stats: RavenizerStats,
) {
+ stats.processedClasses += 1
val newEntry = ZipEntry(entry.name)
outZip.putNextEntry(newEntry)
BufferedInputStream(inZip.getInputStream(entry)).use { bis ->
- processSingleClass(entry, bis, outZip, allClasses, stats)
+ var classBytes = bis.readBytes()
+ stats.totalRavenizeTime += log.vTime("Ravenize ${entry.name}") {
+ classBytes = ravenizeSingleClass(entry, classBytes, processor.allClasses)
+ }
+ stats.totalHostStubGenTime += log.vTime("HostStubGen ${entry.name}") {
+ classBytes = processor.processClassBytecode(classBytes)
+ }
+ outZip.write(classBytes)
}
outZip.closeEntry()
}
@@ -217,41 +232,34 @@
* Whether a class needs to be processed. This must be kept in sync with [processSingleClass].
*/
private fun shouldProcessClass(classes: ClassNodes, classInternalName: String): Boolean {
- return !classInternalName.shouldByBypassed()
+ return !classInternalName.shouldBypass()
&& RunnerRewritingAdapter.shouldProcess(classes, classInternalName)
}
- private fun processSingleClass(
+ private fun ravenizeSingleClass(
entry: ZipEntry,
- input: InputStream,
- output: OutputStream,
+ input: ByteArray,
allClasses: ClassNodes,
- stats: RavenizerStats,
- ) {
- val cr = ClassReader(input)
+ ): ByteArray {
+ val classInternalName = zipEntryNameToClassName(entry.name)
+ ?: throw RavenizerInternalException("Unexpected zip entry name: ${entry.name}")
- lateinit var data: ByteArray
- stats.totalConversionTime += log.vTime("Modify ${entry.name}") {
+ val flags = ClassWriter.COMPUTE_MAXS
+ val cw = ClassWriter(flags)
+ var outVisitor: ClassVisitor = cw
- val classInternalName = zipEntryNameToClassName(entry.name)
- ?: throw RavenizerInternalException("Unexpected zip entry name: ${entry.name}")
- val flags = ClassWriter.COMPUTE_MAXS
- val cw = ClassWriter(flags)
- var outVisitor: ClassVisitor = cw
-
- val enableChecker = false
- if (enableChecker) {
- outVisitor = CheckClassAdapter(outVisitor)
- }
-
- // This must be kept in sync with shouldProcessClass.
- outVisitor = RunnerRewritingAdapter.maybeApply(
- classInternalName, allClasses, outVisitor)
-
- cr.accept(outVisitor, ClassReader.EXPAND_FRAMES)
-
- data = cw.toByteArray()
+ val enableChecker = false
+ if (enableChecker) {
+ outVisitor = CheckClassAdapter(outVisitor)
}
- output.write(data)
+
+ // This must be kept in sync with shouldProcessClass.
+ outVisitor = RunnerRewritingAdapter.maybeApply(
+ classInternalName, allClasses, outVisitor)
+
+ val cr = ClassReader(input)
+ cr.accept(outVisitor, ClassReader.EXPAND_FRAMES)
+
+ return cw.toByteArray()
}
}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt
index 7f4829e..8a09e6d 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt
@@ -21,6 +21,7 @@
import com.android.hoststubgen.executableName
import com.android.hoststubgen.log
import com.android.hoststubgen.runMainWithBoilerplate
+import com.android.hoststubgen.utils.JAR_RESOURCE_PREFIX
import java.nio.file.Paths
import kotlin.io.path.exists
@@ -36,6 +37,10 @@
*/
private val RAVENIZER_DOTFILE = System.getenv("HOME") + "/.ravenizer-unsafe"
+/**
+ * This is the name of the standard option text file embedded inside ravenizer.jar.
+ */
+private const val RAVENIZER_STANDARD_OPTIONS = "texts/ravenizer-standard-options.txt"
/**
* Entry point.
@@ -45,12 +50,12 @@
log.setConsoleLogLevel(LogLevel.Info)
runMainWithBoilerplate {
- var newArgs = args.asList()
+ val newArgs = args.toMutableList()
+ newArgs.add(0, "@$JAR_RESOURCE_PREFIX$RAVENIZER_STANDARD_OPTIONS")
+
if (Paths.get(RAVENIZER_DOTFILE).exists()) {
log.i("Reading options from $RAVENIZER_DOTFILE")
- newArgs = args.toMutableList().apply {
- add(0, "@$RAVENIZER_DOTFILE")
- }
+ newArgs.add(0, "@$RAVENIZER_DOTFILE")
}
val options = RavenizerOptions().apply { parseArgs(newArgs) }
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
index 2c03654..5d278bb 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
@@ -16,10 +16,10 @@
package com.android.platform.test.ravenwood.ravenizer
import com.android.hoststubgen.ArgumentsException
-import com.android.hoststubgen.ensureFileExists
+import com.android.hoststubgen.HostStubGenClassProcessorOptions
import com.android.hoststubgen.utils.ArgIterator
-import com.android.hoststubgen.utils.BaseOptions
import com.android.hoststubgen.utils.SetOnce
+import com.android.hoststubgen.utils.ensureFileExists
class RavenizerOptions(
/** Input jar file*/
@@ -36,10 +36,10 @@
/** Whether to remove mockito and dexmaker classes. */
var stripMockito: SetOnce<Boolean> = SetOnce(false),
-) : BaseOptions() {
+) : HostStubGenClassProcessorOptions() {
- override fun parseOption(option: String, ai: ArgIterator): Boolean {
- fun nextArg(): String = ai.nextArgRequired(option)
+ override fun parseOption(option: String, args: ArgIterator): Boolean {
+ fun nextArg(): String = args.nextArgRequired(option)
when (option) {
// TODO: Write help
@@ -57,7 +57,7 @@
"--strip-mockito" -> stripMockito.set(true)
"--no-strip-mockito" -> stripMockito.set(false)
- else -> return false
+ else -> return super.parseOption(option, args)
}
return true
@@ -79,6 +79,6 @@
enableValidation=$enableValidation,
fatalValidation=$fatalValidation,
stripMockito=$stripMockito,
- """.trimIndent()
+ """.trimIndent() + '\n' + super.dumpFields()
}
}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
index 6092fcc..b394a76 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
@@ -15,8 +15,8 @@
*/
package com.android.platform.test.ravenwood.ravenizer
-import android.platform.test.annotations.internal.InnerRunner
import android.platform.test.annotations.NoRavenizer
+import android.platform.test.annotations.internal.InnerRunner
import android.platform.test.ravenwood.RavenwoodAwareTestRunner
import com.android.hoststubgen.asm.ClassNodes
import com.android.hoststubgen.asm.findAnyAnnotation
@@ -85,7 +85,7 @@
/**
* Classes that should never be modified.
*/
-fun String.shouldByBypassed(): Boolean {
+fun String.shouldBypass(): Boolean {
if (this.isRavenwoodClass()) {
return true
}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt
index 61e254b..d252b4d 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt
@@ -20,8 +20,8 @@
import com.android.hoststubgen.asm.startsWithAny
import com.android.hoststubgen.asm.toHumanReadableClassName
import com.android.hoststubgen.log
-import org.objectweb.asm.tree.ClassNode
import java.util.regex.Pattern
+import org.objectweb.asm.tree.ClassNode
fun validateClasses(classes: ClassNodes): Boolean {
var allOk = true
@@ -37,7 +37,7 @@
*
*/
fun checkClass(cn: ClassNode, classes: ClassNodes): Boolean {
- if (cn.name.shouldByBypassed()) {
+ if (cn.name.shouldBypass()) {
// Class doesn't need to be checked.
return true
}
@@ -145,4 +145,4 @@
private fun isAllowListedLegacyTest(targetClass: ClassNode): Boolean {
return allowListedLegacyTests.contains(targetClass.name)
-}
\ No newline at end of file
+}
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index b52b3dab..35db3c6 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -260,6 +260,16 @@
}
flag {
+ name: "pointer_up_motion_event_in_touch_exploration"
+ namespace: "accessibility"
+ description: "Allows POINTER_UP motionEvents to trigger during touch exploration."
+ bug: "374930391"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "proxy_use_apps_on_virtual_device_listener"
namespace: "accessibility"
description: "Fixes race condition described in b/286587811"
@@ -336,3 +346,13 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "hearing_input_change_when_comm_device"
+ namespace: "accessibility"
+ description: "Listen to the CommunicationDeviceChanged to show hearing device input notification."
+ bug: "394070235"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index c49151d..6c26c1f 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -521,15 +521,6 @@
@Nullable IBinder focusedToken) {
return AccessibilityManagerService.this.handleKeyGestureEvent(event);
}
-
- @Override
- public boolean isKeyGestureSupported(int gestureType) {
- return switch (gestureType) {
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION,
- KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK -> true;
- default -> false;
- };
- }
};
@VisibleForTesting
@@ -5619,7 +5610,7 @@
}
private boolean isValidDisplay(@Nullable Display display) {
- if (display == null || display.getType() == Display.TYPE_OVERLAY) {
+ if (display == null) {
return false;
}
// Private virtual displays are created by the ap and is not allowed to access by other
diff --git a/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java b/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java
index 10dffb5..805d7f8 100644
--- a/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java
@@ -65,9 +65,9 @@
private final Executor mCallbackExecutor;
public HearingDevicePhoneCallNotificationController(@NonNull Context context) {
- mTelephonyListener = new CallStateListener(context);
mTelephonyManager = context.getSystemService(TelephonyManager.class);
mCallbackExecutor = Executors.newSingleThreadExecutor();
+ mTelephonyListener = new CallStateListener(context, mCallbackExecutor);
}
@VisibleForTesting
@@ -109,14 +109,29 @@
AudioDeviceAttributes.ROLE_INPUT, AudioDeviceInfo.TYPE_BUILTIN_MIC, "");
private final Context mContext;
+ private final Executor mCommDeviceChangedExecutor;
+ private final AudioManager.OnCommunicationDeviceChangedListener mCommDeviceChangedListener;
private NotificationManager mNotificationManager;
private AudioManager mAudioManager;
private BroadcastReceiver mHearingDeviceActionReceiver;
private BluetoothDevice mHearingDevice;
+ private boolean mIsCommDeviceChangedRegistered = false;
private boolean mIsNotificationShown = false;
- CallStateListener(@NonNull Context context) {
+ CallStateListener(@NonNull Context context, @NonNull Executor executor) {
mContext = context;
+ mCommDeviceChangedExecutor = executor;
+ mCommDeviceChangedListener = device -> {
+ if (device == null) {
+ return;
+ }
+ mHearingDevice = getSupportedInputHearingDeviceInfo(List.of(device));
+ if (mHearingDevice != null) {
+ showNotificationIfNeeded();
+ } else {
+ dismissNotificationIfNeeded();
+ }
+ };
}
@Override
@@ -134,6 +149,11 @@
}
if (state == TelephonyManager.CALL_STATE_IDLE) {
+ if (mIsCommDeviceChangedRegistered) {
+ mIsCommDeviceChangedRegistered = false;
+ mAudioManager.removeOnCommunicationDeviceChangedListener(
+ mCommDeviceChangedListener);
+ }
dismissNotificationIfNeeded();
if (mHearingDevice != null) {
@@ -143,10 +163,23 @@
mHearingDevice = null;
}
if (state == TelephonyManager.CALL_STATE_OFFHOOK) {
- mHearingDevice = getSupportedInputHearingDeviceInfo(
- mAudioManager.getAvailableCommunicationDevices());
- if (mHearingDevice != null) {
- showNotificationIfNeeded();
+ if (com.android.server.accessibility.Flags.hearingInputChangeWhenCommDevice()) {
+ AudioDeviceInfo commDevice = mAudioManager.getCommunicationDevice();
+ mHearingDevice = getSupportedInputHearingDeviceInfo(List.of(commDevice));
+ if (mHearingDevice != null) {
+ showNotificationIfNeeded();
+ } else {
+ mAudioManager.addOnCommunicationDeviceChangedListener(
+ mCommDeviceChangedExecutor,
+ mCommDeviceChangedListener);
+ mIsCommDeviceChangedRegistered = true;
+ }
+ } else {
+ mHearingDevice = getSupportedInputHearingDeviceInfo(
+ mAudioManager.getAvailableCommunicationDevices());
+ if (mHearingDevice != null) {
+ showNotificationIfNeeded();
+ }
}
}
}
@@ -264,6 +297,10 @@
PendingIntent.FLAG_IMMUTABLE);
}
case ACTION_BLUETOOTH_DEVICE_DETAILS -> {
+ if (mHearingDevice == null) {
+ return null;
+ }
+
Bundle bundle = new Bundle();
bundle.putString(KEY_BLUETOOTH_ADDRESS, mHearingDevice.getAddress());
intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle);
diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
index cc93d08..a71224a 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
@@ -124,6 +124,22 @@
}
};
+ @VisibleForTesting
+ final AutoclickScrollPanel.ScrollPanelControllerInterface mScrollPanelController =
+ new AutoclickScrollPanel.ScrollPanelControllerInterface() {
+ @Override
+ public void handleScroll(@AutoclickScrollPanel.ScrollDirection int direction) {
+ // TODO(b/388845721): Perform actual scroll.
+ }
+
+ @Override
+ public void exitScrollMode() {
+ if (mAutoclickScrollPanel != null) {
+ mAutoclickScrollPanel.hide();
+ }
+ }
+ };
+
public AutoclickController(Context context, int userId, AccessibilityTraceManager trace) {
mTrace = trace;
mContext = context;
@@ -168,7 +184,8 @@
mWindowManager = mContext.getSystemService(WindowManager.class);
mAutoclickTypePanel =
new AutoclickTypePanel(mContext, mWindowManager, mUserId, clickPanelController);
- mAutoclickScrollPanel = new AutoclickScrollPanel(mContext, mWindowManager);
+ mAutoclickScrollPanel = new AutoclickScrollPanel(mContext, mWindowManager,
+ mScrollPanelController);
mAutoclickTypePanel.show();
mWindowManager.addView(mAutoclickIndicatorView, mAutoclickIndicatorView.getLayoutParams());
diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java
index 86f79a8..e79be50 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java
@@ -18,6 +18,7 @@
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import android.annotation.IntDef;
import android.content.Context;
import android.graphics.PixelFormat;
import android.view.Gravity;
@@ -25,23 +26,97 @@
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowManager;
+import android.widget.ImageButton;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.internal.R;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
public class AutoclickScrollPanel {
+ public static final int DIRECTION_UP = 0;
+ public static final int DIRECTION_DOWN = 1;
+ public static final int DIRECTION_LEFT = 2;
+ public static final int DIRECTION_RIGHT = 3;
+
+ @IntDef({
+ DIRECTION_UP,
+ DIRECTION_DOWN,
+ DIRECTION_LEFT,
+ DIRECTION_RIGHT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ScrollDirection {}
+
private final Context mContext;
private final View mContentView;
private final WindowManager mWindowManager;
+ private ScrollPanelControllerInterface mScrollPanelController;
+
+ // Scroll panel buttons.
+ private final ImageButton mUpButton;
+ private final ImageButton mDownButton;
+ private final ImageButton mLeftButton;
+ private final ImageButton mRightButton;
+ private final ImageButton mExitButton;
+
private boolean mInScrollMode = false;
- public AutoclickScrollPanel(Context context, WindowManager windowManager) {
+ /**
+ * Interface for handling scroll operations.
+ */
+ public interface ScrollPanelControllerInterface {
+ /**
+ * Called when a scroll direction is hovered.
+ *
+ * @param direction The direction to scroll: one of {@link ScrollDirection} values.
+ */
+ void handleScroll(@ScrollDirection int direction);
+
+ /**
+ * Called when the exit button is hovered.
+ */
+ void exitScrollMode();
+ }
+
+ public AutoclickScrollPanel(Context context, WindowManager windowManager,
+ ScrollPanelControllerInterface controller) {
mContext = context;
mWindowManager = windowManager;
+ mScrollPanelController = controller;
mContentView = LayoutInflater.from(context).inflate(
R.layout.accessibility_autoclick_scroll_panel, null);
+
+ // Initialize buttons.
+ mUpButton = mContentView.findViewById(R.id.scroll_up);
+ mLeftButton = mContentView.findViewById(R.id.scroll_left);
+ mRightButton = mContentView.findViewById(R.id.scroll_right);
+ mDownButton = mContentView.findViewById(R.id.scroll_down);
+ mExitButton = mContentView.findViewById(R.id.scroll_exit);
+
+ initializeButtonState();
+ }
+
+ /**
+ * Sets up hover listeners for scroll panel buttons.
+ */
+ private void initializeButtonState() {
+ // Set up hover listeners for direction buttons.
+ setupHoverListenerForDirectionButton(mUpButton, DIRECTION_UP);
+ setupHoverListenerForDirectionButton(mLeftButton, DIRECTION_LEFT);
+ setupHoverListenerForDirectionButton(mRightButton, DIRECTION_RIGHT);
+ setupHoverListenerForDirectionButton(mDownButton, DIRECTION_DOWN);
+
+ // Set up hover listener for exit button.
+ mExitButton.setOnHoverListener((v, event) -> {
+ if (mScrollPanelController != null) {
+ mScrollPanelController.exitScrollMode();
+ }
+ return true;
+ });
}
/**
@@ -67,6 +142,19 @@
}
/**
+ * Sets up a hover listener for a direction button.
+ */
+ private void setupHoverListenerForDirectionButton(ImageButton button,
+ @ScrollDirection int direction) {
+ button.setOnHoverListener((v, event) -> {
+ if (mScrollPanelController != null) {
+ mScrollPanelController.handleScroll(direction);
+ }
+ return true;
+ });
+ }
+
+ /**
* Retrieves the layout params for AutoclickScrollPanel, used when it's added to the Window
* Manager.
*/
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
index fb32943..b02fe27 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
@@ -653,6 +653,14 @@
case ACTION_UP:
handleActionUp(event, rawEvent, policyFlags);
break;
+ case ACTION_POINTER_UP:
+ if (com.android.server.accessibility.Flags
+ .pointerUpMotionEventInTouchExploration()) {
+ if (mState.isServiceDetectingGestures()) {
+ mAms.sendMotionEventToListeningServices(rawEvent);
+ }
+ }
+ break;
default:
break;
}
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
index cd46b38..568abd1 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
@@ -26,6 +26,8 @@
import android.view.MotionEvent;
import android.view.accessibility.AccessibilityEvent;
+import androidx.annotation.VisibleForTesting;
+
import com.android.server.accessibility.AccessibilityManagerService;
/**
@@ -73,7 +75,8 @@
private int mState = STATE_CLEAR;
// Helper class to track received pointers.
// Todo: collapse or hide this class so multiple classes don't modify it.
- private final ReceivedPointerTracker mReceivedPointerTracker;
+ @VisibleForTesting
+ public final ReceivedPointerTracker mReceivedPointerTracker;
// The most recently received motion event.
private MotionEvent mLastReceivedEvent;
// The accompanying raw event without any transformations.
@@ -219,8 +222,19 @@
startTouchInteracting();
break;
case AccessibilityEvent.TYPE_TOUCH_INTERACTION_END:
- setState(STATE_CLEAR);
- // We will clear when we actually handle the next ACTION_DOWN.
+ // When interaction ends, check if there are still down pointers.
+ // If there are any down pointers, go directly to TouchExploring instead.
+ if (com.android.server.accessibility.Flags
+ .pointerUpMotionEventInTouchExploration()) {
+ if (mReceivedPointerTracker.mReceivedPointersDown > 0) {
+ startTouchExploring();
+ } else {
+ setState(STATE_CLEAR);
+ // We will clear when we actually handle the next ACTION_DOWN.
+ }
+ } else {
+ setState(STATE_CLEAR);
+ }
break;
case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START:
startTouchExploring();
@@ -419,7 +433,8 @@
private final PointerDownInfo[] mReceivedPointers = new PointerDownInfo[MAX_POINTER_COUNT];
// Which pointers are down.
- private int mReceivedPointersDown;
+ @VisibleForTesting
+ public int mReceivedPointersDown;
// The edge flags of the last received down event.
private int mLastReceivedDownEdgeFlags;
diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
index 6ccf5e4..5956667 100644
--- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
+++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
@@ -39,6 +39,14 @@
import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__DISPLAY_PRESENTATION_TYPE__INLINE;
import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__DISPLAY_PRESENTATION_TYPE__MENU;
import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__DISPLAY_PRESENTATION_TYPE__UNKNOWN_AUTOFILL_DISPLAY_PRESENTATION_TYPE;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_DELAY_AFTER_ANIMATION_END;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_FILL_DIALOG_DISABLED;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_LAST_TRIGGERED_ID_CHANGED;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_SCREEN_HAS_CREDMAN_FIELD;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_AFTER_DELAY;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_SINCE_IME_ANIMATED;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_UNKNOWN;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_WAIT_FOR_IME_ANIMATION;
import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__ANY_SHOWN;
import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_ACTIVITY_FINISHED;
import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_FILL_REQUEST_FAILED;
@@ -157,8 +165,24 @@
DETECTION_PREFER_PCC
})
@Retention(RetentionPolicy.SOURCE)
- public @interface DetectionPreference {
- }
+ public @interface DetectionPreference {}
+
+ /**
+ * The fill dialog not shown reason. These are wrappers around
+ * {@link com.android.os.AtomsProto.AutofillPresentationEventReported.FillDialogNotShownReason}.
+ */
+ @IntDef(prefix = {"FILL_DIALOG_NOT_SHOWN_REASON"}, value = {
+ FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN,
+ FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED,
+ FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD,
+ FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED,
+ FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION,
+ FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED,
+ FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END,
+ FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_AFTER_DELAY
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FillDialogNotShownReason {}
public static final int NOT_SHOWN_REASON_ANY_SHOWN =
AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__ANY_SHOWN;
@@ -219,6 +243,25 @@
public static final int DETECTION_PREFER_PCC =
AUTOFILL_FILL_RESPONSE_REPORTED__DETECTION_PREFERENCE__DETECTION_PREFER_PCC;
+ // Values for AutofillFillResponseReported.fill_dialog_not_shown_reason
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_UNKNOWN;
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_FILL_DIALOG_DISABLED;
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_SCREEN_HAS_CREDMAN_FIELD;
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_LAST_TRIGGERED_ID_CHANGED;
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_WAIT_FOR_IME_ANIMATION;
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_SINCE_IME_ANIMATED;
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_DELAY_AFTER_ANIMATION_END;
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_AFTER_DELAY =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_AFTER_DELAY;
+
+
private static final int DEFAULT_VALUE_INT = -1;
private final int mSessionId;
@@ -871,6 +914,43 @@
}
/**
+ * Set fill_dialog_not_shown_reason
+ * @param reason
+ */
+ public void maybeSetFillDialogNotShownReason(@FillDialogNotShownReason int reason) {
+ mEventInternal.ifPresent(event -> {
+ if ((event.mFillDialogNotShownReason
+ == FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END
+ || event.mFillDialogNotShownReason
+ == FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION) && reason
+ == FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED) {
+ event.mFillDialogNotShownReason = FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_AFTER_DELAY;
+ } else {
+ event.mFillDialogNotShownReason = reason;
+ }
+ });
+ }
+
+ /**
+ * Set fill_dialog_ready_to_show_ms
+ * @param val
+ */
+ public void maybeSetFillDialogReadyToShowMs(long val) {
+ mEventInternal.ifPresent(event -> {
+ event.mFillDialogReadyToShowMs = (int) (val - mSessionStartTimestamp);
+ });
+ }
+
+ /**
+ * Set ime_animation_finish_ms
+ * @param val
+ */
+ public void maybeSetImeAnimationFinishMs(long val) {
+ mEventInternal.ifPresent(event -> {
+ event.mImeAnimationFinishMs = (int) (val - mSessionStartTimestamp);
+ });
+ }
+ /**
* Set the log contains relayout metrics.
* This is being added as a temporary measure to add logging.
* In future, when we map Session's old view states to the new autofill id's as part of fixing
@@ -959,7 +1039,13 @@
+ " event.notExpiringResponseDuringAuthCount="
+ event.mFixExpireResponseDuringAuthCount
+ " event.notifyViewEnteredIgnoredDuringAuthCount="
- + event.mNotifyViewEnteredIgnoredDuringAuthCount);
+ + event.mNotifyViewEnteredIgnoredDuringAuthCount
+ + " event.fillDialogNotShownReason="
+ + event.mFillDialogNotShownReason
+ + " event.fillDialogReadyToShowMs="
+ + event.mFillDialogReadyToShowMs
+ + " event.imeAnimationFinishMs="
+ + event.mImeAnimationFinishMs);
}
// TODO(b/234185326): Distinguish empty responses from other no presentation reasons.
@@ -1020,7 +1106,10 @@
event.mViewFilledSuccessfullyOnRefillCount,
event.mViewFailedOnRefillCount,
event.mFixExpireResponseDuringAuthCount,
- event.mNotifyViewEnteredIgnoredDuringAuthCount);
+ event.mNotifyViewEnteredIgnoredDuringAuthCount,
+ event.mFillDialogNotShownReason,
+ event.mFillDialogReadyToShowMs,
+ event.mImeAnimationFinishMs);
mEventInternal = Optional.empty();
}
@@ -1087,6 +1176,9 @@
// Following are not logged and used only for internal logic
boolean shouldResetShownCount = false;
boolean mHasRelayoutLog = false;
+ @FillDialogNotShownReason int mFillDialogNotShownReason = FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN;
+ int mFillDialogReadyToShowMs = DEFAULT_VALUE_INT;
+ int mImeAnimationFinishMs = DEFAULT_VALUE_INT;
PresentationStatsEventInternal() {}
}
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 6515b23..ff3bf2a 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -81,6 +81,13 @@
import static com.android.server.autofill.PresentationStatsEventLogger.AUTHENTICATION_RESULT_SUCCESS;
import static com.android.server.autofill.PresentationStatsEventLogger.AUTHENTICATION_TYPE_DATASET_AUTHENTICATION;
import static com.android.server.autofill.PresentationStatsEventLogger.AUTHENTICATION_TYPE_FULL_AUTHENTICATION;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION;
import static com.android.server.autofill.PresentationStatsEventLogger.NOT_SHOWN_REASON_ANY_SHOWN;
import static com.android.server.autofill.PresentationStatsEventLogger.NOT_SHOWN_REASON_NO_FOCUS;
import static com.android.server.autofill.PresentationStatsEventLogger.NOT_SHOWN_REASON_REQUEST_FAILED;
@@ -5622,6 +5629,10 @@
synchronized (mLock) {
final ViewState currentView = mViewStates.get(mCurrentViewId);
currentView.setState(ViewState.STATE_FILL_DIALOG_SHOWN);
+ // Set fill_dialog_not_shown_reason to unknown (a.k.a shown). It is needed due
+ // to possible SHOW_FILL_DIALOG_WAIT.
+ mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+ FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN);
}
// Just show fill dialog once per fill request, so disabled after shown.
// Note: Cannot disable before requestShowFillDialog() because the method
@@ -5725,6 +5736,15 @@
private boolean isFillDialogUiEnabled() {
synchronized (mLock) {
+ if (mSessionFlags.mFillDialogDisabled) {
+ mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+ FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED);
+ }
+ if (mSessionFlags.mScreenHasCredmanField) {
+ // Prefer to log "HAS_CREDMAN_FIELD" over "FILL_DIALOG_DISABLED".
+ mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+ FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD);
+ }
return !mSessionFlags.mFillDialogDisabled && !mSessionFlags.mScreenHasCredmanField;
}
}
@@ -5789,6 +5809,8 @@
|| !ArrayUtils.contains(mLastFillDialogTriggerIds, filledId)) {
// Last fill dialog triggered ids are changed.
if (sDebug) Log.w(TAG, "Last fill dialog triggered ids are changed.");
+ mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+ FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED);
return SHOW_FILL_DIALOG_NO;
}
@@ -5815,6 +5837,8 @@
// we need to wait for animation to happen. We can't return from here yet.
// This is the situation #2 described above.
Log.d(TAG, "Waiting for ime animation to complete before showing fill dialog");
+ mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+ FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION);
mFillDialogRunnable = createFillDialogEvalRunnable(
response, filledId, filterText, flags);
return SHOW_FILL_DIALOG_WAIT;
@@ -5824,9 +5848,15 @@
// max of start input time or the ime finish time
long effectiveDuration = currentTimestampMs
- Math.max(mLastInputStartTime, mImeAnimationFinishTimeMs);
+ mPresentationStatsEventLogger.maybeSetFillDialogReadyToShowMs(
+ currentTimestampMs);
+ mPresentationStatsEventLogger.maybeSetImeAnimationFinishMs(
+ Math.max(mLastInputStartTime, mImeAnimationFinishTimeMs));
if (effectiveDuration >= mFillDialogTimeoutMs) {
Log.d(TAG, "Fill dialog not shown since IME has been up for more time than "
+ mFillDialogTimeoutMs + "ms");
+ mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+ FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED);
return SHOW_FILL_DIALOG_NO;
} else if (effectiveDuration < mFillDialogMinWaitAfterImeAnimationMs) {
// we need to wait for some time after animation ends
@@ -5834,6 +5864,8 @@
response, filledId, filterText, flags);
mHandler.postDelayed(runnable,
mFillDialogMinWaitAfterImeAnimationMs - effectiveDuration);
+ mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+ FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END);
return SHOW_FILL_DIALOG_WAIT;
}
}
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 14d9d3f..decac40 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -122,10 +122,10 @@
}
genrule {
- name: "statslog-mediarouter-java-gen",
- tools: ["stats-log-api-gen"],
- cmd: "$(location stats-log-api-gen) --java $(out) --module mediarouter --javaPackage com.android.server.media --javaClass MediaRouterStatsLog",
- out: ["com/android/server/media/MediaRouterStatsLog.java"],
+ name: "statslog-mediarouter-java-gen",
+ tools: ["stats-log-api-gen"],
+ cmd: "$(location stats-log-api-gen) --java $(out) --module mediarouter --javaPackage com.android.server.media --javaClass MediaRouterStatsLog",
+ out: ["com/android/server/media/MediaRouterStatsLog.java"],
}
java_library_static {
@@ -138,6 +138,7 @@
"ondeviceintelligence_conditionally",
],
srcs: [
+ ":android.hardware.audio.effect-V1-java-source",
":android.hardware.tv.hdmi.connection-V1-java-source",
":android.hardware.tv.hdmi.earc-V1-java-source",
":android.hardware.tv.mediaquality-V1-java-source",
diff --git a/services/core/java/com/android/server/am/ConnectionRecord.java b/services/core/java/com/android/server/am/ConnectionRecord.java
index 31704c4..4e1d77c 100644
--- a/services/core/java/com/android/server/am/ConnectionRecord.java
+++ b/services/core/java/com/android/server/am/ConnectionRecord.java
@@ -142,6 +142,10 @@
| Context.BIND_BYPASS_USER_NETWORK_RESTRICTIONS);
}
+ @Override
+ public boolean transmitsCpuTime() {
+ return !hasFlag(Context.BIND_ALLOW_FREEZE);
+ }
public long getFlags() {
return flags;
@@ -273,6 +277,9 @@
if (hasFlag(Context.BIND_INCLUDE_CAPABILITIES)) {
sb.append("CAPS ");
}
+ if (hasFlag(Context.BIND_ALLOW_FREEZE)) {
+ sb.append("!CPU ");
+ }
if (serviceDead) {
sb.append("DEAD ");
}
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 336a35e..fa35da3 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -2802,7 +2802,7 @@
// we check the final procstate, and remove it if the procsate is below BFGS.
capability |= getBfslCapabilityFromClient(client);
- capability |= getCpuCapabilityFromClient(client);
+ capability |= getCpuCapabilityFromClient(cr, client);
if (cr.notHasFlag(Context.BIND_WAIVE_PRIORITY)) {
if (cr.hasFlag(Context.BIND_INCLUDE_CAPABILITIES)) {
@@ -3259,7 +3259,7 @@
// we check the final procstate, and remove it if the procsate is below BFGS.
capability |= getBfslCapabilityFromClient(client);
- capability |= getCpuCapabilityFromClient(client);
+ capability |= getCpuCapabilityFromClient(conn, client);
if (clientProcState >= PROCESS_STATE_CACHED_ACTIVITY) {
// If the other app is cached for any reason, for purposes here
@@ -3502,10 +3502,13 @@
/**
* @return the CPU capability from a client (of a service binding or provider).
*/
- private static int getCpuCapabilityFromClient(ProcessRecord client) {
- // Just grant CPU capability every time
- // TODO(b/370817323): Populate with reasons to not propagate cpu capability across bindings.
- return client.mState.getCurCapability() & PROCESS_CAPABILITY_CPU_TIME;
+ private static int getCpuCapabilityFromClient(OomAdjusterModernImpl.Connection conn,
+ ProcessRecord client) {
+ if (conn == null || conn.transmitsCpuTime()) {
+ return client.mState.getCurCapability() & PROCESS_CAPABILITY_CPU_TIME;
+ } else {
+ return 0;
+ }
}
/**
diff --git a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
index 1b7e8f0..7e7b568 100644
--- a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
+++ b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
@@ -635,6 +635,15 @@
* Returns true if this connection can propagate capabilities.
*/
boolean canAffectCapabilities();
+
+ /**
+ * Returns whether this connection transmits PROCESS_CAPABILITY_CPU_TIME to the host, if the
+ * client possesses it.
+ */
+ default boolean transmitsCpuTime() {
+ // Always lend this capability by default.
+ return true;
+ }
}
/**
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 4b5f06b..8ef79a91 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -1472,8 +1472,8 @@
mAudioService.postAccessoryPlugMediaUnmute(device);
}
- /*package*/ int getVolumeForDeviceIgnoreMute(int streamType, int device) {
- return mAudioService.getVolumeForDeviceIgnoreMute(streamType, device);
+ /*package*/ int getVssVolumeForDevice(int streamType, int device) {
+ return mAudioService.getVssVolumeForDevice(streamType, device);
}
/*package*/ int getMaxVssVolumeForStream(int streamType) {
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 2e6d984..829d9ea 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -2482,7 +2482,7 @@
@GuardedBy("mDevicesLock")
private void makeHearingAidDeviceAvailable(
String address, String name, int streamType, String eventSource) {
- final int hearingAidVolIndex = mDeviceBroker.getVolumeForDeviceIgnoreMute(streamType,
+ final int hearingAidVolIndex = mDeviceBroker.getVssVolumeForDevice(streamType,
DEVICE_OUT_HEARING_AID);
mDeviceBroker.postSetHearingAidVolumeIndex(hearingAidVolIndex, streamType);
@@ -2672,7 +2672,7 @@
}
final int leAudioVolIndex = (volumeIndex == -1)
- ? mDeviceBroker.getVolumeForDeviceIgnoreMute(streamType, device)
+ ? mDeviceBroker.getVssVolumeForDevice(streamType, device)
: volumeIndex;
final int maxIndex = mDeviceBroker.getMaxVssVolumeForStream(streamType);
mDeviceBroker.postSetLeAudioVolumeIndex(leAudioVolIndex, maxIndex, streamType);
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index a43e4d9..7664561 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -529,7 +529,7 @@
*/
private InputDeviceVolumeHelper mInputDeviceVolumeHelper;
- /*package*/ int getVolumeForDeviceIgnoreMute(int stream, int device) {
+ /*package*/ int getVssVolumeForDevice(int stream, int device) {
final VolumeStreamState streamState = mStreamStates.get(stream);
return streamState != null ? streamState.getIndex(device) : -1;
}
@@ -4997,6 +4997,8 @@
pw.println("\tcom.android.media.audio.disablePrescaleAbsoluteVolume:"
+ disablePrescaleAbsoluteVolume());
pw.println("\tcom.android.media.audio.setStreamVolumeOrder - EOL");
+ pw.println("\tandroid.media.audio.ringtoneUserUriCheck:"
+ + android.media.audio.Flags.ringtoneUserUriCheck());
pw.println("\tandroid.media.audio.roForegroundAudioControl:"
+ roForegroundAudioControl());
pw.println("\tandroid.media.audio.scoManagedByAudio:"
@@ -5098,7 +5100,7 @@
}
final int device = absVolumeDevices.toArray(new Integer[0])[0].intValue();
- final int index = getVolumeForDeviceIgnoreMute(streamType, device);
+ final int index = getStreamVolume(streamType, device);
if (DEBUG_VOL) {
Slog.i(TAG, "onUpdateContextualVolumes streamType: " + streamType
diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java
index 67afff7..643f330 100644
--- a/services/core/java/com/android/server/audio/SoundDoseHelper.java
+++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java
@@ -724,7 +724,7 @@
int device = mAudioService.getDeviceForStream(AudioSystem.STREAM_MUSIC);
if (safeDevicesContains(device) && isStreamActive) {
scheduleMusicActiveCheck();
- int index = mAudioService.getVolumeForDeviceIgnoreMute(AudioSystem.STREAM_MUSIC,
+ int index = mAudioService.getVssVolumeForDevice(AudioSystem.STREAM_MUSIC,
device);
if (index > safeMediaVolumeIndex(device)) {
// Approximate cumulative active music time
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index c2fecf2..d9db178 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -568,6 +568,7 @@
}
mWindowManagerCallbacks = callbacks;
registerLidSwitchCallbackInternal(mWindowManagerCallbacks);
+ mKeyGestureController.setWindowManagerCallbacks(callbacks);
}
public void setWiredAccessoryCallbacks(WiredAccessoryCallbacks callbacks) {
@@ -2756,24 +2757,6 @@
@Nullable IBinder focussedToken) {
return InputManagerService.this.handleKeyGestureEvent(event);
}
-
- @Override
- public boolean isKeyGestureSupported(int gestureType) {
- switch (gestureType) {
- case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP:
- case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN:
- case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS:
- return true;
- default:
- return false;
-
- }
- }
});
}
@@ -3371,6 +3354,11 @@
*/
@Nullable
SurfaceControl createSurfaceForGestureMonitor(String name, int displayId);
+
+ /**
+ * Provide information on whether the keyguard is currently locked or not.
+ */
+ boolean isKeyguardLocked(int displayId);
}
/**
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index ef5babf..395c773 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -62,8 +62,10 @@
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
+import android.view.ViewConfiguration;
import com.android.internal.R;
+import com.android.internal.accessibility.AccessibilityShortcutController;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.IShortcutService;
@@ -104,6 +106,7 @@
private static final int MSG_NOTIFY_KEY_GESTURE_EVENT = 1;
private static final int MSG_PERSIST_CUSTOM_GESTURES = 2;
private static final int MSG_LOAD_CUSTOM_GESTURES = 3;
+ private static final int MSG_ACCESSIBILITY_SHORTCUT = 4;
// must match: config_settingsKeyBehavior in config.xml
private static final int SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY = 0;
@@ -122,12 +125,15 @@
static final int POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS = 2;
private final Context mContext;
+ private InputManagerService.WindowManagerCallbacks mWindowManagerCallbacks;
private final Handler mHandler;
private final Handler mIoHandler;
private final int mSystemPid;
private final KeyCombinationManager mKeyCombinationManager;
private final SettingsObserver mSettingsObserver;
private final AppLaunchShortcutManager mAppLaunchShortcutManager;
+ @VisibleForTesting
+ final AccessibilityShortcutController mAccessibilityShortcutController;
private final InputGestureManager mInputGestureManager;
private final DisplayManager mDisplayManager;
@GuardedBy("mInputDataStore")
@@ -175,8 +181,14 @@
private final boolean mVisibleBackgroundUsersEnabled = isVisibleBackgroundUsersEnabled();
- KeyGestureController(Context context, Looper looper, Looper ioLooper,
+ public KeyGestureController(Context context, Looper looper, Looper ioLooper,
InputDataStore inputDataStore) {
+ this(context, looper, ioLooper, inputDataStore, new Injector());
+ }
+
+ @VisibleForTesting
+ KeyGestureController(Context context, Looper looper, Looper ioLooper,
+ InputDataStore inputDataStore, Injector injector) {
mContext = context;
mHandler = new Handler(looper, this::handleMessage);
mIoHandler = new Handler(ioLooper, this::handleIoMessage);
@@ -197,6 +209,8 @@
mSettingsObserver = new SettingsObserver(mHandler);
mAppLaunchShortcutManager = new AppLaunchShortcutManager(mContext);
mInputGestureManager = new InputGestureManager(mContext);
+ mAccessibilityShortcutController = injector.getAccessibilityShortcutController(mContext,
+ mHandler);
mDisplayManager = Objects.requireNonNull(mContext.getSystemService(DisplayManager.class));
mInputDataStore = inputDataStore;
mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
@@ -295,8 +309,8 @@
KeyEvent.KEYCODE_VOLUME_UP) {
@Override
public boolean preCondition() {
- return isKeyGestureSupported(
- KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD);
+ return mAccessibilityShortcutController.isAccessibilityShortcutAvailable(
+ mWindowManagerCallbacks.isKeyguardLocked(DEFAULT_DISPLAY));
}
@Override
@@ -376,15 +390,15 @@
KeyEvent.KEYCODE_DPAD_DOWN) {
@Override
public boolean preCondition() {
- return isKeyGestureSupported(
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD);
+ return mAccessibilityShortcutController
+ .isAccessibilityShortcutAvailable(false);
}
@Override
public void execute() {
handleMultiKeyGesture(
new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN},
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
KeyGestureEvent.ACTION_GESTURE_START, 0);
}
@@ -392,7 +406,7 @@
public void cancel() {
handleMultiKeyGesture(
new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN},
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
KeyGestureEvent.ACTION_GESTURE_COMPLETE,
KeyGestureEvent.FLAG_CANCELLED);
}
@@ -438,6 +452,7 @@
mSettingsObserver.observe();
mAppLaunchShortcutManager.systemRunning();
mInputGestureManager.systemRunning();
+ initKeyGestures();
int userId;
synchronized (mUserLock) {
@@ -447,6 +462,27 @@
mIoHandler.obtainMessage(MSG_LOAD_CUSTOM_GESTURES, userId).sendToTarget();
}
+ @SuppressLint("MissingPermission")
+ private void initKeyGestures() {
+ InputManager im = Objects.requireNonNull(mContext.getSystemService(InputManager.class));
+ im.registerKeyGestureEventHandler((event, focusedToken) -> {
+ switch (event.getKeyGestureType()) {
+ case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
+ if (event.getAction() == KeyGestureEvent.ACTION_GESTURE_START) {
+ mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT);
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_ACCESSIBILITY_SHORTCUT),
+ getAccessibilityShortcutTimeout());
+ } else {
+ mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT);
+ }
+ return true;
+ default:
+ return false;
+ }
+ });
+ }
+
public boolean interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
if (mVisibleBackgroundUsersEnabled && shouldIgnoreKeyEventForVisibleBackgroundUser(event)) {
return false;
@@ -971,17 +1007,6 @@
return false;
}
- private boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) {
- synchronized (mKeyGestureHandlerRecords) {
- for (KeyGestureHandlerRecord handler : mKeyGestureHandlerRecords.values()) {
- if (handler.isKeyGestureSupported(gestureType)) {
- return true;
- }
- }
- }
- return false;
- }
-
public void notifyKeyGestureCompleted(int deviceId, int[] keycodes, int modifierState,
@KeyGestureEvent.KeyGestureType int gestureType) {
// TODO(b/358569822): Once we move the gesture detection logic to IMS, we ideally
@@ -1019,9 +1044,16 @@
synchronized (mUserLock) {
mCurrentUserId = userId;
}
+ mAccessibilityShortcutController.setCurrentUser(userId);
mIoHandler.obtainMessage(MSG_LOAD_CUSTOM_GESTURES, userId).sendToTarget();
}
+
+ public void setWindowManagerCallbacks(
+ @NonNull InputManagerService.WindowManagerCallbacks callbacks) {
+ mWindowManagerCallbacks = callbacks;
+ }
+
private boolean isDefaultDisplayOn() {
Display defaultDisplay = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
if (defaultDisplay == null) {
@@ -1068,6 +1100,9 @@
AidlKeyGestureEvent event = (AidlKeyGestureEvent) msg.obj;
notifyKeyGestureEvent(event);
break;
+ case MSG_ACCESSIBILITY_SHORTCUT:
+ mAccessibilityShortcutController.performAccessibilityShortcut();
+ break;
}
return true;
}
@@ -1347,17 +1382,6 @@
}
return false;
}
-
- public boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) {
- try {
- return mKeyGestureHandler.isKeyGestureSupported(gestureType);
- } catch (RemoteException ex) {
- Slog.w(TAG, "Failed to identify if key gesture type is supported by the "
- + "process " + mPid + ", assuming it died.", ex);
- binderDied();
- }
- return false;
- }
}
private class SettingsObserver extends ContentObserver {
@@ -1413,6 +1437,25 @@
return event;
}
+ private long getAccessibilityShortcutTimeout() {
+ synchronized (mUserLock) {
+ final ViewConfiguration config = ViewConfiguration.get(mContext);
+ final boolean hasDialogShown = Settings.Secure.getIntForUser(
+ mContext.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, mCurrentUserId) != 0;
+ final boolean skipTimeoutRestriction =
+ Settings.Secure.getIntForUser(mContext.getContentResolver(),
+ Settings.Secure.SKIP_ACCESSIBILITY_SHORTCUT_DIALOG_TIMEOUT_RESTRICTION,
+ 0, mCurrentUserId) != 0;
+
+ // If users manually set the volume key shortcut for any accessibility service, the
+ // system would bypass the timeout restriction of the shortcut dialog.
+ return hasDialogShown || skipTimeoutRestriction
+ ? config.getAccessibilityShortcutKeyTimeoutAfterConfirmation()
+ : config.getAccessibilityShortcutKeyTimeout();
+ }
+ }
+
public void dump(IndentingPrintWriter ipw) {
ipw.println("KeyGestureController:");
ipw.increaseIndent();
@@ -1459,4 +1502,12 @@
mAppLaunchShortcutManager.dump(ipw);
mInputGestureManager.dump(ipw);
}
+
+ @VisibleForTesting
+ static class Injector {
+ AccessibilityShortcutController getAccessibilityShortcutController(Context context,
+ Handler handler) {
+ return new AccessibilityShortcutController(context, handler, UserHandle.USER_SYSTEM);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java
index 02987a9..15f186b0 100644
--- a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java
+++ b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java
@@ -132,8 +132,8 @@
@Nullable EditorInfo editorInfo, IRemoteInputConnection inputConnection,
IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
int unverifiedTargetSdkVersion, @UserIdInt int userId,
- @NonNull ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq,
- boolean useAsyncShowHideMethod);
+ @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible,
+ int startInputSeq, boolean useAsyncShowHideMethod);
InputBindResult startInputOrWindowGainedFocus(
@StartInputReason int startInputReason, IInputMethodClient client,
@@ -142,7 +142,7 @@
@Nullable EditorInfo editorInfo, IRemoteInputConnection inputConnection,
IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
int unverifiedTargetSdkVersion, @UserIdInt int userId,
- @NonNull ImeOnBackInvokedDispatcher imeDispatcher);
+ @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible);
void showInputMethodPickerFromClient(IInputMethodClient client, int auxiliarySubtypeMode);
@@ -324,11 +324,11 @@
IRemoteInputConnection inputConnection,
IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
int unverifiedTargetSdkVersion, @UserIdInt int userId,
- @NonNull ImeOnBackInvokedDispatcher imeDispatcher) {
+ @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible) {
return mCallback.startInputOrWindowGainedFocus(
startInputReason, client, windowToken, startInputFlags, softInputMode,
windowFlags, editorInfo, inputConnection, remoteAccessibilityInputConnection,
- unverifiedTargetSdkVersion, userId, imeDispatcher);
+ unverifiedTargetSdkVersion, userId, imeDispatcher, imeRequestedVisible);
}
@Override
@@ -340,13 +340,13 @@
IRemoteInputConnection inputConnection,
IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
int unverifiedTargetSdkVersion, @UserIdInt int userId,
- @NonNull ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq,
- boolean useAsyncShowHideMethod) {
+ @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible,
+ int startInputSeq, boolean useAsyncShowHideMethod) {
mCallback.startInputOrWindowGainedFocusAsync(
startInputReason, client, windowToken, startInputFlags, softInputMode,
windowFlags, editorInfo, inputConnection, remoteAccessibilityInputConnection,
- unverifiedTargetSdkVersion, userId, imeDispatcher, startInputSeq,
- useAsyncShowHideMethod);
+ unverifiedTargetSdkVersion, userId, imeDispatcher, imeRequestedVisible,
+ startInputSeq, useAsyncShowHideMethod);
}
@Override
diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
index 69353be..2c07a31 100644
--- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
+++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
@@ -443,7 +443,8 @@
}
@GuardedBy("ImfLock.class")
- ImeVisibilityResult computeState(ImeTargetWindowState state, boolean allowVisible) {
+ ImeVisibilityResult computeState(ImeTargetWindowState state, boolean allowVisible,
+ boolean imeRequestedVisible) {
// TODO: Output the request IME visibility state according to the requested window state
final int softInputVisibility = state.mSoftInputModeState & SOFT_INPUT_MASK_STATE;
// Should we auto-show the IME even if the caller has not
@@ -576,7 +577,8 @@
SoftInputShowHideReason.HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR);
}
}
- if (!state.hasEditorFocused() && mInputShown && state.isStartInputByGainFocus()
+ if (!state.hasEditorFocused() && (mInputShown || (Flags.refactorInsetsController()
+ && imeRequestedVisible)) && state.isStartInputByGainFocus()
&& mService.mInputMethodDeviceConfigs.shouldHideImeWhenNoEditorFocus()) {
// Hide the soft-keyboard when the system do nothing for softInputModeState
// of the window being gained focus without an editor. This behavior benefits
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 68ad8f7..2375775 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -3725,8 +3725,8 @@
IRemoteInputConnection inputConnection,
IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
int unverifiedTargetSdkVersion, @UserIdInt int userId,
- @NonNull ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq,
- boolean useAsyncShowHideMethod) {
+ @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible,
+ int startInputSeq, boolean useAsyncShowHideMethod) {
// implemented by ZeroJankProxy
}
@@ -3739,7 +3739,7 @@
IRemoteInputConnection inputConnection,
IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
int unverifiedTargetSdkVersion, @UserIdInt int userId,
- @NonNull ImeOnBackInvokedDispatcher imeDispatcher) {
+ @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible) {
if (UserHandle.getCallingUserId() != userId) {
mContext.enforceCallingOrSelfPermission(
Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
@@ -3870,7 +3870,8 @@
result = startInputOrWindowGainedFocusInternalLocked(startInputReason,
client, windowToken, startInputFlags, softInputMode, windowFlags,
editorInfo, inputConnection, remoteAccessibilityInputConnection,
- unverifiedTargetSdkVersion, bindingController, imeDispatcher, cs);
+ unverifiedTargetSdkVersion, bindingController, imeDispatcher, cs,
+ imeRequestedVisible);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -3899,7 +3900,8 @@
IRemoteInputConnection inputContext,
@Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
int unverifiedTargetSdkVersion, @NonNull InputMethodBindingController bindingController,
- @NonNull ImeOnBackInvokedDispatcher imeDispatcher, @NonNull ClientState cs) {
+ @NonNull ImeOnBackInvokedDispatcher imeDispatcher, @NonNull ClientState cs,
+ boolean imeRequestedVisible) {
ProtoLog.v(IMMS_DEBUG, "startInputOrWindowGainedFocusInternalLocked: reason=%s"
+ " client=%s"
+ " inputContext=%s"
@@ -3910,12 +3912,13 @@
+ " unverifiedTargetSdkVersion=%s"
+ " bindingController=%s"
+ " imeDispatcher=%s"
- + " cs=%s",
+ + " cs=%s"
+ + " imeRequestedVisible=%s",
InputMethodDebug.startInputReasonToString(startInputReason), client.asBinder(),
inputContext, editorInfo, InputMethodDebug.startInputFlagsToString(startInputFlags),
InputMethodDebug.softInputModeToString(softInputMode),
Integer.toHexString(windowFlags), unverifiedTargetSdkVersion, bindingController,
- imeDispatcher, cs);
+ imeDispatcher, cs, imeRequestedVisible);
final int userId = bindingController.getUserId();
final var userData = getUserData(userId);
@@ -3963,7 +3966,8 @@
InputBindResult res = null;
final ImeVisibilityResult imeVisRes = visibilityStateComputer.computeState(windowState,
- isSoftInputModeStateVisibleAllowed(unverifiedTargetSdkVersion, startInputFlags));
+ isSoftInputModeStateVisibleAllowed(unverifiedTargetSdkVersion, startInputFlags),
+ imeRequestedVisible);
if (imeVisRes != null) {
boolean isShow = false;
switch (imeVisRes.getReason()) {
diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
index 7252925..12c1d9c 100644
--- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
+++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
@@ -234,15 +234,15 @@
IRemoteInputConnection inputConnection,
IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
int unverifiedTargetSdkVersion, @UserIdInt int userId,
- @NonNull ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq,
- boolean useAsyncShowHideMethod) {
+ @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible,
+ int startInputSeq, boolean useAsyncShowHideMethod) {
offload(() -> {
InputBindResult result = mInner.startInputOrWindowGainedFocus(startInputReason, client,
windowToken, startInputFlags, softInputMode, windowFlags,
editorInfo,
inputConnection, remoteAccessibilityInputConnection,
unverifiedTargetSdkVersion,
- userId, imeDispatcher);
+ userId, imeDispatcher, imeRequestedVisible);
sendOnStartInputResult(client, result, startInputSeq);
// For first-time client bind, MSG_BIND should arrive after MSG_START_INPUT_RESULT.
if (result.result == InputBindResult.ResultCode.SUCCESS_WAITING_IME_SESSION) {
@@ -269,7 +269,7 @@
IRemoteInputConnection inputConnection,
IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
int unverifiedTargetSdkVersion, @UserIdInt int userId,
- @NonNull ImeOnBackInvokedDispatcher imeDispatcher) {
+ @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible) {
// Should never be called when flag is enabled i.e. when this proxy is used.
return null;
}
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java
index 9e38435..ad108f6 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityService.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java
@@ -28,6 +28,7 @@
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
+import android.hardware.audio.effect.DefaultExtension;
import android.hardware.tv.mediaquality.AmbientBacklightColorFormat;
import android.hardware.tv.mediaquality.IMediaQuality;
import android.hardware.tv.mediaquality.IPictureProfileAdjustmentListener;
@@ -57,6 +58,7 @@
import android.os.Bundle;
import android.os.Environment;
import android.os.IBinder;
+import android.os.Parcel;
import android.os.PersistableBundle;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
@@ -365,13 +367,21 @@
try {
if (mMediaQuality != null) {
+ PictureParameters pp = new PictureParameters();
PictureParameter[] pictureParameters = MediaQualityUtils
.convertPersistableBundleToPictureParameterList(params);
- PictureParameters pp = new PictureParameters();
+ PersistableBundle vendorPictureParameters = params
+ .getPersistableBundle(BaseParameters.VENDOR_PARAMETERS);
+ Parcel parcel = Parcel.obtain();
+ if (vendorPictureParameters != null) {
+ setVendorPictureParameters(pp, parcel, vendorPictureParameters);
+ }
+
pp.pictureParameters = pictureParameters;
mMediaQuality.sendDefaultPictureParameters(pp);
+ parcel.recycle();
return true;
}
} catch (RemoteException e) {
@@ -1419,11 +1429,19 @@
MediaQualityUtils.convertPersistableBundleToPictureParameterList(
params);
+ PersistableBundle vendorPictureParameters = params
+ .getPersistableBundle(BaseParameters.VENDOR_PARAMETERS);
+ Parcel parcel = Parcel.obtain();
+ if (vendorPictureParameters != null) {
+ setVendorPictureParameters(pictureParameters, parcel, vendorPictureParameters);
+ }
+
android.hardware.tv.mediaquality.PictureProfile toReturn =
new android.hardware.tv.mediaquality.PictureProfile();
toReturn.pictureProfileId = id;
toReturn.parameters = pictureParameters;
+ parcel.recycle();
return toReturn;
}
@@ -1729,4 +1747,16 @@
return android.hardware.tv.mediaquality.IMediaQualityCallback.Stub.VERSION;
}
}
+
+ private void setVendorPictureParameters(
+ PictureParameters pictureParameters,
+ Parcel parcel,
+ PersistableBundle vendorPictureParameters) {
+ vendorPictureParameters.writeToParcel(parcel, 0);
+ byte[] vendorBundleToByteArray = parcel.marshall();
+ DefaultExtension defaultExtension = new DefaultExtension();
+ defaultExtension.bytes = Arrays.copyOf(
+ vendorBundleToByteArray, vendorBundleToByteArray.length);
+ pictureParameters.vendorPictureParameters.setParcelable(defaultExtension);
+ }
}
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 46dc758..3230e89 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -4240,66 +4240,14 @@
if (!useKeyGestureEventHandler()) {
return;
}
- mInputManager.registerKeyGestureEventHandler(new InputManager.KeyGestureEventHandler() {
- @Override
- public boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event,
- @Nullable IBinder focusedToken) {
- boolean handled = PhoneWindowManager.this.handleKeyGestureEvent(event,
- focusedToken);
- if (handled && !event.isCancelled() && Arrays.stream(event.getKeycodes()).anyMatch(
- (keycode) -> keycode == KeyEvent.KEYCODE_POWER)) {
- mPowerKeyHandled = true;
- }
- return handled;
+ mInputManager.registerKeyGestureEventHandler((event, focusedToken) -> {
+ boolean handled = PhoneWindowManager.this.handleKeyGestureEvent(event,
+ focusedToken);
+ if (handled && !event.isCancelled() && Arrays.stream(event.getKeycodes()).anyMatch(
+ (keycode) -> keycode == KeyEvent.KEYCODE_POWER)) {
+ mPowerKeyHandled = true;
}
-
- @Override
- public boolean isKeyGestureSupported(int gestureType) {
- switch (gestureType) {
- case KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH:
- case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_HOME:
- case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_BACK:
- case KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION:
- case KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE:
- case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER:
- case KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP:
- case KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN:
- case KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER:
- case KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH:
- case KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH:
- case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB:
- case KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD:
- case KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD:
- case KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS:
- return true;
- case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
- return mAccessibilityShortcutController.isAccessibilityShortcutAvailable(
- isKeyguardLocked());
- case KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
- return mAccessibilityShortcutController.isAccessibilityShortcutAvailable(
- false);
- default:
- return false;
- }
- }
+ return handled;
});
}
@@ -4457,13 +4405,6 @@
cancelPendingScreenshotChordAction();
}
return true;
- case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
- if (start) {
- interceptAccessibilityShortcutChord();
- } else {
- cancelPendingAccessibilityShortcutAction();
- }
- return true;
case KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD:
if (start) {
interceptRingerToggleChord();
@@ -4481,14 +4422,6 @@
cancelGlobalActionsAction();
}
return true;
- // TODO (b/358569822): Consolidate TV and non-TV gestures into same KeyGestureEvent
- case KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
- if (start) {
- interceptAccessibilityGestureTv();
- } else {
- cancelAccessibilityGestureTv();
- }
- return true;
case KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
if (start) {
interceptBugreportGestureTv();
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index 1299a4d..f243d4f 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -153,7 +153,7 @@
final DisplayContent dc = mService.mRoot.getDisplayContent(displayId);
if (dc != null) {
final Display display = dc.getDisplay();
- if (display != null && display.getType() != Display.TYPE_OVERLAY) {
+ if (display != null) {
final DisplayMagnifier magnifier = new DisplayMagnifier(
mService, dc, display, callbacks);
magnifier.notifyImeWindowVisibilityChanged(
diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java
index 3f24da9..51025d2 100644
--- a/services/core/java/com/android/server/wm/ActivityStartController.java
+++ b/services/core/java/com/android/server/wm/ActivityStartController.java
@@ -60,6 +60,7 @@
import com.android.server.wm.ActivityStarter.Factory;
import java.io.PrintWriter;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -97,6 +98,9 @@
/** Whether an {@link ActivityStarter} is currently executing (starting an Activity). */
private boolean mInExecution = false;
+ /** The {@link TaskDisplayArea}s that are currently starting home activity. */
+ private ArrayList<TaskDisplayArea> mHomeLaunchingTaskDisplayAreas = new ArrayList<>();
+
/**
* TODO(b/64750076): Capture information necessary for dump and
* {@link #postStartActivityProcessingForLastStarter} rather than keeping the entire object
@@ -162,6 +166,11 @@
void startHomeActivity(Intent intent, ActivityInfo aInfo, String reason,
TaskDisplayArea taskDisplayArea) {
+ if (mHomeLaunchingTaskDisplayAreas.contains(taskDisplayArea)) {
+ Slog.e(TAG, "Abort starting home on " + taskDisplayArea + " recursively.");
+ return;
+ }
+
final ActivityOptions options = ActivityOptions.makeBasic();
options.setLaunchWindowingMode(WINDOWING_MODE_FULLSCREEN);
if (!ActivityRecord.isResolverActivity(aInfo.name)) {
@@ -186,13 +195,18 @@
mSupervisor.endDeferResume();
}
- mLastHomeActivityStartResult = obtainStarter(intent, "startHomeActivity: " + reason)
- .setOutActivity(tmpOutRecord)
- .setCallingUid(0)
- .setActivityInfo(aInfo)
- .setActivityOptions(options.toBundle(),
- Binder.getCallingPid(), Binder.getCallingUid())
- .execute();
+ try {
+ mHomeLaunchingTaskDisplayAreas.add(taskDisplayArea);
+ mLastHomeActivityStartResult = obtainStarter(intent, "startHomeActivity: " + reason)
+ .setOutActivity(tmpOutRecord)
+ .setCallingUid(0)
+ .setActivityInfo(aInfo)
+ .setActivityOptions(options.toBundle(),
+ Binder.getCallingPid(), Binder.getCallingUid())
+ .execute();
+ } finally {
+ mHomeLaunchingTaskDisplayAreas.remove(taskDisplayArea);
+ }
mLastHomeActivityStartRecord = tmpOutRecord[0];
if (rootHomeTask.mInResumeTopActivity) {
// If we are in resume section already, home activity will be initialized, but not
@@ -479,9 +493,9 @@
}
} catch (SecurityException securityException) {
ActivityStarter.logAndThrowExceptionForIntentRedirect(mService.mContext,
- "Creator URI Grant Caused Exception.", intent, creatorUid,
- creatorPackage, filterCallingUid, callingPackage,
- securityException);
+ ActivityStarter.INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION,
+ intent, creatorUid, creatorPackage, filterCallingUid,
+ callingPackage, securityException);
}
}
if ((aInfo.applicationInfo.privateFlags
@@ -720,6 +734,12 @@
}
}
+ if (!mHomeLaunchingTaskDisplayAreas.isEmpty()) {
+ dumped = true;
+ pw.print(prefix);
+ pw.println("mHomeLaunchingTaskDisplayAreas:" + mHomeLaunchingTaskDisplayAreas);
+ }
+
if (!dumped) {
pw.print(prefix);
pw.println("(nothing)");
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 233f913..a84a008 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -65,6 +65,7 @@
import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_CONFIGURATION;
import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_TASKS;
import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS;
+import static com.android.internal.util.FrameworkStatsLog.INTENT_REDIRECT_BLOCKED;
import static com.android.server.pm.PackageArchiver.isArchivingEnabled;
import static com.android.server.wm.ActivityRecord.State.RESUMED;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_PERMISSIONS_REVIEW;
@@ -140,6 +141,7 @@
import com.android.internal.app.HeavyWeightSwitcherActivity;
import com.android.internal.app.IVoiceInteractor;
import com.android.internal.protolog.ProtoLog;
+import com.android.internal.util.FrameworkStatsLog;
import com.android.server.UiThread;
import com.android.server.am.ActivityManagerService.IntentCreatorToken;
import com.android.server.am.PendingIntentRecord;
@@ -623,7 +625,7 @@
if ((intent.getExtendedFlags() & Intent.EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN)
!= 0) {
logAndThrowExceptionForIntentRedirect(supervisor.mService.mContext,
- "Unparceled intent does not have a creator token set.", intent,
+ ActivityStarter.INTENT_REDIRECT_EXCEPTION_MISSING_OR_INVALID_TOKEN, intent,
intentCreatorUid, intentCreatorPackage, resolvedCallingUid,
resolvedCallingPackage, null);
}
@@ -659,9 +661,9 @@
}
} catch (SecurityException securityException) {
logAndThrowExceptionForIntentRedirect(supervisor.mService.mContext,
- "Creator URI Grant Caused Exception.", intent, intentCreatorUid,
- intentCreatorPackage, resolvedCallingUid,
- resolvedCallingPackage, securityException);
+ ActivityStarter.INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION,
+ intent, intentCreatorUid, intentCreatorPackage,
+ resolvedCallingUid, resolvedCallingPackage, securityException);
}
}
} else {
@@ -683,9 +685,9 @@
}
} catch (SecurityException securityException) {
logAndThrowExceptionForIntentRedirect(supervisor.mService.mContext,
- "Creator URI Grant Caused Exception.", intent, intentCreatorUid,
- intentCreatorPackage, resolvedCallingUid,
- resolvedCallingPackage, securityException);
+ ActivityStarter.INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION,
+ intent, intentCreatorUid, intentCreatorPackage,
+ resolvedCallingUid, resolvedCallingPackage, securityException);
}
}
}
@@ -1109,8 +1111,11 @@
if (sourceRecord != null) {
if (requestCode >= 0 && !sourceRecord.finishing) {
resultRecord = sourceRecord;
+ request.logMessage.append(" (rr=");
+ } else {
+ request.logMessage.append(" (sr=");
}
- request.logMessage.append(" (sr=" + System.identityHashCode(sourceRecord) + ")");
+ request.logMessage.append(System.identityHashCode(sourceRecord) + ")");
}
}
@@ -1261,27 +1266,27 @@
request.ignoreTargetSecurity, inTask != null, null, resultRecord,
resultRootTask)) {
abort = logAndAbortForIntentRedirect(mService.mContext,
- "Creator checkStartAnyActivityPermission Caused abortion.",
+ ActivityStarter.INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION,
intent, intentCreatorUid, intentCreatorPackage, callingUid,
callingPackage);
}
} catch (SecurityException e) {
logAndThrowExceptionForIntentRedirect(mService.mContext,
- "Creator checkStartAnyActivityPermission Caused Exception.",
+ ActivityStarter.INTENT_REDIRECT_EXCEPTION_START_ANY_ACTIVITY_PERMISSION,
intent, intentCreatorUid, intentCreatorPackage, callingUid, callingPackage,
e);
}
if (!mService.mIntentFirewall.checkStartActivity(intent, intentCreatorUid,
0, resolvedType, aInfo.applicationInfo)) {
abort = logAndAbortForIntentRedirect(mService.mContext,
- "Creator IntentFirewall.checkStartActivity Caused abortion.",
+ ActivityStarter.INTENT_REDIRECT_ABORT_INTENT_FIREWALL_START_ACTIVITY,
intent, intentCreatorUid, intentCreatorPackage, callingUid, callingPackage);
}
if (!mService.getPermissionPolicyInternal().checkStartActivity(intent,
intentCreatorUid, intentCreatorPackage)) {
abort = logAndAbortForIntentRedirect(mService.mContext,
- "Creator PermissionPolicyService.checkStartActivity Caused abortion.",
+ ActivityStarter.INTENT_REDIRECT_ABORT_PERMISSION_POLICY_START_ACTIVITY,
intent, intentCreatorUid, intentCreatorPackage, callingUid, callingPackage);
}
}
@@ -3626,13 +3631,41 @@
pw.println(mInTaskFragment);
}
+ /**
+ * Error codes for intent redirect.
+ *
+ * @hide
+ */
+ @IntDef(prefix = {"INTENT_REDIRECT_"}, value = {
+ INTENT_REDIRECT_EXCEPTION_MISSING_OR_INVALID_TOKEN,
+ INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION,
+ INTENT_REDIRECT_EXCEPTION_START_ANY_ACTIVITY_PERMISSION,
+ INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION,
+ INTENT_REDIRECT_ABORT_INTENT_FIREWALL_START_ACTIVITY,
+ INTENT_REDIRECT_ABORT_PERMISSION_POLICY_START_ACTIVITY,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface IntentRedirectErrorCode {
+ }
+
+ /**
+ * Error codes for intent redirect issues
+ */
+ static final int INTENT_REDIRECT_EXCEPTION_MISSING_OR_INVALID_TOKEN = 1;
+ static final int INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION = 2;
+ static final int INTENT_REDIRECT_EXCEPTION_START_ANY_ACTIVITY_PERMISSION = 3;
+ static final int INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION = 4;
+ static final int INTENT_REDIRECT_ABORT_INTENT_FIREWALL_START_ACTIVITY = 5;
+ static final int INTENT_REDIRECT_ABORT_PERMISSION_POLICY_START_ACTIVITY = 6;
+
static void logAndThrowExceptionForIntentRedirect(@NonNull Context context,
- @NonNull String message, @NonNull Intent intent, int intentCreatorUid,
+ @IntentRedirectErrorCode int errorCode, @NonNull Intent intent, int intentCreatorUid,
@Nullable String intentCreatorPackage, int callingUid, @Nullable String callingPackage,
@Nullable SecurityException originalException) {
- String msg = getIntentRedirectPreventedLogMessage(message, intent, intentCreatorUid,
+ String msg = getIntentRedirectPreventedLogMessage(errorCode, intent, intentCreatorUid,
intentCreatorPackage, callingUid, callingPackage);
Slog.wtf(TAG, msg);
+ FrameworkStatsLog.write(INTENT_REDIRECT_BLOCKED, intentCreatorUid, callingUid, errorCode);
if (preventIntentRedirectShowToast()) {
UiThread.getHandler().post(
() -> Toast.makeText(context,
@@ -3646,12 +3679,13 @@
}
private static boolean logAndAbortForIntentRedirect(@NonNull Context context,
- @NonNull String message, @NonNull Intent intent, int intentCreatorUid,
+ @IntentRedirectErrorCode int errorCode, @NonNull Intent intent, int intentCreatorUid,
@Nullable String intentCreatorPackage, int callingUid,
@Nullable String callingPackage) {
- String msg = getIntentRedirectPreventedLogMessage(message, intent, intentCreatorUid,
+ String msg = getIntentRedirectPreventedLogMessage(errorCode, intent, intentCreatorUid,
intentCreatorPackage, callingUid, callingPackage);
Slog.wtf(TAG, msg);
+ FrameworkStatsLog.write(INTENT_REDIRECT_BLOCKED, intentCreatorUid, callingUid, errorCode);
if (preventIntentRedirectShowToast()) {
UiThread.getHandler().post(
() -> Toast.makeText(context,
@@ -3662,11 +3696,38 @@
ENABLE_PREVENT_INTENT_REDIRECT_TAKE_ACTION, callingUid);
}
- private static String getIntentRedirectPreventedLogMessage(@NonNull String message,
+ private static String getIntentRedirectPreventedLogMessage(
+ @IntentRedirectErrorCode int errorCode,
@NonNull Intent intent, int intentCreatorUid, @Nullable String intentCreatorPackage,
int callingUid, @Nullable String callingPackage) {
+ String message = getIntentRedirectErrorMessageFromCode(errorCode);
return "[IntentRedirect Hardening] " + message + " intentCreatorUid: " + intentCreatorUid
+ "; intentCreatorPackage: " + intentCreatorPackage + "; callingUid: " + callingUid
+ "; callingPackage: " + callingPackage + "; intent: " + intent;
}
+
+ private static String getIntentRedirectErrorMessageFromCode(
+ @IntentRedirectErrorCode int errorCode) {
+ return switch (errorCode) {
+ case INTENT_REDIRECT_EXCEPTION_MISSING_OR_INVALID_TOKEN ->
+ "INTENT_REDIRECT_EXCEPTION_MISSING_OR_INVALID_TOKEN"
+ + " (Unparceled intent does not have a creator token set, throw exception.)";
+ case INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION ->
+ "INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION"
+ + " (Creator URI permission grant throw exception.)";
+ case INTENT_REDIRECT_EXCEPTION_START_ANY_ACTIVITY_PERMISSION ->
+ "INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION"
+ + " (Creator checkStartAnyActivityPermission, throw exception)";
+ case INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION ->
+ "INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION"
+ + " (Creator checkStartAnyActivityPermission, abort)";
+ case INTENT_REDIRECT_ABORT_INTENT_FIREWALL_START_ACTIVITY ->
+ "INTENT_REDIRECT_ABORT_INTENT_FIREWALL_START_ACTIVITY"
+ + " (Creator IntentFirewall.checkStartActivity, abort)";
+ case INTENT_REDIRECT_ABORT_PERMISSION_POLICY_START_ACTIVITY ->
+ "INTENT_REDIRECT_ABORT_PERMISSION_POLICY_START_ACTIVITY"
+ + " (Creator PermissionPolicyService.checkStartActivity, abort)";
+ default -> "Unknown error code: " + errorCode;
+ };
+ }
}
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 353ccd5..42b63d1 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -7104,6 +7104,10 @@
public void setAnimatingTypes(@InsetsType int animatingTypes) {
if (mAnimatingTypes != animatingTypes) {
mAnimatingTypes = animatingTypes;
+
+ if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) {
+ getInsetsStateController().onAnimatingTypesChanged(this);
+ }
}
}
}
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index 2cac63c..040bbe4 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -263,8 +263,8 @@
boolean oldVisibility = mSource.isVisible();
super.updateVisibility();
if (Flags.refactorInsetsController()) {
- if (mSource.isVisible() && !oldVisibility && mImeRequester != null) {
- reportImeDrawnForOrganizerIfNeeded(mImeRequester);
+ if (mSource.isVisible() && !oldVisibility && mControlTarget != null) {
+ reportImeDrawnForOrganizerIfNeeded(mControlTarget);
}
}
onSourceChanged();
@@ -288,11 +288,15 @@
// If insets target is not available (e.g. RemoteInsetsControlTarget), use current
// IME input target to update IME request state. For example, switch from a task
// with showing IME to a split-screen task without showing IME.
- InsetsTarget insetsTarget = target.getWindow();
- if (insetsTarget == null && mServerVisible) {
- insetsTarget = mDisplayContent.getImeInputTarget();
+ InputTarget imeInputTarget = mDisplayContent.getImeInputTarget();
+ if (imeInputTarget != target && imeInputTarget != null) {
+ // The controlTarget should be updated with the visibility of the
+ // current IME input target.
+ reportImeInputTargetStateToControlTarget(imeInputTarget, target,
+ statsToken);
+ } else {
+ invokeOnImeRequestedChangedListener(target, statsToken);
}
- invokeOnImeRequestedChangedListener(insetsTarget, statsToken);
}
}
}
@@ -328,8 +332,7 @@
if (changed) {
ImeTracker.forLogging().onProgress(statsToken,
ImeTracker.PHASE_SERVER_UPDATE_CLIENT_VISIBILITY);
- invokeOnImeRequestedChangedListener(mDisplayContent.getImeInputTarget(),
- statsToken);
+ invokeOnImeRequestedChangedListener(controlTarget, statsToken);
} else {
// TODO(b/353463205) check cancelled / failed
ImeTracker.forLogging().onCancelled(statsToken,
@@ -383,7 +386,8 @@
// not all virtual displays have an ImeInsetsSourceProvider, so it is not
// guaranteed that the IME will be started when the control target reports its
// requested visibility back. Thus, invoking the listener here.
- invokeOnImeRequestedChangedListener(imeInsetsTarget, statsToken);
+ invokeOnImeRequestedChangedListener((InsetsControlTarget) imeInsetsTarget,
+ statsToken);
} else {
ImeTracker.forLogging().onFailed(statsToken,
ImeTracker.PHASE_WM_SET_REMOTE_TARGET_IME_VISIBILITY);
@@ -392,18 +396,21 @@
}
// TODO(b/353463205) check callers to see if we can make statsToken @NonNull
- private void invokeOnImeRequestedChangedListener(InsetsTarget insetsTarget,
+ private void invokeOnImeRequestedChangedListener(InsetsControlTarget controlTarget,
@Nullable ImeTracker.Token statsToken) {
final var imeListener = mDisplayContent.mWmService.mOnImeRequestedChangedListener;
if (imeListener != null) {
- if (insetsTarget != null) {
+ if (controlTarget != null) {
+ final boolean imeAnimating = Flags.reportAnimatingInsetsTypes()
+ && (controlTarget.getAnimatingTypes() & WindowInsets.Type.ime()) != 0;
ImeTracker.forLogging().onProgress(statsToken,
ImeTracker.PHASE_WM_POSTING_CHANGED_IME_VISIBILITY);
mDisplayContent.mWmService.mH.post(() -> {
ImeTracker.forLogging().onProgress(statsToken,
ImeTracker.PHASE_WM_INVOKING_IME_REQUESTED_LISTENER);
- imeListener.onImeRequestedChanged(insetsTarget.getWindowToken(),
- insetsTarget.isRequestedVisible(WindowInsets.Type.ime()), statsToken);
+ imeListener.onImeRequestedChanged(controlTarget.getWindowToken(),
+ controlTarget.isRequestedVisible(WindowInsets.Type.ime())
+ || imeAnimating, statsToken);
});
} else {
ImeTracker.forLogging().onFailed(statsToken,
@@ -416,6 +423,21 @@
}
}
+ @Override
+ void onAnimatingTypesChanged(InsetsControlTarget caller) {
+ if (Flags.reportAnimatingInsetsTypes()) {
+ final InsetsControlTarget controlTarget = getControlTarget();
+ // If the IME is not being requested anymore and the animation is finished, we need to
+ // invoke the listener, to let IMS eventually know
+ if (caller != null && caller == controlTarget && !caller.isRequestedVisible(
+ WindowInsets.Type.ime())
+ && (caller.getAnimatingTypes() & WindowInsets.Type.ime()) == 0) {
+ // TODO(b/353463205) check statsToken
+ invokeOnImeRequestedChangedListener(caller, null);
+ }
+ }
+ }
+
private void reportImeDrawnForOrganizerIfNeeded(@NonNull InsetsControlTarget caller) {
final WindowState callerWindow = caller.getWindow();
if (callerWindow == null) {
diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java
index 7751ac3..a4bc5cb 100644
--- a/services/core/java/com/android/server/wm/InputManagerCallback.java
+++ b/services/core/java/com/android/server/wm/InputManagerCallback.java
@@ -343,6 +343,13 @@
}
}
+ @Override
+ public boolean isKeyguardLocked(int displayId) {
+ synchronized (mService.mGlobalLock) {
+ return mService.mAtmService.mKeyguardController.isKeyguardLocked(displayId);
+ }
+ }
+
/** Waits until the built-in input devices have been configured. */
public boolean waitForInputDevicesReady(long timeoutMillis) {
synchronized (mInputDevicesReadyMonitor) {
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index b748902..1b693fc 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -673,6 +673,9 @@
mServerVisible, mClientVisible);
}
+ void onAnimatingTypesChanged(InsetsControlTarget caller) {
+ }
+
protected boolean isLeashReadyForDispatching() {
return isLeashInitialized();
}
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index 5e0395f..810e48f 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -393,6 +393,13 @@
}
}
+ void onAnimatingTypesChanged(InsetsControlTarget target) {
+ for (int i = mProviders.size() - 1; i >= 0; i--) {
+ final InsetsSourceProvider provider = mProviders.valueAt(i);
+ provider.onAnimatingTypesChanged(target);
+ }
+ }
+
private void notifyPendingInsetsControlChanged() {
if (mPendingTargetProvidersMap.isEmpty()) {
return;
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index e98b2b7..8587b5a 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -3831,10 +3831,11 @@
pw.print(ActivityInfo.resizeModeToString(mResizeMode));
pw.print(" mSupportsPictureInPicture="); pw.print(mSupportsPictureInPicture);
pw.print(" isResizeable="); pw.println(isResizeable());
- pw.print(" isPerceptible="); pw.println(mIsPerceptible);
+ pw.print(prefix); pw.print("isPerceptible="); pw.println(mIsPerceptible);
pw.print(prefix); pw.print("lastActiveTime="); pw.print(lastActiveTime);
pw.println(" (inactive for " + (getInactiveDuration() / 1000) + "s)");
- pw.print(prefix); pw.println(" isTrimmable=" + mIsTrimmableFromRecents);
+ pw.print(prefix); pw.print("isTrimmable=" + mIsTrimmableFromRecents);
+ pw.print(" isForceHidden="); pw.println(isForceHidden());
if (mLaunchAdjacentDisabled) {
pw.println(prefix + "mLaunchAdjacentDisabled=true");
}
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 2dabb25..960f5be 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -24,7 +24,6 @@
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
@@ -2453,7 +2452,8 @@
inOutConfig.windowConfiguration.setAppBounds(mTmpFullBounds);
outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
- if (!customContainerPolicy && windowingMode != WINDOWING_MODE_FREEFORM) {
+ // Floating tasks shouldn't be restricted by containing app bounds.
+ if (!customContainerPolicy && !isFloating(windowingMode)) {
final Rect containingAppBounds;
if (insideParentBounds) {
containingAppBounds = useOverrideInsetsForConfig
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 1022d18..ce91fc5 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -862,6 +862,12 @@
mWmService.scheduleAnimationLocked();
mAnimatingTypes = animatingTypes;
+
+ if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) {
+ final InsetsStateController insetsStateController =
+ getDisplayContent().getInsetsStateController();
+ insetsStateController.onAnimatingTypesChanged(this);
+ }
}
}
diff --git a/services/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp
index ae9a34e..c1d8382 100644
--- a/services/tests/InputMethodSystemServerTests/Android.bp
+++ b/services/tests/InputMethodSystemServerTests/Android.bp
@@ -73,10 +73,7 @@
static_libs: [
"androidx.annotation_annotation",
"androidx.test.rules",
- "framework",
- "ravenwood-runtime",
- "ravenwood-utils",
- "services",
+ "services.core",
],
libs: [
"android.test.base.stubs.system",
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
index 05615f6..2339a94 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
@@ -287,6 +287,7 @@
mMockRemoteAccessibilityInputConnection /* remoteAccessibilityInputConnection */,
mTargetSdkVersion /* unverifiedTargetSdkVersion */,
mUserId /* userId */,
- mMockImeOnBackInvokedDispatcher /* imeDispatcher */);
+ mMockImeOnBackInvokedDispatcher /* imeDispatcher */,
+ true /* imeRequestedVisible */);
}
}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
index 70eeae6..aa77919 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
@@ -267,7 +267,8 @@
// visibility state will be preserved to the current window state.
final ImeTargetWindowState stateWithUnChangedFlag = initImeTargetWindowState(
mWindowToken);
- mComputer.computeState(stateWithUnChangedFlag, true /* allowVisible */);
+ mComputer.computeState(stateWithUnChangedFlag, true /* allowVisible */,
+ true /* imeRequestedVisible */);
assertThat(stateWithUnChangedFlag.isRequestedImeVisible()).isEqualTo(
lastState.isRequestedImeVisible());
}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
index 11abc94..b81b570 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
@@ -320,7 +320,8 @@
mMockRemoteAccessibilityInputConnection /* remoteAccessibilityInputConnection */,
mTargetSdkVersion /* unverifiedTargetSdkVersion */,
mUserId /* userId */,
- mMockImeOnBackInvokedDispatcher /* imeDispatcher */);
+ mMockImeOnBackInvokedDispatcher /* imeDispatcher */,
+ true /* imeRequestedVisible */);
}
@Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index 5d8f578..e094111 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -746,6 +746,36 @@
@SuppressWarnings("GuardedBy")
@Test
@EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY)
+ public void testUpdateOomAdjFreezeState_bindingWithAllowFreeze() {
+ ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
+ MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
+ WindowProcessController wpc = app.getWindowProcessController();
+ doReturn(true).when(wpc).hasVisibleActivities();
+
+ final ProcessRecord app2 = spy(makeDefaultProcessRecord(MOCKAPP2_PID, MOCKAPP2_UID,
+ MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
+
+ // App with a visible activity binds to app2 without any special flag.
+ bindService(app2, app, null, null, 0, mock(IBinder.class));
+
+ final ProcessRecord app3 = spy(makeDefaultProcessRecord(MOCKAPP3_PID, MOCKAPP3_UID,
+ MOCKAPP3_PROCESSNAME, MOCKAPP3_PACKAGENAME, false));
+
+ // App with a visible activity binds to app3 with ALLOW_FREEZE.
+ bindService(app3, app, null, null, Context.BIND_ALLOW_FREEZE, mock(IBinder.class));
+
+ setProcessesToLru(app, app2, app3);
+
+ updateOomAdj(app);
+
+ assertCpuTime(app);
+ assertCpuTime(app2);
+ assertNoCpuTime(app3);
+ }
+
+ @SuppressWarnings("GuardedBy")
+ @Test
+ @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY)
@DisableFlags(Flags.FLAG_PROTOTYPE_AGGRESSIVE_FREEZING)
public void testUpdateOomAdjFreezeState_bindingFromFgs() {
final ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java
index efea214..63c572a 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java
@@ -171,7 +171,7 @@
HearingDevicePhoneCallNotificationController.CallStateListener {
TestCallStateListener(@NonNull Context context) {
- super(context);
+ super(context, context.getMainExecutor());
}
@Override
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java
index f445b50..02361ff 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java
@@ -28,7 +28,12 @@
import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
import android.testing.TestableLooper;
+import android.view.MotionEvent;
+import android.view.View;
import android.view.WindowManager;
+import android.widget.ImageButton;
+
+import com.android.internal.R;
import org.junit.Before;
import org.junit.Rule;
@@ -49,12 +54,31 @@
new TestableContext(getInstrumentation().getContext());
@Mock private WindowManager mMockWindowManager;
+ @Mock private AutoclickScrollPanel.ScrollPanelControllerInterface mMockScrollPanelController;
+
private AutoclickScrollPanel mScrollPanel;
+ // Scroll panel buttons.
+ private ImageButton mUpButton;
+ private ImageButton mDownButton;
+ private ImageButton mLeftButton;
+ private ImageButton mRightButton;
+ private ImageButton mExitButton;
+
@Before
public void setUp() {
mTestableContext.addMockSystemService(Context.WINDOW_SERVICE, mMockWindowManager);
- mScrollPanel = new AutoclickScrollPanel(mTestableContext, mMockWindowManager);
+ mScrollPanel = new AutoclickScrollPanel(mTestableContext, mMockWindowManager,
+ mMockScrollPanelController);
+
+ View contentView = mScrollPanel.getContentViewForTesting();
+
+ // Initialize buttons.
+ mUpButton = contentView.findViewById(R.id.scroll_up);
+ mDownButton = contentView.findViewById(R.id.scroll_down);
+ mLeftButton = contentView.findViewById(R.id.scroll_left);
+ mRightButton = contentView.findViewById(R.id.scroll_right);
+ mExitButton = contentView.findViewById(R.id.scroll_exit);
}
@Test
@@ -89,4 +113,55 @@
// Verify scroll panel is hidden.
assertThat(mScrollPanel.isVisible()).isFalse();
}
+
+ @Test
+ public void initialState_correctButtonVisibility() {
+ // Verify all expected buttons exist in the view.
+ assertThat(mUpButton.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mDownButton.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mLeftButton.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mRightButton.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mExitButton.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @Test
+ public void directionButtons_onHover_callsHandleScroll() {
+ // Test up button.
+ triggerHoverEvent(mUpButton);
+ verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_UP);
+
+ // Test down button.
+ triggerHoverEvent(mDownButton);
+ verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_DOWN);
+
+ // Test left button.
+ triggerHoverEvent(mLeftButton);
+ verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_LEFT);
+
+ // Test right button.
+ triggerHoverEvent(mRightButton);
+ verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_RIGHT);
+ }
+
+ @Test
+ public void exitButton_onHover_callsExitScrollMode() {
+ // Test exit button.
+ triggerHoverEvent(mExitButton);
+ verify(mMockScrollPanelController).exitScrollMode();
+ }
+
+ // Helper method to simulate a hover event on a view.
+ private void triggerHoverEvent(View view) {
+ MotionEvent event = MotionEvent.obtain(
+ /* downTime= */ 0,
+ /* eventTime= */ 0,
+ /* action= */ MotionEvent.ACTION_HOVER_ENTER,
+ /* x= */ 0,
+ /* y= */ 0,
+ /* metaState= */ 0);
+
+ // Dispatch the event to the view's OnHoverListener.
+ view.dispatchGenericMotionEvent(event);
+ event.recycle();
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
index 1af59da..5922b12 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
@@ -37,6 +37,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -46,6 +47,7 @@
import android.graphics.PointF;
import android.os.Looper;
import android.os.SystemClock;
+import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.testing.DexmakerShareClassLoaderRule;
@@ -504,6 +506,36 @@
assertThat(sentRawEvent.getDisplayId()).isEqualTo(rawDisplayId);
}
+ @Test
+ @DisableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION)
+ public void handleMotionEventStateTouchExploring_pointerUp_doesNotSendToManager() {
+ mTouchExplorer.getState().setServiceDetectsGestures(true);
+ mTouchExplorer.getState().clear();
+
+ mLastEvent = pointerDownEvent();
+ mTouchExplorer.getState().startTouchExploring();
+ MotionEvent event = fromTouchscreen(pointerUpEvent());
+
+ mTouchExplorer.onMotionEvent(event, event, /*policyFlags=*/0);
+
+ verify(mMockAms, never()).sendMotionEventToListeningServices(event);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION)
+ public void handleMotionEventStateTouchExploring_pointerUp_sendsToManager() {
+ mTouchExplorer.getState().setServiceDetectsGestures(true);
+ mTouchExplorer.getState().clear();
+
+ mLastEvent = pointerDownEvent();
+ mTouchExplorer.getState().startTouchExploring();
+ MotionEvent event = fromTouchscreen(pointerUpEvent());
+
+ mTouchExplorer.onMotionEvent(event, event, /*policyFlags=*/0);
+
+ verify(mMockAms).sendMotionEventToListeningServices(event);
+ }
+
/**
* Used to play back event data of a gesture by parsing the log into MotionEvents and sending
* them to TouchExplorer.
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchStateTest.java b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchStateTest.java
new file mode 100644
index 0000000..3e7d9fd
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchStateTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2025 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.accessibility.gestures;
+
+import static android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_INTERACTION_END;
+
+import static com.android.server.accessibility.gestures.TouchState.STATE_CLEAR;
+import static com.android.server.accessibility.gestures.TouchState.STATE_TOUCH_EXPLORING;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.view.Display;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.accessibility.AccessibilityManagerService;
+import com.android.server.accessibility.Flags;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class TouchStateTest {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ private TouchState mTouchState;
+ @Mock private AccessibilityManagerService mMockAms;
+
+ @Before
+ public void setup() {
+ mTouchState = new TouchState(Display.DEFAULT_DISPLAY, mMockAms);
+ }
+
+ @EnableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION)
+ @Test
+ public void injectedEvent_interactionEnd_pointerDown_startsTouchExploring() {
+ mTouchState.mReceivedPointerTracker.mReceivedPointersDown = 1;
+ mTouchState.onInjectedAccessibilityEvent(TYPE_TOUCH_INTERACTION_END);
+ assertThat(mTouchState.getState()).isEqualTo(STATE_TOUCH_EXPLORING);
+ }
+
+ @EnableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION)
+ @Test
+ public void injectedEvent_interactionEnd_pointerUp_clears() {
+ mTouchState.mReceivedPointerTracker.mReceivedPointersDown = 0;
+ mTouchState.onInjectedAccessibilityEvent(TYPE_TOUCH_INTERACTION_END);
+ assertThat(mTouchState.getState()).isEqualTo(STATE_CLEAR);
+ }
+
+ @DisableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION)
+ @Test
+ public void injectedEvent_interactionEnd_clears() {
+ mTouchState.onInjectedAccessibilityEvent(TYPE_TOUCH_INTERACTION_END);
+ assertThat(mTouchState.getState()).isEqualTo(STATE_CLEAR);
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
index 3ca0197..fcdf88f 100644
--- a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
@@ -605,29 +605,6 @@
}
@Test
- public void testKeyGestureAccessibilityShortcutChord() {
- Assert.assertTrue(
- sendKeyGestureEventStart(
- KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
- mPhoneWindowManager.moveTimeForward(5000);
- Assert.assertTrue(
- sendKeyGestureEventCancel(
- KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
- mPhoneWindowManager.assertAccessibilityKeychordCalled();
- }
-
- @Test
- public void testKeyGestureAccessibilityShortcutChordCancelled() {
- Assert.assertTrue(
- sendKeyGestureEventStart(
- KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
- Assert.assertTrue(
- sendKeyGestureEventCancel(
- KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
- mPhoneWindowManager.assertAccessibilityKeychordNotCalled();
- }
-
- @Test
public void testKeyGestureRingerToggleChord() {
mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_MUTE);
Assert.assertTrue(
@@ -670,29 +647,6 @@
}
@Test
- public void testKeyGestureAccessibilityTvShortcutChord() {
- Assert.assertTrue(
- sendKeyGestureEventStart(
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
- mPhoneWindowManager.moveTimeForward(5000);
- Assert.assertTrue(
- sendKeyGestureEventCancel(
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
- mPhoneWindowManager.assertAccessibilityKeychordCalled();
- }
-
- @Test
- public void testKeyGestureAccessibilityTvShortcutChordCancelled() {
- Assert.assertTrue(
- sendKeyGestureEventStart(
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
- Assert.assertTrue(
- sendKeyGestureEventCancel(
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
- mPhoneWindowManager.assertAccessibilityKeychordNotCalled();
- }
-
- @Test
public void testKeyGestureTvTriggerBugReport() {
Assert.assertTrue(
sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index f884924..e56fd3c 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -750,11 +750,6 @@
verify(mAccessibilityShortcutController).performAccessibilityShortcut();
}
- void assertAccessibilityKeychordNotCalled() {
- mTestLooper.dispatchAll();
- verify(mAccessibilityShortcutController, never()).performAccessibilityShortcut();
- }
-
void assertCloseAllDialogs() {
verify(mContext).closeSystemDialogs();
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
index 7d59f48..eb6d5cf 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
@@ -21,10 +21,18 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import android.graphics.PixelFormat;
+import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.view.WindowInsets;
import android.view.inputmethod.Flags;
import android.view.inputmethod.ImeTracker;
@@ -211,4 +219,29 @@
mImeProvider.setFrozen(false);
assertFalse(mImeProvider.getSource().isVisible());
}
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
+ public void testUpdateControlForTarget_remoteInsetsControlTarget() throws RemoteException {
+ final WindowState ime = newWindowBuilder("ime", TYPE_INPUT_METHOD).build();
+ makeWindowVisibleAndDrawn(ime);
+ mImeProvider.setWindowContainer(ime, null, null);
+ mImeProvider.setServerVisible(true);
+ mImeProvider.setClientVisible(true);
+ final WindowState inputTarget = newWindowBuilder("app", TYPE_APPLICATION).build();
+ final var displayWindowInsetsController = spy(createDisplayWindowInsetsController());
+ mDisplayContent.setRemoteInsetsController(displayWindowInsetsController);
+ final var controlTarget = mDisplayContent.mRemoteInsetsControlTarget;
+
+ inputTarget.setRequestedVisibleTypes(
+ WindowInsets.Type.defaultVisible() | WindowInsets.Type.ime());
+ mDisplayContent.setImeInputTarget(inputTarget);
+ mDisplayContent.setImeControlTarget(controlTarget);
+
+ assertTrue(inputTarget.isRequestedVisible(WindowInsets.Type.ime()));
+ assertFalse(controlTarget.isRequestedVisible(WindowInsets.Type.ime()));
+ mImeProvider.updateControlForTarget(controlTarget, true /* force */, null /* statsToken */);
+ verify(displayWindowInsetsController, times(1)).setImeInputTargetRequestedVisibility(
+ eq(true), 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 ae61447..5401a44 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -910,7 +910,7 @@
final RunningTaskInfo info2 = task2.getTaskInfo();
WindowContainerTransaction wct = new WindowContainerTransaction();
- wct.setAdjacentRoots(info1.token, info2.token);
+ wct.setAdjacentRootSet(info1.token, info2.token);
mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct);
assertTrue(task1.isAdjacentTo(task2));
assertTrue(task2.isAdjacentTo(task1));
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index fbba999..14d567d 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -3371,14 +3371,13 @@
return telephony.getDataNetworkTypeForSubscriber(subId, getOpPackageName(),
getAttributionTag());
} else {
- // This can happen when the ITelephony interface is not up yet.
+ Log.e(TAG, "getDataNetworkType: ITelephony interface is not up yet");
return NETWORK_TYPE_UNKNOWN;
}
- } catch(RemoteException ex) {
- // This shouldn't happen in the normal case
- return NETWORK_TYPE_UNKNOWN;
- } catch (NullPointerException ex) {
- // This could happen before phone restarts due to crashing
+ } catch (RemoteException // Shouldn't happen in the normal case
+ | NullPointerException ex // Could happen before phone restarts due to crashing
+ ) {
+ Log.e(TAG, "getDataNetworkType: " + ex.getMessage());
return NETWORK_TYPE_UNKNOWN;
}
}
diff --git a/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt
index e99c814..794fd02 100644
--- a/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt
+++ b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt
@@ -214,9 +214,5 @@
): Boolean {
return handler(event, focusedToken)
}
-
- override fun isKeyGestureSupported(gestureType: Int): Boolean {
- return true
- }
}
}
diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index 2799f6c..4f1fb64 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -32,6 +32,7 @@
import android.hardware.input.InputManager
import android.hardware.input.InputManagerGlobal
import android.hardware.input.KeyGestureEvent
+import android.os.Handler
import android.os.IBinder
import android.os.Process
import android.os.SystemClock
@@ -48,9 +49,11 @@
import androidx.test.core.app.ApplicationProvider
import com.android.dx.mockito.inline.extended.ExtendedMockito
import com.android.internal.R
+import com.android.internal.accessibility.AccessibilityShortcutController
import com.android.internal.annotations.Keep
import com.android.internal.util.FrameworkStatsLog
import com.android.modules.utils.testing.ExtendedMockitoRule
+import com.android.server.input.InputManagerService.WindowManagerCallbacks
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
@@ -67,6 +70,8 @@
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
/**
* Tests for {@link KeyGestureController}.
@@ -102,6 +107,7 @@
const val SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY = 0
const val SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL = 1
const val SETTINGS_KEY_BEHAVIOR_NOTHING = 2
+ const val TEST_PID = 10
}
@JvmField
@@ -116,11 +122,10 @@
@Rule
val rule = SetFlagsRule()
- @Mock
- private lateinit var iInputManager: IInputManager
-
- @Mock
- private lateinit var packageManager: PackageManager
+ @Mock private lateinit var iInputManager: IInputManager
+ @Mock private lateinit var packageManager: PackageManager
+ @Mock private lateinit var wmCallbacks: WindowManagerCallbacks
+ @Mock private lateinit var accessibilityShortcutController: AccessibilityShortcutController
private var currentPid = 0
private lateinit var context: Context
@@ -207,8 +212,34 @@
private fun setupKeyGestureController() {
keyGestureController =
- KeyGestureController(context, testLooper.looper, testLooper.looper, inputDataStore)
- Mockito.`when`(iInputManager.getAppLaunchBookmarks())
+ KeyGestureController(
+ context,
+ testLooper.looper,
+ testLooper.looper,
+ inputDataStore,
+ object : KeyGestureController.Injector() {
+ override fun getAccessibilityShortcutController(
+ context: Context?,
+ handler: Handler?
+ ): AccessibilityShortcutController {
+ return accessibilityShortcutController
+ }
+ })
+ Mockito.`when`(iInputManager.registerKeyGestureHandler(Mockito.any()))
+ .thenAnswer {
+ val args = it.arguments
+ if (args[0] != null) {
+ keyGestureController.registerKeyGestureHandler(
+ args[0] as IKeyGestureHandler,
+ TEST_PID
+ )
+ }
+ }
+ keyGestureController.setWindowManagerCallbacks(wmCallbacks)
+ Mockito.`when`(wmCallbacks.isKeyguardLocked(Mockito.anyInt())).thenReturn(false)
+ Mockito.`when`(accessibilityShortcutController
+ .isAccessibilityShortcutAvailable(Mockito.anyBoolean())).thenReturn(true)
+ Mockito.`when`(iInputManager.appLaunchBookmarks)
.thenReturn(keyGestureController.appLaunchBookmarks)
keyGestureController.systemRunning()
testLooper.dispatchAll()
@@ -1270,9 +1301,9 @@
)
),
TestData(
- "BACK + DPAD_DOWN -> TV Accessibility Chord",
+ "BACK + DPAD_DOWN -> Accessibility Chord(for TV)",
intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
0,
intArrayOf(
@@ -1622,6 +1653,52 @@
)
}
+ @Test
+ fun testAccessibilityShortcutChordPressed() {
+ setupKeyGestureController()
+
+ sendKeys(
+ intArrayOf(KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN),
+ // Assuming this value is always greater than the accessibility shortcut timeout, which
+ // currently defaults to 3000ms
+ timeDelayMs = 10000
+ )
+ Mockito.verify(accessibilityShortcutController, times(1)).performAccessibilityShortcut()
+ }
+
+ @Test
+ fun testAccessibilityTvShortcutChordPressed() {
+ setupKeyGestureController()
+
+ sendKeys(
+ intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
+ timeDelayMs = 10000
+ )
+ Mockito.verify(accessibilityShortcutController, times(1)).performAccessibilityShortcut()
+ }
+
+ @Test
+ fun testAccessibilityShortcutChordPressedForLessThanTimeout() {
+ setupKeyGestureController()
+
+ sendKeys(
+ intArrayOf(KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN),
+ timeDelayMs = 0
+ )
+ Mockito.verify(accessibilityShortcutController, never()).performAccessibilityShortcut()
+ }
+
+ @Test
+ fun testAccessibilityTvShortcutChordPressedForLessThanTimeout() {
+ setupKeyGestureController()
+
+ sendKeys(
+ intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
+ timeDelayMs = 0
+ )
+ Mockito.verify(accessibilityShortcutController, never()).performAccessibilityShortcut()
+ }
+
private fun testKeyGestureInternal(test: TestData) {
val handledEvents = mutableListOf<KeyGestureEvent>()
val handler = KeyGestureHandler { event, _ ->
@@ -1683,7 +1760,11 @@
assertEquals("Test: $testName should not produce Key gesture", 0, handledEvents.size)
}
- private fun sendKeys(testKeys: IntArray, assertNotSentToApps: Boolean = false) {
+ private fun sendKeys(
+ testKeys: IntArray,
+ assertNotSentToApps: Boolean = false,
+ timeDelayMs: Long = 0
+ ) {
var metaState = 0
val now = SystemClock.uptimeMillis()
for (key in testKeys) {
@@ -1699,6 +1780,11 @@
testLooper.dispatchAll()
}
+ if (timeDelayMs > 0) {
+ testLooper.moveTimeForward(timeDelayMs)
+ testLooper.dispatchAll()
+ }
+
for (key in testKeys.reversed()) {
val upEvent = KeyEvent(
now, now, KeyEvent.ACTION_UP, key, 0 /*repeat*/, metaState,
@@ -1742,9 +1828,5 @@
override fun handleKeyGesture(event: AidlKeyGestureEvent, token: IBinder?): Boolean {
return handler(event, token)
}
-
- override fun isKeyGestureSupported(gestureType: Int): Boolean {
- return true
- }
}
}
diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java
index 8cd89ce..649241a 100644
--- a/tests/testables/src/android/testing/TestableLooper.java
+++ b/tests/testables/src/android/testing/TestableLooper.java
@@ -75,16 +75,7 @@
* Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection.
*/
private static boolean isAtLeastBaklava() {
- Method[] methods = TestLooperManager.class.getMethods();
- for (Method method : methods) {
- if (method.getName().equals("peekWhen")) {
- return true;
- }
- }
- return false;
- // TODO(shayba): delete the above, uncomment the below.
- // SDK_INT has not yet ramped to Baklava in all 25Q2 builds.
- // return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA;
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA;
}
static {
diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java
index 4d379e4..bb54a26 100644
--- a/tests/utils/testutils/java/android/os/test/TestLooper.java
+++ b/tests/utils/testutils/java/android/os/test/TestLooper.java
@@ -68,16 +68,7 @@
* Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection.
*/
private static boolean isAtLeastBaklava() {
- Method[] methods = TestLooperManager.class.getMethods();
- for (Method method : methods) {
- if (method.getName().equals("peekWhen")) {
- return true;
- }
- }
- return false;
- // TODO(shayba): delete the above, uncomment the below.
- // SDK_INT has not yet ramped to Baklava in all 25Q2 builds.
- // return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA;
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA;
}
static {
diff --git a/tools/aapt2/Resource.h b/tools/aapt2/Resource.h
index 0d261ab..e51477c 100644
--- a/tools/aapt2/Resource.h
+++ b/tools/aapt2/Resource.h
@@ -249,6 +249,8 @@
// Flag
std::optional<FeatureFlagAttribute> flag;
+
+ bool uses_readwrite_feature_flags = false;
};
/**
diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp
index 5435cba2..db7dddc 100644
--- a/tools/aapt2/ResourceTable.cpp
+++ b/tools/aapt2/ResourceTable.cpp
@@ -664,6 +664,7 @@
if (!config_value->value) {
// Resource does not exist, add it now.
config_value->value = std::move(res.value);
+ config_value->uses_readwrite_feature_flags = res.uses_readwrite_feature_flags;
} else {
// When validation is enabled, ensure that a resource cannot have multiple values defined for
// the same configuration unless protected by flags.
@@ -681,12 +682,14 @@
ConfigKey{&res.config, res.product}, lt_config_key_ref()),
util::make_unique<ResourceConfigValue>(res.config, res.product));
(*it)->value = std::move(res.value);
+ (*it)->uses_readwrite_feature_flags = res.uses_readwrite_feature_flags;
break;
}
case CollisionResult::kTakeNew:
// Take the incoming value.
config_value->value = std::move(res.value);
+ config_value->uses_readwrite_feature_flags = res.uses_readwrite_feature_flags;
break;
case CollisionResult::kConflict:
@@ -843,6 +846,12 @@
return *this;
}
+NewResourceBuilder& NewResourceBuilder::SetUsesReadWriteFeatureFlags(
+ bool uses_readwrite_feature_flags) {
+ res_.uses_readwrite_feature_flags = uses_readwrite_feature_flags;
+ return *this;
+}
+
NewResource NewResourceBuilder::Build() {
return std::move(res_);
}
diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h
index b0e1855..778b43a 100644
--- a/tools/aapt2/ResourceTable.h
+++ b/tools/aapt2/ResourceTable.h
@@ -104,6 +104,9 @@
// The actual Value.
std::unique_ptr<Value> value;
+ // Whether the value uses read/write feature flags
+ bool uses_readwrite_feature_flags = false;
+
ResourceConfigValue(const android::ConfigDescription& config, android::StringPiece product)
: config(config), product(product) {
}
@@ -284,6 +287,7 @@
std::optional<AllowNew> allow_new;
std::optional<StagedId> staged_id;
bool allow_mangled = false;
+ bool uses_readwrite_feature_flags = false;
};
struct NewResourceBuilder {
@@ -297,6 +301,7 @@
NewResourceBuilder& SetAllowNew(AllowNew allow_new);
NewResourceBuilder& SetStagedId(StagedId id);
NewResourceBuilder& SetAllowMangled(bool allow_mangled);
+ NewResourceBuilder& SetUsesReadWriteFeatureFlags(bool uses_feature_flags);
NewResource Build();
private:
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index 2a79216..755dbb6 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -673,11 +673,13 @@
// Update the output format of this XML file.
file_ref->type = XmlFileTypeForOutputFormat(options_.output_format);
- bool result = table->AddResource(NewResourceBuilder(file.name)
- .SetValue(std::move(file_ref), file.config)
- .SetAllowMangled(true)
- .Build(),
- context_->GetDiagnostics());
+ bool result = table->AddResource(
+ NewResourceBuilder(file.name)
+ .SetValue(std::move(file_ref), file.config)
+ .SetAllowMangled(true)
+ .SetUsesReadWriteFeatureFlags(doc->file.uses_readwrite_feature_flags)
+ .Build(),
+ context_->GetDiagnostics());
if (!result) {
return false;
}
diff --git a/tools/aapt2/format/binary/BinaryResourceParser.cpp b/tools/aapt2/format/binary/BinaryResourceParser.cpp
index 2e20e81..bac871b 100644
--- a/tools/aapt2/format/binary/BinaryResourceParser.cpp
+++ b/tools/aapt2/format/binary/BinaryResourceParser.cpp
@@ -414,6 +414,8 @@
.SetId(res_id, OnIdConflict::CREATE_ENTRY)
.SetAllowMangled(true);
+ res_builder.SetUsesReadWriteFeatureFlags(entry->uses_feature_flags());
+
if (entry->flags() & ResTable_entry::FLAG_PUBLIC) {
Visibility visibility{Visibility::Level::kPublic};
diff --git a/tools/aapt2/format/binary/ResEntryWriter.cpp b/tools/aapt2/format/binary/ResEntryWriter.cpp
index 9dc205f..0be3921 100644
--- a/tools/aapt2/format/binary/ResEntryWriter.cpp
+++ b/tools/aapt2/format/binary/ResEntryWriter.cpp
@@ -199,6 +199,10 @@
flags |= ResTable_entry::FLAG_WEAK;
}
+ if (entry->uses_readwrite_feature_flags) {
+ flags |= ResTable_entry::FLAG_USES_FEATURE_FLAGS;
+ }
+
if constexpr (std::is_same_v<ResTable_entry_ext, T>) {
flags |= ResTable_entry::FLAG_COMPLEX;
}
diff --git a/tools/aapt2/format/binary/ResEntryWriter.h b/tools/aapt2/format/binary/ResEntryWriter.h
index c11598e..f54b29a 100644
--- a/tools/aapt2/format/binary/ResEntryWriter.h
+++ b/tools/aapt2/format/binary/ResEntryWriter.h
@@ -38,6 +38,8 @@
// The entry string pool index to the entry's name.
uint32_t entry_key;
+
+ bool uses_readwrite_feature_flags;
};
// Pair of ResTable_entry and Res_value. These pairs are stored sequentially in values buffer.
diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp
index 1a82021..50144ae 100644
--- a/tools/aapt2/format/binary/TableFlattener.cpp
+++ b/tools/aapt2/format/binary/TableFlattener.cpp
@@ -502,7 +502,8 @@
// Group values by configuration.
for (auto& config_value : entry.values) {
config_to_entry_list_map[config_value->config].push_back(
- FlatEntry{&entry, config_value->value.get(), local_key_index});
+ FlatEntry{&entry, config_value->value.get(), local_key_index,
+ config_value->uses_readwrite_feature_flags});
}
}
diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp
index 0f11685..9156b96 100644
--- a/tools/aapt2/format/binary/TableFlattener_test.cpp
+++ b/tools/aapt2/format/binary/TableFlattener_test.cpp
@@ -1069,4 +1069,23 @@
testing::IsTrue());
}
+TEST_F(TableFlattenerTest, UsesReadWriteFeatureFlagSerializesCorrectly) {
+ std::unique_ptr<ResourceTable> table =
+ test::ResourceTableBuilder()
+ .Add(NewResourceBuilder("com.app.a:color/foo")
+ .SetValue(util::make_unique<BinaryPrimitive>(
+ uint8_t(Res_value::TYPE_INT_COLOR_ARGB8), 0xffaabbcc))
+ .SetUsesReadWriteFeatureFlags(true)
+ .SetId(0x7f020000)
+ .Build())
+ .Build();
+ ResTable res_table;
+ TableFlattenerOptions options;
+ ASSERT_TRUE(Flatten(context_.get(), options, table.get(), &res_table));
+
+ uint32_t flags;
+ ASSERT_TRUE(res_table.getResourceEntryFlags(0x7f020000, &flags));
+ ASSERT_EQ(flags, ResTable_entry::FLAG_USES_FEATURE_FLAGS);
+}
+
} // namespace aapt
diff --git a/tools/aapt2/link/FlaggedResources_test.cpp b/tools/aapt2/link/FlaggedResources_test.cpp
index dbef776..47a71fe 100644
--- a/tools/aapt2/link/FlaggedResources_test.cpp
+++ b/tools/aapt2/link/FlaggedResources_test.cpp
@@ -14,6 +14,9 @@
* limitations under the License.
*/
+#include <regex>
+#include <string>
+
#include "LoadedApk.h"
#include "cmd/Dump.h"
#include "io/StringStream.h"
@@ -183,4 +186,49 @@
"Only read only flags may be used with resources: test.package.rwFlag"));
}
+TEST_F(FlaggedResourcesTest, ReadWriteFlagInXmlGetsFlagged) {
+ auto apk_path = file::BuildPath({android::base::GetExecutableDirectory(), "resapp.apk"});
+ auto loaded_apk = LoadedApk::LoadApkFromPath(apk_path, &noop_diag);
+
+ std::string output;
+ DumpChunksToString(loaded_apk.get(), &output);
+
+ // The actual line looks something like:
+ // [ResTable_entry] id: 0x0000 name: layout1 keyIndex: 14 size: 8 flags: 0x0010
+ //
+ // This regex matches that line and captures the name and the flag value for checking.
+ std::regex regex("[0-9a-zA-Z:_\\]\\[ ]+name: ([0-9a-zA-Z]+)[0-9a-zA-Z: ]+flags: (0x\\d{4})");
+ std::smatch match;
+
+ std::stringstream ss(output);
+ std::string line;
+ bool found = false;
+ int fields_flagged = 0;
+ while (std::getline(ss, line)) {
+ bool first_line = false;
+ if (line.contains("config: v36")) {
+ std::getline(ss, line);
+ first_line = true;
+ }
+ if (!line.contains("flags")) {
+ continue;
+ }
+ if (std::regex_search(line, match, regex) && (match.size() == 3)) {
+ unsigned int hex_value;
+ std::stringstream hex_ss;
+ hex_ss << std::hex << match[2];
+ hex_ss >> hex_value;
+ if (hex_value & android::ResTable_entry::FLAG_USES_FEATURE_FLAGS) {
+ fields_flagged++;
+ if (first_line && match[1] == "layout1") {
+ found = true;
+ }
+ }
+ }
+ }
+ ASSERT_TRUE(found) << "No entry for layout1 at v36 with FLAG_USES_FEATURE_FLAGS bit set";
+ // There should only be 1 entry that has the FLAG_USES_FEATURE_FLAGS bit of flags set to 1
+ ASSERT_EQ(fields_flagged, 1);
+}
+
} // namespace aapt
diff --git a/tools/aapt2/link/FlaggedXmlVersioner.cpp b/tools/aapt2/link/FlaggedXmlVersioner.cpp
index 75c6f17..8a3337c 100644
--- a/tools/aapt2/link/FlaggedXmlVersioner.cpp
+++ b/tools/aapt2/link/FlaggedXmlVersioner.cpp
@@ -66,6 +66,28 @@
bool had_flags_ = false;
};
+// An xml visitor that goes through the a doc and determines if any elements are behind a flag.
+class FindFlagsVisitor : public xml::Visitor {
+ public:
+ void Visit(xml::Element* node) override {
+ if (had_flags_) {
+ return;
+ }
+ auto* attr = node->FindAttribute(xml::kSchemaAndroid, xml::kAttrFeatureFlag);
+ if (attr != nullptr) {
+ had_flags_ = true;
+ return;
+ }
+ VisitChildren(node);
+ }
+
+ bool HadFlags() const {
+ return had_flags_;
+ }
+
+ bool had_flags_ = false;
+};
+
std::vector<std::unique_ptr<xml::XmlResource>> FlaggedXmlVersioner::Process(IAaptContext* context,
xml::XmlResource* doc) {
std::vector<std::unique_ptr<xml::XmlResource>> docs;
@@ -74,15 +96,20 @@
// Support for read/write flags was added in baklava so if the doc will only get used on
// baklava or later we can just return the original doc.
docs.push_back(doc->Clone());
+ FindFlagsVisitor visitor;
+ doc->root->Accept(&visitor);
+ docs.back()->file.uses_readwrite_feature_flags = visitor.HadFlags();
} else {
auto preBaklavaVersion = doc->Clone();
AllDisabledFlagsVisitor visitor;
preBaklavaVersion->root->Accept(&visitor);
+ preBaklavaVersion->file.uses_readwrite_feature_flags = false;
docs.push_back(std::move(preBaklavaVersion));
if (visitor.HadFlags()) {
auto baklavaVersion = doc->Clone();
baklavaVersion->file.config.sdkVersion = SDK_BAKLAVA;
+ baklavaVersion->file.uses_readwrite_feature_flags = true;
docs.push_back(std::move(baklavaVersion));
}
}