diff options
143 files changed, 2993 insertions, 1205 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 2dd16de3e188..4d9fdf041d58 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -587,6 +587,7 @@ aconfig_declarations { 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/core/api/test-current.txt b/core/api/test-current.txt index bb023f25094e..12bfccf2172c 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -4507,7 +4507,7 @@ package android.window { 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/app/notification.aconfig b/core/java/android/app/notification.aconfig index 5c267c9f6475..4afe75f7814c 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -265,6 +265,16 @@ flag { } 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 55d78f9b8c48..cc288b1f5601 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -744,15 +744,22 @@ public abstract class Context { */ 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 509b9482154e..4da991ee85b1 100644 --- a/core/java/android/hardware/input/IKeyGestureHandler.aidl +++ b/core/java/android/hardware/input/IKeyGestureHandler.aidl @@ -28,15 +28,4 @@ interface IKeyGestureHandler { * 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 49db54d81e65..d6419afb2a5a 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -1758,13 +1758,6 @@ public final class InputManager { */ 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 a9a45ae45ec3..c4b4831ba76e 100644 --- a/core/java/android/hardware/input/InputManagerGlobal.java +++ b/core/java/android/hardware/input/InputManagerGlobal.java @@ -1193,23 +1193,6 @@ public final class InputManagerGlobal { } 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 1a712d2b3f31..9dd1fed4a85a 100644 --- a/core/java/android/hardware/input/KeyGestureEvent.java +++ b/core/java/android/hardware/input/KeyGestureEvent.java @@ -108,7 +108,8 @@ public final class KeyGestureEvent { 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 @@ public final class KeyGestureEvent { 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 @@ public final class KeyGestureEvent { 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/text/Layout.java b/core/java/android/text/Layout.java index 44c3f9a8244e..0152c52a6753 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -75,9 +75,12 @@ public abstract class Layout { // 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 @@ public abstract class Layout { 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 7c1e4976b9d3..3a2ec91b3b20 100644 --- a/core/java/android/view/Choreographer.java +++ b/core/java/android/view/Choreographer.java @@ -967,8 +967,11 @@ public final class Choreographer { 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 @@ public final class Choreographer { + ((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/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java index ea345a5ef46a..485e7b33f3a7 100644 --- a/core/java/android/window/WindowContainerTransaction.java +++ b/core/java/android/window/WindowContainerTransaction.java @@ -684,7 +684,10 @@ public final class WindowContainerTransaction implements Parcelable { * 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 @@ public final class WindowContainerTransaction implements Parcelable { * * @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 @@ public final class WindowContainerTransaction implements Parcelable { /** * 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/res/res/values/config.xml b/core/res/res/values/config.xml index 8c5b20da73ef..7a38dce296de 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 3de30f7f25a8..46d18e3d3302 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 9e78af57b470..11ec9f8e1912 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 @@ public class LayoutTest { } } + @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 class LayoutTest { 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 @@ public class LayoutTest { this.paint = new Paint(paint); path = null; rect = null; + this.rX = 0; + this.rY = 0; } DrawCommand(Path path, Paint paint) { @@ -1107,15 +1176,19 @@ public class LayoutTest { 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 @@ public class LayoutTest { @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/libs/WindowManager/Shell/multivalentTests/Android.bp b/libs/WindowManager/Shell/multivalentTests/Android.bp index 03076c0940a4..50666911e313 100644 --- a/libs/WindowManager/Shell/multivalentTests/Android.bp +++ b/libs/WindowManager/Shell/multivalentTests/Android.bp @@ -51,6 +51,7 @@ android_robolectric_test { "androidx.test.ext.junit", "mockito-robolectric-prebuilt", "mockito-kotlin2", + "platform-parametric-runner-lib", "truth", "flag-junit-base", "flag-junit", @@ -74,6 +75,7 @@ android_test { "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 000000000000..bdfaef2c6960 --- /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 9087da34d259..636ff669d6b4 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 @@ class BubbleTaskViewListenerTest { 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 @@ class BubbleTaskViewListenerTest { 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 @@ class BubbleTaskViewListenerTest { 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/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index 2c2451cab999..8ac9230c36c3 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 @@ public class BubbleExpandedView extends LinearLayout { 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 @@ public class BubbleExpandedView extends LinearLayout { 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 63d713495177..9c20e3af9ab4 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 @@ public class BubbleTaskViewListener implements TaskView.Listener { 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 d5f2dbdbf5f5..51a5b12edb84 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 @@ public class BubbleTransitions { @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/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 92c3020a14e6..bc2ed3f35b45 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 @@ public abstract class WMShellModule { 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 @@ public abstract class WMShellModule { 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 e89aafe267ed..904d86282c39 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_FULLSCREEN 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.IWindowManager 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 @@ class DesktopDisplayModeController( 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 @@ class DesktopDisplayModeController( 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 @@ class DesktopDisplayModeController( 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 5269318943d9..1ea545f3ab67 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 @@ class DesktopModeKeyGestureHandler( override fun handleKeyGestureEvent(event: KeyGestureEvent, focusedToken: IBinder?): Boolean { if ( - !isKeyGestureSupported(event.keyGestureType) || !desktopTasksController.isPresent || !desktopModeWindowDecorViewModel.isPresent ) { @@ -136,19 +135,6 @@ class DesktopModeKeyGestureHandler( } } - 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 c370c0cb0930..75c09829e551 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.ActivityManager; 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 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, 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 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, }); 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/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 cc37c440f650..450989dd334d 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_FREEFORM 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.android.wm.shell.transition.Transitions 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 @@ import org.mockito.kotlin.whenever */ @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 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { 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 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { 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 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { 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 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { @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 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { 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 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { } } + 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( + FREEFORM_EXTERNAL_EXTENDED( + defaultWindowingMode = WINDOWING_MODE_FREEFORM, + hasExternalDisplay = true, + extendedDisplayEnabled = true, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + 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, - expectToSwitchByDefault = 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, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, ), - FULLSCREEN_DISPLAY_MIRRORING( + FULLSCREEN_EXTERNAL_MIRROR( defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, + hasExternalDisplay = true, extendedDisplayEnabled = false, - expectToSwitchByDefault = false, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - FREEFORM_DISPLAY( + 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, - expectToSwitchByDefault = false, + tabletModeStatus = SwitchState.ON, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, ), - FREEFORM_DISPLAY_MIRRORING( - defaultWindowingMode = 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, - expectToSwitchByDefault = false, + tabletModeStatus = SwitchState.UNKNOWN, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), } } diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp index a18c5f5f92f6..8ecd6ba9b253 100644 --- a/libs/androidfw/ResourceTypes.cpp +++ b/libs/androidfw/ResourceTypes.cpp @@ -6520,41 +6520,79 @@ base::expected<StringPiece16, NullOrIOError> StringPoolRef::string16() const { } 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 (t < 0) { - ALOGW("No type identifier 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); } + 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; +} + +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; + } - *outFlags = entry.specFlags; - return true; + 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 0d45149267cf..63b28da075cd 100644 --- a/libs/androidfw/include/androidfw/ResourceTypes.h +++ b/libs/androidfw/include/androidfw/ResourceTypes.h @@ -1593,6 +1593,8 @@ union ResTable_entry // 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 @@ union ResTable_entry 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 @@ public: 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 e4de3e4420fe..fccdba8e727f 100644 --- a/media/java/android/media/quality/MediaQualityContract.java +++ b/media/java/android/media/quality/MediaQualityContract.java @@ -82,7 +82,7 @@ public class MediaQualityContract { 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 d55bbb3bbdbf..bf739620bc99 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 @@ public class IllustrationPreference extends Preference implements GroupSectionDi 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 4421424c0e39..e59cc81d3ba8 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 @@ public class LottieColorUtils { /** 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/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 3c86f28cccb3..9dd49d6b1015 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1401,6 +1401,16 @@ flag { } 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 { } 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/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt index 0f400892f988..56b06de0a9ba 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 kotlinx.coroutines.test.runTest 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 @@ class TouchHandlingViewInteractionHandlerTest : SysuiTestCase() { 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 @@ class TouchHandlingViewInteractionHandlerTest : SysuiTestCase() { 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/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt index e203a276a2f2..1dddfc1bba9c 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.statusbar.policy.AccessibilityManagerWrapper 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.runCurrent 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 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { 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 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { @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 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { } @Test - fun isEnabled_alwaysFalseWhenQuickSettingsAreVisible() = + fun isLongPressEnabled_alwaysFalseWhenQuickSettingsAreVisible() = testScope.runTest { val isEnabled = collectLastValue(underTest.isLongPressHandlingEnabled) KeyguardState.values().forEach { keyguardState -> @@ -112,7 +131,7 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { } @Test - fun isEnabled_alwaysFalseWhenConfigEnabledBooleanIsFalse() = + fun isLongPressEnabled_alwaysFalseWhenConfigEnabledBooleanIsFalse() = testScope.runTest { overrideResource(R.bool.long_press_keyguard_customize_lockscreen_enabled, false) createUnderTest() @@ -294,6 +313,119 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { 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 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { 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/qs/panels/ui/viewmodel/DetailsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt index 68a591dd075f..1d42424bc6ed 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 @@ class DetailsViewModelTest : SysuiTestCase() { } @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 c3089761effc..5bde7ad27b7a 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 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() { 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 c5752691da44..65763a359c0f 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.SysuiTestCase; 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 @@ public class RankingCoordinatorTest extends SysuiTestCase { } @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 784743740434..e6b2c2541447 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 @@ public class NotificationTestHelper { .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/qs/TileDetailsViewModel.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt index be0362fd7481..ac7a85742385 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 b1d6a270bc67..3f03fcff7603 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/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt index 5863a9385234..7d8752ef7222 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 @@ import com.android.systemui.plugins.qs.TileDetailsViewModel 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 42f1b738ec20..6c3535a42a6e 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.ViewConfiguration 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 @@ class TouchHandlingView( /** 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 @@ class TouchHandlingView( private val interactionHandler: TouchHandlingViewInteractionHandler by lazy { TouchHandlingViewInteractionHandler( + context = context, postDelayed = { block, timeoutMs -> val dispatchToken = Any() @@ -84,6 +88,9 @@ class TouchHandlingView( 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 @@ class TouchHandlingView( 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 @@ class TouchHandlingView( } } } - -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 5863fc644c8e..fe509d74edc0 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 @@ class TouchHandlingViewInteractionHandler( 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 @@ class TouchHandlingViewInteractionHandler( } 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 - } - is MotionEventModel.Cancel -> { - logger?.motionEventCancelled() - cancelScheduledLongPress() - false + + else -> false } - else -> false } + + return false } private fun scheduleLongPress(x: Int, y: Int) { @@ -134,4 +194,30 @@ class TouchHandlingViewInteractionHandler( 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/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt index 705eaa22aa9a..55534c4f1444 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 @@ package com.android.systemui.keyguard.domain.interactor 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.res.R 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 @@ constructor( 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 @@ constructor( 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 @@ constructor( private var delayedHideMenuJob: Job? = null init { - if (isFeatureEnabled()) { + if (isLongPressFeatureEnabled()) { broadcastDispatcher .broadcastFlow(IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) .onEach { hideMenu() } @@ -175,17 +208,30 @@ constructor( /** 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 17e14c3e83da..70a827d5e45b 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 @@ object DeviceEntryIconViewBinder { /** * 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/KeyguardTouchViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt index 195413a80f4b..485e1ce5b2f7 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 @@ object KeyguardTouchViewBinder { onSingleTap(x, y) } + + override fun onDoubleTapDetected(view: View) { + if (falsingManager.isFalseDoubleTap()) { + return + } + viewModel.onDoubleClick() + } } view.repeatWhenAttached { @@ -90,9 +97,20 @@ object KeyguardTouchViewBinder { } } } + 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 1d2edc6d406b..d4e7af48adfe 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 @@ constructor( /** 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 e7c2a454e16c..b71d8c995e51 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 @@ public class RingtonePlayer implements CoreStartable { 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 @@ public class RingtonePlayer implements CoreStartable { @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 @@ public class RingtonePlayer implements CoreStartable { @Override public ParcelFileDescriptor openRingtone(Uri uri) { + enforceUriUserId(uri); final UserHandle user = Binder.getCallingUserHandle(); final ContentResolver resolver = getContextForUser(user).getContentResolver(); @@ -241,6 +246,28 @@ public class RingtonePlayer implements CoreStartable { } }; + /** + * 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 49b53c2d78ae..dfb32e66dae5 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.LocalMediaManager 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,8 +95,39 @@ constructor( data: MediaData, immediately: Boolean, receivedSmartspaceCardLatency: Int, - isSsReactivated: Boolean + isSsReactivated: Boolean, ) { + if (mediaControlsDeviceManagerBackgroundExecution()) { + bgExecutor.execute { onMediaLoaded(key, oldKey, data) } + } else { + onMediaLoaded(key, oldKey, data) + } + } + + override fun onMediaDataRemoved(key: String, userInitiated: Boolean) { + if (mediaControlsDeviceManagerBackgroundExecution()) { + bgExecutor.execute { onMediaRemoved(key, userInitiated) } + } else { + onMediaRemoved(key, userInitiated) + } + } + + fun dump(pw: PrintWriter) { + with(pw) { + println("MediaDeviceManager state:") + entries.forEach { (key, entry) -> + println(" key=$key") + entry.dump(pw) + } + } + } + + @MainThread + private fun processDevice(key: String, oldKey: String?, device: MediaDeviceData?) { + 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() @@ -104,9 +136,13 @@ constructor( 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) + // 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) } @@ -120,27 +156,18 @@ constructor( } } - override fun onMediaDataRemoved(key: String, userInitiated: Boolean) { + private fun onMediaRemoved(key: String, userInitiated: Boolean) { val token = entries.remove(key) token?.stop() - token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } } - } - - fun dump(pw: PrintWriter) { - with(pw) { - println("MediaDeviceManager state:") - entries.forEach { (key, entry) -> - println(" key=$key") - entry.dump(pw) + if (mediaControlsDeviceManagerBackgroundExecution()) { + fgExecutor.execute { + token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } } } + } else { + token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } } } } - @MainThread - private fun processDevice(key: String, oldKey: String?, device: MediaDeviceData?) { - listeners.forEach { it.onMediaDeviceChanged(key, oldKey, device) } - } - interface Listener { /** Called when the route has changed for a given notification. */ fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?) @@ -260,7 +287,7 @@ constructor( override fun onAboutToConnectDeviceAdded( deviceAddress: String, deviceName: String, - deviceIcon: Drawable? + deviceIcon: Drawable?, ) { aboutToConnectDeviceOverride = AboutToConnectDevice( @@ -270,8 +297,8 @@ constructor( /* enabled */ enabled = true, /* icon */ deviceIcon, /* name */ deviceName, - /* showBroadcastButton */ showBroadcastButton = false - ) + /* showBroadcastButton */ showBroadcastButton = false, + ), ) updateCurrent() } @@ -292,7 +319,7 @@ constructor( override fun onBroadcastMetadataChanged( broadcastId: Int, - metadata: BluetoothLeBroadcastMetadata + metadata: BluetoothLeBroadcastMetadata, ) { logger.logBroadcastMetadataChanged(broadcastId, metadata.toString()) updateCurrent() @@ -352,14 +379,14 @@ constructor( // 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 @@ constructor( device?.iconWithoutBackground, name, id = device?.id, - showBroadcastButton = false + showBroadcastButton = false, ) } } @@ -415,7 +442,7 @@ constructor( icon = iconWithoutBackground, name = name, id = id, - showBroadcastButton = false + showBroadcastButton = false, ) private fun getLeAudioBroadcastDeviceData(): MediaDeviceData { @@ -425,7 +452,7 @@ constructor( icon = MediaControlDrawables.getLeAudioSharing(context), name = context.getString(R.string.audio_sharing_description), intent = null, - showBroadcastButton = false + showBroadcastButton = false, ) } else { MediaDeviceData( @@ -433,7 +460,7 @@ constructor( icon = MediaControlDrawables.getAntenna(context), name = broadcastDescription, intent = null, - showBroadcastButton = true + showBroadcastButton = true, ) } } @@ -449,7 +476,7 @@ constructor( device, controller, routingSession?.name, - selectedRoutes?.firstOrNull()?.name + selectedRoutes?.firstOrNull()?.name, ) if (controller == null) { @@ -514,7 +541,7 @@ constructor( 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 @@ constructor( */ 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 795e811db2bc..6ab4a52dc919 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 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { private final Executor mMainExecutor; private final Executor mBackgroundExecutor; View mHolderView; - private boolean mIsInitVolumeFirstTime; public MediaOutputAdapterLegacy( MediaSwitchingController controller, @@ -78,7 +77,6 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { super(controller); mMainExecutor = mainExecutor; mBackgroundExecutor = backgroundExecutor; - mIsInitVolumeFirstTime = true; } @Override @@ -261,10 +259,9 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { 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 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { 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 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { 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 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { 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 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { } 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 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { } // 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 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { 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 9e6fa48d6f98..f79693138e24 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 @@ public class MediaSwitchingController @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 @@ public class MediaSwitchingController 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 @@ public class MediaSwitchingController protected void start(@NonNull Callback cb) { synchronized (mMediaDevicesLock) { mCachedMediaDevices.clear(); - mOutputMediaItemList.clear(); + mOutputMediaItemListProxy.clear(); } mNearbyDeviceInfoMap.clear(); if (mNearbyMediaDevicesManager != null) { @@ -291,7 +292,7 @@ public class MediaSwitchingController mLocalMediaManager.stopScan(); synchronized (mMediaDevicesLock) { mCachedMediaDevices.clear(); - mOutputMediaItemList.clear(); + mOutputMediaItemListProxy.clear(); } if (mNearbyMediaDevicesManager != null) { mNearbyMediaDevicesManager.unregisterNearbyDevicesCallback(this); @@ -333,7 +334,7 @@ public class MediaSwitchingController @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 @@ public class MediaSwitchingController } @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 @@ public class MediaSwitchingController @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 @@ public class MediaSwitchingController } try { synchronized (mMediaDevicesLock) { - mOutputMediaItemList.removeIf((MediaItem::isMutingExpectedDevice)); + mOutputMediaItemListProxy.removeMutingExpectedDevices(); } mAudioManager.cancelMuteAwaitConnection(mAudioManager.getMutingExpectedDevice()); } catch (Exception e) { @@ -574,14 +577,14 @@ public class MediaSwitchingController 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 @@ public class MediaSwitchingController * 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 @@ public class MediaSwitchingController } 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 @@ public class MediaSwitchingController } // 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 @@ public class MediaSwitchingController 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 class MediaSwitchingController 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 @@ public class MediaSwitchingController 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 @@ public class MediaSwitchingController 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 000000000000..1c9c0b102cb7 --- /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 d8fc52bcc55a..8dc27bf4ac3e 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 @@ constructor( ): Boolean { return this@NoteTaskInitializer.handleKeyGestureEvent(event) } - - override fun isKeyGestureSupported(gestureType: Int): Boolean { - return this@NoteTaskInitializer.isKeyGestureSupported(gestureType) - } } /** @@ -225,10 +221,6 @@ constructor( 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 701f44e9981c..d40ecc9565ae 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 @@ fun TileDetails(modifier: Modifier = Modifier, detailsViewModel: DetailsViewMode DisposableEffect(Unit) { onDispose { detailsViewModel.closeDetailedView() } } + val title = tileDetailedViewModel.title + val subTitle = tileDetailedViewModel.subTitle + Column( modifier = modifier @@ -90,7 +93,7 @@ fun TileDetails(modifier: Modifier = Modifier, detailsViewModel: DetailsViewMode ) } Text( - text = tileDetailedViewModel.getTitle(), + text = title, modifier = Modifier.align(Alignment.CenterVertically), textAlign = TextAlign.Center, style = MaterialTheme.typography.titleLarge, @@ -110,7 +113,7 @@ fun TileDetails(modifier: Modifier = Modifier, detailsViewModel: DetailsViewMode } } 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/tiles/dialog/InternetDetailsContent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt index 7d396c58630e..8ffba1e5f3dd 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 @@ package com.android.systemui.qs.tiles.dialog 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 @@ fun InternetDetailsContent(viewModel: InternetDetailsViewModel) { // 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 659488bdd0d3..d8e1755e6cca 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.Switch 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 @@ constructor( 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 @@ constructor( 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 @@ constructor( * * @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 @@ constructor( } } - 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 @@ constructor( 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 6709fd2bb508..fb63bea4fb9f 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,38 +28,27 @@ class InternetDetailsViewModel @AssistedInject constructor( private val accessPointController: AccessPointController, - val contentManagerFactory: InternetDetailsContentManager.Factory, + private val contentManagerFactory: InternetDetailsContentManager.Factory, private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler, -) : TileDetailsViewModel() { - override fun clickOnSettingsButton() { - qsTileIntentUserActionHandler.handle( - /* expandable= */ null, - Intent(Settings.ACTION_WIFI_SETTINGS), +) : TileDetailsViewModel { + val internetDetailsContentManager by lazy { + contentManagerFactory.create( + canConfigMobileData = accessPointController.canConfigMobileData(), + canConfigWifi = accessPointController.canConfigWifi(), ) } - 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 val title: String + get() = internetDetailsContentManager.title - 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" - } + override val subTitle: String + get() = internetDetailsContentManager.subTitle - fun getCanConfigMobileData(): Boolean { - return accessPointController.canConfigMobileData() - } - - fun getCanConfigWifi(): Boolean { - return accessPointController.canConfigWifi() + override fun clickOnSettingsButton() { + qsTileIntentUserActionHandler.handle( + /* expandable= */ null, + Intent(Settings.ACTION_WIFI_SETTINGS), + ) } @AssistedFactory 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 9a39c3c095ef..4f7e03bd3bc3 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 @@ import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogView 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 c84ddb6fdb36..59f209edb546 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 @@ import com.android.systemui.screenrecord.RecordingController 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 3be2f1b7b957..362b5db012e1 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.GestureDetector 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 @@ constructor( ) : 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 @@ constructor( 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 @@ constructor( 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 fa17b4fad592..dafb1a559d59 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 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } 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 4d68f2e6ef1b..f6535730cf77 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 androidx.annotation.VisibleForTesting; 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 @@ public class BundleEntry extends PipelineEntry { // 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 3fad8f0510a1..1f32b945ce7e 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.NonNull; 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 @@ public class RankingCoordinator implements Coordinator { 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 c2f0806a9cd6..6b32c6a18ec0 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 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { 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 b481455699ef..8da2f768bf71 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 @@ public class ExpandableNotificationRow extends ActivatableNotificationView 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/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 175e8d4331a1..e048d462ef8d 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.EnableFlags 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.verifyNoMoreInteractions 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 BROADCAST_APP_NAME = "BROADCAST_APP_NAME" 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 @@ public class MediaDeviceManagerTest : SysuiTestCase() { @Test fun loadMediaData() { manager.onMediaDataLoaded(KEY, null, mediaData) + fakeBgExecutor.runAllReady() + fakeFgExecutor.runAllReady() verify(lmmFactory).create(PACKAGE) } @@ -195,6 +211,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { manager.onMediaDataLoaded(KEY, null, mediaData) manager.onMediaDataRemoved(KEY, false) fakeBgExecutor.runAllReady() + fakeFgExecutor.runAllReady() verify(lmm).unregisterCallback(any()) verify(muteAwaitManager).stopListening() } @@ -406,6 +423,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() { 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 c20a801cd5e3..a8bfbd18d0c5 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 @@ class InternetDetailsContentManagerTest : SysuiTestCase() { 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 @@ class InternetDetailsContentManagerTest : SysuiTestCase() { 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 @@ class InternetDetailsContentManagerTest : SysuiTestCase() { } } + @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 000000000000..c231be181977 --- /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 a978ecdb3534..8de931a7af40 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.TaskViewRepository; 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.Mock; 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.List; 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 @@ public class BubblesTest extends SysuiTestCase { // 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/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt index 255a780a84be..113059222aa2 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.keyguard.data.repository.keyguardRepository 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 @@ val Kosmos.keyguardTouchHandlingInteractor by 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 4f8d5a14e390..9457de18b3b9 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 @@ package com.android.systemui.qs 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/ravenwood/Android.bp b/ravenwood/Android.bp index ccbc46fdb03b..5424ac3bd897 100644 --- a/ravenwood/Android.bp +++ b/ravenwood/Android.bp @@ -16,7 +16,7 @@ filegroup { srcs: [ "texts/ravenwood-common-policies.txt", ], - visibility: ["//visibility:private"], + visibility: [":__subpackages__"], } filegroup { @@ -44,6 +44,22 @@ filegroup { } 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 e36677189e02..f5b075b17fd4 100644 --- a/ravenwood/Framework.bp +++ b/ravenwood/Framework.bp @@ -33,6 +33,7 @@ genrule_defaults { ":ravenwood-common-policies", ":ravenwood-framework-policies", ":ravenwood-standard-options", + ":ravenwood-standard-annotations", ":ravenwood-annotation-allowed-classes", ], out: [ @@ -44,6 +45,7 @@ genrule_defaults { 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 @@ java_genrule { 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 @@ java_genrule { ":ravenwood-common-policies", ":ravenwood-services-policies", ":ravenwood-standard-options", + ":ravenwood-standard-annotations", ":ravenwood-annotation-allowed-classes", ], out: [ @@ -247,6 +251,7 @@ java_genrule { 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 @@ java_genrule { ":ravenwood-common-policies", ":icu-ravenwood-policies", ":ravenwood-standard-options", + ":ravenwood-standard-annotations", ], out: [ "ravenwood.jar", @@ -301,6 +307,7 @@ java_genrule { 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 @@ java_genrule { ":ravenwood-common-policies", ":framework-configinfrastructure-ravenwood-policies", ":ravenwood-standard-options", + ":ravenwood-standard-annotations", ], out: [ "ravenwood.jar", @@ -355,6 +363,7 @@ java_genrule { 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 @@ java_genrule { ":ravenwood-common-policies", ":framework-statsd-ravenwood-policies", ":ravenwood-standard-options", + ":ravenwood-standard-annotations", ], out: [ "ravenwood.jar", @@ -409,6 +419,7 @@ java_genrule { 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 @@ java_genrule { ":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 df63cb9dfc50..1148539187ac 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 000000000000..cdfd4a877f43 --- /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 000000000000..cef736f87e72 --- /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 000000000000..75ec5cadb6fc --- /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 233657557747..0a650254a71f 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 f59e143c1e4e..ae0a00855650 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 DuplicateAnnotationException(annotationName: String?) : 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 4fe21eac6972..98f96a89d889 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.KeepNativeFilter 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 @@ class HostStubGenClassProcessor( 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 @@ class HostStubGenClassProcessor( return filter } - fun processClassBytecode(bytecode: ByteArray): ByteArray { - val cr = ClassReader(bytecode) - - // COMPUTE_FRAMES wouldn't be happy if code uses - val flags = ClassWriter.COMPUTE_MAXS // or ClassWriter.COMPUTE_FRAMES - val cw = ClassWriter(flags) + private fun buildVisitor(base: ClassVisitor, className: String): ClassVisitor { + // Connect to the base visitor + var outVisitor: ClassVisitor = base - // Connect to the class writer - var outVisitor: ClassVisitor = cw if (options.enableClassChecker.get) { outVisitor = CheckClassAdapter(outVisitor) } @@ -149,15 +150,59 @@ class HostStubGenClassProcessor( 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 c7c45e65a8b6..e7166f11f597 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 @@ package com.android.hoststubgen 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 @@ open class HostStubGenClassProcessorOptions( 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 @@ open class HostStubGenClassProcessorOptions( 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 b41ce0f65017..112ef01e20cb 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 @@ private val numericalInnerClassName = """.*\$\d+$""".toRegex() 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 b8b0d8a31268..7f36aca33eee 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 @@ package com.android.hoststubgen.filters * 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 @@ abstract class DelegatingFilter( } 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 @@ abstract class DelegatingFilter( } 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 474da6dfa1b9..d44d016f7c5b 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 @@ import org.objectweb.asm.tree.ClassNode * 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 @@ class ImplicitOutputFilter( 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 @@ class ImplicitOutputFilter( // "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 @@ class ImplicitOutputFilter( // 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 @@ class ImplicitOutputFilter( } override fun getPolicyForField( - className: String, - fieldName: String + className: String, + fieldName: String ): FilterPolicyWithReason { val fallback = super.getPolicyForField(className, fieldName) @@ -161,4 +159,4 @@ class ImplicitOutputFilter( 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 fc885d6f463b..59da3da99ea5 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 @@ class InMemoryOutputFilter( 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 @@ class InMemoryOutputFilter( 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 @@ class InMemoryOutputFilter( } } + 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 @@ class InMemoryOutputFilter( 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 @@ class InMemoryOutputFilter( } 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 @@ class InMemoryOutputFilter( 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 @@ class InMemoryOutputFilter( } 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 f99ce906240a..c47bb302920f 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 class OutputFilter { 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 @@ abstract class OutputFilter { * 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 dd353e9caeff..97fc35302528 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.toHumanReadableClassName 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 @@ enum class SpecialClass { 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 @@ interface PolicyFileProcessor { className: String, methodName: String, methodDesc: String, - replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec, + replaceSpec: MethodCallReplaceSpec, ) } @@ -116,9 +133,6 @@ class TextFileFilterPolicyBuilder( 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 @@ class TextFileFilterPolicyBuilder( */ 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 @@ class TextFileFilterPolicyBuilder( } 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 @@ class TextFileFilterPolicyBuilder( 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 @@ class TextFileFilterPolicyParser { 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 a3f934cacc2c..000000000000 --- 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 bc90d1248322..000000000000 --- 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 0b17879b862c..d0869929edfb 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 @@ abstract class BaseOptions { * * 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 @@ class ArgIterator( 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 @@ class ArgIterator( * * 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 @@ class IntSetOnce(value: Int) : SetOnce<Int>(value) { } } } + +/** + * 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 a08d1d605949..769b769d7a20 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 @@ package com.android.hoststubgen.visitors 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.FilterPolicyWithReason 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 @@ abstract class BaseAdapter( 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 @@ abstract class BaseAdapter( 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 b8a357668c2b..617385ad438e 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 @@ class ImplGeneratingAdapter( } 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 8bb454fa12e7..d9cc54aebf51 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 @@ package com.android.hoststubgen 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 @@ class HostStubGenOptions( } } - 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 @@ class HostStubGenOptions( } } - 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 f7fd0804c151..58bd9e987fd1 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 @@ class PtaOptions( 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 fd6f732a06ce..5ce9a23e6e05 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.LogLevel 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 @@ private class TextPolicyToAnnotationConverter( 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 8b95843f08a6..6e0b7b89cf13 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 @@ class MapOptions( 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 957e20647d44..93cda4e3c4c9 100644 --- a/ravenwood/tools/ravenizer/Android.bp +++ b/ravenwood/tools/ravenizer/Android.bp @@ -15,5 +15,10 @@ java_binary_host { "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 e67c730df069..04e3bda2ba27 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 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 +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.util.CheckClassAdapter /** * Various stats on Ravenizer. @@ -41,7 +40,7 @@ data class RavenizerStats( /** 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 @@ data class RavenizerStats( /** 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 @@ data class RavenizerStats( ) { 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 @@ class Ravenizer { 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 @@ class Ravenizer { 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 @@ class Ravenizer { } } } - if (includeUnsupportedMockito(allClasses)) { + if (includeUnsupportedMockito(processor.allClasses)) { log.w("Unsupported Mockito detected in $inJar!") } @@ -134,7 +141,7 @@ class Ravenizer { 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 @@ class Ravenizer { 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 @@ class Ravenizer { 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 @@ class Ravenizer { * 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) - - lateinit var data: ByteArray - stats.totalConversionTime += log.vTime("Modify ${entry.name}") { + ): ByteArray { + val classInternalName = zipEntryNameToClassName(entry.name) + ?: throw RavenizerInternalException("Unexpected zip entry name: ${entry.name}") - 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 flags = ClassWriter.COMPUTE_MAXS + val cw = ClassWriter(flags) + var outVisitor: ClassVisitor = cw - val enableChecker = false - if (enableChecker) { - outVisitor = CheckClassAdapter(outVisitor) - } + val enableChecker = false + if (enableChecker) { + outVisitor = CheckClassAdapter(outVisitor) + } - // This must be kept in sync with shouldProcessClass. - outVisitor = RunnerRewritingAdapter.maybeApply( - classInternalName, allClasses, outVisitor) + // This must be kept in sync with shouldProcessClass. + outVisitor = RunnerRewritingAdapter.maybeApply( + classInternalName, allClasses, outVisitor) - cr.accept(outVisitor, ClassReader.EXPAND_FRAMES) + val cr = ClassReader(input) + cr.accept(outVisitor, ClassReader.EXPAND_FRAMES) - data = cw.toByteArray() - } - output.write(data) + 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 7f4829ec6127..8a09e6d533b8 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.LogLevel 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 @@ import kotlin.io.path.exists */ 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 @@ fun main(args: Array<String>) { 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 2c0365404ab6..5d278bb046ae 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 @@ class RavenizerOptions( /** 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 @@ class RavenizerOptions( "--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 @@ class RavenizerOptions( 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 6092fcc9402d..b394a761c7ae 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 @@ fun String.isRavenwoodClass(): Boolean { /** * 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 61e254b225c3..d252b4dc8ab6 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.isAbstract 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 validateClasses(classes: ClassNodes): Boolean { * */ 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 @@ com.android.server.power.stats.BatteryStatsTimerTest 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 b52b3dabd47d..35db3c6f0a6d 100644 --- a/services/accessibility/accessibility.aconfig +++ b/services/accessibility/accessibility.aconfig @@ -260,6 +260,16 @@ flag { } 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 @@ flag { 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 c49151dd5e30..573c591cb504 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub @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 diff --git a/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java b/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java index 10dffb59317e..805d7f820c8d 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 @@ public class HearingDevicePhoneCallNotificationController { 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 @@ public class HearingDevicePhoneCallNotificationController { 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 @@ public class HearingDevicePhoneCallNotificationController { } if (state == TelephonyManager.CALL_STATE_IDLE) { + if (mIsCommDeviceChangedRegistered) { + mIsCommDeviceChangedRegistered = false; + mAudioManager.removeOnCommunicationDeviceChangedListener( + mCommDeviceChangedListener); + } dismissNotificationIfNeeded(); if (mHearingDevice != null) { @@ -143,10 +163,23 @@ public class HearingDevicePhoneCallNotificationController { 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 @@ public class HearingDevicePhoneCallNotificationController { 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/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java index fb329430acb2..b02fe2752a62 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 @@ public class TouchExplorer extends BaseEventStreamTransformation 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 cd46b38272c2..568abd196735 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.Display; import android.view.MotionEvent; import android.view.accessibility.AccessibilityEvent; +import androidx.annotation.VisibleForTesting; + import com.android.server.accessibility.AccessibilityManagerService; /** @@ -73,7 +75,8 @@ public class TouchState { 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 @@ public class TouchState { 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 @@ public class TouchState { 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 6ccf5e47ca6c..59566677b1fc 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_ 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 @@ public final class PresentationStatsEventLogger { 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 final class PresentationStatsEventLogger { 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 @@ public final class PresentationStatsEventLogger { } /** + * 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 @@ public final class PresentationStatsEventLogger { + " 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 @@ public final class PresentationStatsEventLogger { event.mViewFilledSuccessfullyOnRefillCount, event.mViewFailedOnRefillCount, event.mFixExpireResponseDuringAuthCount, - event.mNotifyViewEnteredIgnoredDuringAuthCount); + event.mNotifyViewEnteredIgnoredDuringAuthCount, + event.mFillDialogNotShownReason, + event.mFillDialogReadyToShowMs, + event.mImeAnimationFinishMs); mEventInternal = Optional.empty(); } @@ -1087,6 +1176,9 @@ public final class PresentationStatsEventLogger { // 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 6515b237519a..ff3bf2acb080 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.AUTHENTIC 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 @@ final class Session 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 @@ final class Session 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 @@ final class Session || !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 @@ final class Session // 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 @@ final class Session // 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 @@ final class Session 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 14d9d3f0c0a1..decac40d20f8 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -122,10 +122,10 @@ genrule { } 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 @@ java_library_static { "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 31704c442290..4e1d77c26129 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 @@ final class ConnectionRecord implements OomAdjusterModernImpl.Connection{ | 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 @@ final class ConnectionRecord implements OomAdjusterModernImpl.Connection{ 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 336a35e7a7e3..fa35da30bf4b 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 @@ public class OomAdjuster { // 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 @@ public class OomAdjuster { // 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 @@ public class OomAdjuster { /** * @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 1b7e8f0bd244..7e7b5685cf13 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 @@ public class OomAdjusterModernImpl extends OomAdjuster { * 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/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index ada1cd73f775..766456134b20 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -4997,6 +4997,8 @@ public class AudioService extends IAudioService.Stub 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:" diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index c2fecf283a34..d9db178e0dc2 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 @@ public class InputManagerService extends IInputManager.Stub } mWindowManagerCallbacks = callbacks; registerLidSwitchCallbackInternal(mWindowManagerCallbacks); + mKeyGestureController.setWindowManagerCallbacks(callbacks); } public void setWiredAccessoryCallbacks(WiredAccessoryCallbacks callbacks) { @@ -2756,24 +2757,6 @@ public class InputManagerService extends IInputManager.Stub @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 @@ public class InputManagerService extends IInputManager.Stub */ @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 ef5babf19d83..395c77322c04 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.Display; 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 @@ final class KeyGestureController { 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 @@ final class KeyGestureController { 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 @@ final class KeyGestureController { 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 @@ final class KeyGestureController { 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 @@ final class KeyGestureController { 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 @@ final class KeyGestureController { 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 @@ final class KeyGestureController { 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 @@ final class KeyGestureController { mSettingsObserver.observe(); mAppLaunchShortcutManager.systemRunning(); mInputGestureManager.systemRunning(); + initKeyGestures(); int userId; synchronized (mUserLock) { @@ -447,6 +462,27 @@ final class KeyGestureController { 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 @@ final class KeyGestureController { 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 @@ final class KeyGestureController { 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 @@ final class KeyGestureController { AidlKeyGestureEvent event = (AidlKeyGestureEvent) msg.obj; notifyKeyGestureEvent(event); break; + case MSG_ACCESSIBILITY_SHORTCUT: + mAccessibilityShortcutController.performAccessibilityShortcut(); + break; } return true; } @@ -1347,17 +1382,6 @@ final class KeyGestureController { } 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 @@ final class KeyGestureController { 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 @@ final class KeyGestureController { 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/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java index 9e38435ff7f1..ad108f64ffe3 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.SharedPreferences; 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.Binder; 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 @@ public class MediaQualityService extends SystemService { 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 @@ public class MediaQualityService extends SystemService { 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 @@ public class MediaQualityService extends SystemService { 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 46dc75817a36..3230e891db55 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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; - } - - @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; - } + 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; } + return handled; }); } @@ -4457,13 +4405,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java index 3f24da9d89f2..51025d204b46 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.DefaultFactory; import com.android.server.wm.ActivityStarter.Factory; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.List; /** @@ -97,6 +98,9 @@ public class ActivityStartController { /** 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 @@ public class ActivityStartController { 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 @@ public class ActivityStartController { 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 @@ public class ActivityStartController { } } 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 @@ public class ActivityStartController { } } + 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 233f91385ca4..a84a008f66eb 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 android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TAS 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.annotations.VisibleForTesting; 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 @@ class ActivityStarter { 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 @@ class ActivityStarter { } } 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 @@ class ActivityStarter { } } 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 @@ class ActivityStarter { 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 @@ class ActivityStarter { 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 @@ class ActivityStarter { 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 @@ class ActivityStarter { } 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 @@ class ActivityStarter { 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/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java index 2cac63c1e5e9..a937691e7998 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 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { 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(); diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java index 7751ac3f9fc6..a4bc5cbcb5d3 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 @@ final class InputManagerCallback implements InputManagerService.WindowManagerCal } } + @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/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp index ae9a34efc222..c1d8382fcd0e 100644 --- a/services/tests/InputMethodSystemServerTests/Android.bp +++ b/services/tests/InputMethodSystemServerTests/Android.bp @@ -73,10 +73,7 @@ android_ravenwood_test { 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/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java index 5d8f57866f7d..e094111c327a 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 @@ public class MockingOomAdjusterTests { @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 efea21428937..63c572af37b2 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 @@ public class HearingDevicePhoneCallNotificationControllerTest { HearingDevicePhoneCallNotificationController.CallStateListener { TestCallStateListener(@NonNull Context context) { - super(context); + super(context, context.getMainExecutor()); } @Override 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 1af59daa9c78..5922b12edc1e 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 com.google.common.truth.Truth.assertThat; 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.content.Context; 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 @@ public class TouchExplorerTest { 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 000000000000..3e7d9fd05327 --- /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 3ca019728c2b..fcdf88f16550 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 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { } @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 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { } @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 f88492477487..e56fd3c6272d 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 @@ class TestPhoneWindowManager { 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/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java index ae6144713a1d..5401a44d7016 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 @@ public class WindowOrganizerTests extends WindowTestsBase { 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/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt index e99c81493394..794fd0255726 100644 --- a/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt +++ b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt @@ -214,9 +214,5 @@ class KeyGestureEventHandlerTest { ): 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 2799f6c79215..4f1fb6487b19 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.InputGestureData 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 android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE 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.Test 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 @@ class KeyGestureControllerTests { 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 @@ class KeyGestureControllerTests { @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 @@ class KeyGestureControllerTests { 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 @@ class KeyGestureControllerTests { ) ), 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 @@ class KeyGestureControllerTests { ) } + @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 @@ class KeyGestureControllerTests { 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 @@ class KeyGestureControllerTests { 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 @@ class KeyGestureControllerTests { override fun handleKeyGesture(event: AidlKeyGestureEvent, token: IBinder?): Boolean { return handler(event, token) } - - override fun isKeyGestureSupported(gestureType: Int): Boolean { - return true - } } } diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java index 4d379e45a81a..bb54a26036db 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 @@ public class TestLooper { * 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 0d261abd728d..e51477c668dd 100644 --- a/tools/aapt2/Resource.h +++ b/tools/aapt2/Resource.h @@ -249,6 +249,8 @@ struct ResourceFile { // Flag std::optional<FeatureFlagAttribute> flag; + + bool uses_readwrite_feature_flags = false; }; /** diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp index 5435cba290fc..db7dddc49a99 100644 --- a/tools/aapt2/ResourceTable.cpp +++ b/tools/aapt2/ResourceTable.cpp @@ -664,6 +664,7 @@ bool ResourceTable::AddResource(NewResource&& res, android::IDiagnostics* diag) 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 @@ bool ResourceTable::AddResource(NewResource&& res, android::IDiagnostics* diag) 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 @@ NewResourceBuilder& NewResourceBuilder::SetAllowMangled(bool allow_mangled) { 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 b0e185536d16..778b43adb50b 100644 --- a/tools/aapt2/ResourceTable.h +++ b/tools/aapt2/ResourceTable.h @@ -104,6 +104,9 @@ class ResourceConfigValue { // 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 @@ struct NewResource { 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 @@ struct NewResourceBuilder { 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 2a7921600477..755dbb6f8e42 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -673,11 +673,13 @@ bool ResourceFileFlattener::Flatten(ResourceTable* table, IArchiveWriter* archiv // 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 2e20e8175213..bac871b8bdc3 100644 --- a/tools/aapt2/format/binary/BinaryResourceParser.cpp +++ b/tools/aapt2/format/binary/BinaryResourceParser.cpp @@ -414,6 +414,8 @@ bool BinaryResourceParser::ParseType(const ResourceTablePackage* package, .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 9dc205f4c1ba..0be392164453 100644 --- a/tools/aapt2/format/binary/ResEntryWriter.cpp +++ b/tools/aapt2/format/binary/ResEntryWriter.cpp @@ -199,6 +199,10 @@ void WriteEntry(const FlatEntry* entry, T* out_result, bool compact = false) { 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 c11598ec12f7..f54b29aa8f2a 100644 --- a/tools/aapt2/format/binary/ResEntryWriter.h +++ b/tools/aapt2/format/binary/ResEntryWriter.h @@ -38,6 +38,8 @@ struct FlatEntry { // 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 1a82021bce71..50144ae816b6 100644 --- a/tools/aapt2/format/binary/TableFlattener.cpp +++ b/tools/aapt2/format/binary/TableFlattener.cpp @@ -502,7 +502,8 @@ class PackageFlattener { // 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 0f1168514c4a..9156b96b67ec 100644 --- a/tools/aapt2/format/binary/TableFlattener_test.cpp +++ b/tools/aapt2/format/binary/TableFlattener_test.cpp @@ -1069,4 +1069,23 @@ TEST_F(TableFlattenerTest, FlattenTypeEntryWithNameCollapseInExemption) { 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 dbef77615515..47a71fe36e9f 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 @@ TEST_F(FlaggedResourcesTest, ReadWriteFlagInPathFails) { "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 75c6f17dcb51..8a3337c446cb 100644 --- a/tools/aapt2/link/FlaggedXmlVersioner.cpp +++ b/tools/aapt2/link/FlaggedXmlVersioner.cpp @@ -66,6 +66,28 @@ class AllDisabledFlagsVisitor : public xml::Visitor { 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 @@ std::vector<std::unique_ptr<xml::XmlResource>> FlaggedXmlVersioner::Process(IAap // 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)); } } |