Merge "Add AppFunctionService." into main
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 69b5222..3d1a785 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -393,7 +393,7 @@
* changes.
*
* <p>This broadcast is only sent to registered receivers and receivers in packages that have
- * been granted Do Not Disturb access (see {@link #isNotificationPolicyAccessGranted()}).
+ * been granted Notification Policy access (see {@link #isNotificationPolicyAccessGranted()}).
*/
@FlaggedApi(Flags.FLAG_MODES_API)
@SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
@@ -1627,7 +1627,7 @@
}
/**
- * Checks the ability to modify notification do not disturb policy for the calling package.
+ * Checks the ability to modify Notification Policy for the calling package.
*
* <p>
* Returns true if the calling package can modify notification policy.
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 114a2c4..cb38cf2 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -475,7 +475,7 @@
if (service == null
&& ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)
&& android.server.Flags.allowRemovingVpnService()) {
- throw new ServiceNotFoundException(Context.VPN_MANAGEMENT_SERVICE);
+ return null;
}
return new VpnManager(ctx, service);
}});
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index f751a23..f05c24f 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -21,6 +21,13 @@
}
flag {
+ name: "modes_ui_test"
+ namespace: "systemui"
+ description: "Guards new CTS tests for Modes; dependent on flags modes_api and modes_ui"
+ bug: "360862012"
+}
+
+flag {
name: "api_tvextender"
is_exported: true
namespace: "systemui"
diff --git a/core/java/android/content/pm/TEST_MAPPING b/core/java/android/content/pm/TEST_MAPPING
index c7716e5..ffadd1e 100644
--- a/core/java/android/content/pm/TEST_MAPPING
+++ b/core/java/android/content/pm/TEST_MAPPING
@@ -112,6 +112,39 @@
"exclude-annotation":"org.junit.Ignore"
}
]
+ },
+ {
+ "name": "CtsPackageInstallerCUJInstallationTestCases",
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
+ "name": "CtsPackageInstallerCUJUninstallationTestCases",
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
+ "name": "CtsPackageInstallerCUJUpdateSelfTestCases",
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
}
],
"presubmit-large":[
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 184bac4..85d2325 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -1972,10 +1972,10 @@
"android.provider.extra.NOTIFICATION_LISTENER_COMPONENT_NAME";
/**
- * Activity Action: Show Do Not Disturb access settings.
+ * Activity Action: Show Notification Policy access settings.
* <p>
- * Users can grant and deny access to Do Not Disturb configuration from here. Managed
- * profiles cannot grant Do Not Disturb access.
+ * Users can grant and deny access to Notification Policy (DND / Priority Modes) configuration
+ * from here. Managed profiles cannot grant Notification Policy access.
* See {@link android.app.NotificationManager#isNotificationPolicyAccessGranted()} for more
* details.
* <p>
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index 1c3d738..40070c7 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -259,6 +259,16 @@
}
flag {
+ name: "dont_break_email_in_nobreak_tag"
+ namespace: "text"
+ description: "Prevent line break inside email."
+ bug: "350691716"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "handwriting_gesture_with_transformation"
namespace: "text"
description: "Fix handwriting gesture is not working when view has transformation."
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 017e004..67a207e 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -3462,6 +3462,15 @@
public static final int PRIVATE_FLAG_NOT_MAGNIFIABLE = 1 << 22;
/**
+ * Indicates that the window should receive key events including Action/Meta key.
+ * They will not be intercepted as usual and instead will be passed to the window with other
+ * key events.
+ * TODO(b/358569822) Remove this once we have nicer API for listening to shortcuts
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_ALLOW_ACTION_KEY_EVENTS = 1 << 23;
+
+ /**
* Flag to indicate that the window is color space agnostic, and the color can be
* interpreted to any color space.
* @hide
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index eb35817..1922327 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -83,6 +83,7 @@
import android.os.Process;
import android.os.RemoteException;
import android.os.StrictMode;
+import android.os.Trace;
import android.os.UserHandle;
import android.system.Os;
import android.text.TextUtils;
@@ -6246,6 +6247,18 @@
private View inflateView(Context context, RemoteViews rv, @Nullable ViewGroup parent,
@StyleRes int applyThemeResId, @Nullable ColorResources colorResources) {
+ try {
+ Trace.beginSection(rv.hasDrawInstructions()
+ ? "RemoteViews#inflateViewWithDrawInstructions"
+ : "RemoteViews#inflateView");
+ return inflateViewInternal(context, rv, parent, applyThemeResId, colorResources);
+ } finally {
+ Trace.endSection();
+ }
+ }
+
+ private View inflateViewInternal(Context context, RemoteViews rv, @Nullable ViewGroup parent,
+ @StyleRes int applyThemeResId, @Nullable ColorResources colorResources) {
// RemoteViews may be built by an application installed in another
// user. So build a context that loads resources from that user but
// still returns the current users userId so settings like data / time formats
@@ -6384,7 +6397,7 @@
private View mResult;
private ViewTree mTree;
- private Action[] mActions;
+ private List<Action> mActions;
private Exception mError;
private AsyncApplyTask(
@@ -6411,11 +6424,20 @@
if (mRV.mActions != null) {
int count = mRV.mActions.size();
- mActions = new Action[count];
- for (int i = 0; i < count && !isCancelled(); i++) {
- // TODO: check if isCancelled in nested views.
- mActions[i] = mRV.mActions.get(i)
- .initActionAsync(mTree, mParent, mApplyParams);
+ mActions = new ArrayList<>(count);
+ try {
+ Trace.beginSection(hasDrawInstructions()
+ ? "RemoteViews#initActionAsyncWithDrawInstructions"
+ : "RemoteViews#initActionAsync");
+ for (Action action : mRV.mActions) {
+ if (isCancelled()) {
+ break;
+ }
+ // TODO: check if isCancelled in nested views.
+ mActions.add(action.initActionAsync(mTree, mParent, mApplyParams));
+ }
+ } finally {
+ Trace.endSection();
}
} else {
mActions = null;
@@ -6437,14 +6459,7 @@
try {
if (mActions != null) {
-
- ActionApplyParams applyParams = mApplyParams.clone();
- if (applyParams.handler == null) {
- applyParams.handler = DEFAULT_INTERACTION_HANDLER;
- }
- for (Action a : mActions) {
- a.apply(viewTree.mRoot, mParent, applyParams);
- }
+ mRV.performApply(viewTree.mRoot, mParent, mApplyParams, mActions);
}
// If the parent of the view is has is a root, resolve the recycling.
if (mTopLevel && mResult instanceof ViewGroup) {
@@ -6620,6 +6635,11 @@
}
private void performApply(View v, ViewGroup parent, ActionApplyParams params) {
+ performApply(v, parent, params, mActions);
+ }
+
+ private void performApply(
+ View v, ViewGroup parent, ActionApplyParams params, List<Action> actions) {
params = params.clone();
if (params.handler == null) {
params.handler = DEFAULT_INTERACTION_HANDLER;
@@ -6630,8 +6650,15 @@
}
if (mActions != null) {
final int count = mActions.size();
- for (int i = 0; i < count; i++) {
- mActions.get(i).apply(v, parent, params);
+ try {
+ Trace.beginSection(hasDrawInstructions()
+ ? "RemoteViews#applyActionsWithDrawInstructions"
+ : "RemoteViews#applyActions");
+ for (int i = 0; i < count; i++) {
+ mActions.get(i).apply(v, parent, params);
+ }
+ } finally {
+ Trace.endSection();
}
}
}
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index 383033d..118acac 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -282,6 +282,11 @@
<string name="config_satellite_emergency_handover_intent_action" translatable="false"></string>
<java-symbol type="string" name="config_satellite_emergency_handover_intent_action" />
+ <!-- The action of the intent that hidden menu sends to the app to launch demo mode for sos
+ emergency messaging via satellite. -->
+ <string name="config_satellite_demo_mode_sos_intent_action" translatable="false"></string>
+ <java-symbol type="string" name="config_satellite_demo_mode_sos_intent_action" />
+
<!-- Whether outgoing satellite datagrams should be sent to modem in demo mode. When satellite
is enabled for demo mode, if this config is enabled, outgoing datagrams will be sent to
modem; otherwise, success results will be returned. If demo mode is disabled, outgoing
diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml
index a115c65..38ea4ac 100644
--- a/data/etc/com.android.systemui.xml
+++ b/data/etc/com.android.systemui.xml
@@ -93,5 +93,6 @@
<permission name="android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS" />
<permission name="android.permission.CONTROL_UI_TRACING" />
<permission name="android.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND" />
+ <permission name="android.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW"/>
</privapp-permissions>
</permissions>
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 1fe6ca7..9a55b80 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -592,6 +592,10 @@
<permission name="android.permission.INTERACT_ACROSS_USERS" />
</privapp-permissions>
+ <privapp-permissions package="com.android.providers.tv">
+ <permission name="android.permission.INTERACT_ACROSS_USERS"/>
+ </privapp-permissions>
+
<privapp-permissions package="com.android.tv">
<permission name="android.permission.CHANGE_HDMI_CEC_ACTIVE_SOURCE"/>
<permission name="android.permission.DVB_DEVICE"/>
diff --git a/data/fonts/font_fallback.xml b/data/fonts/font_fallback.xml
index 53024ab..ae50da1 100644
--- a/data/fonts/font_fallback.xml
+++ b/data/fonts/font_fallback.xml
@@ -304,7 +304,7 @@
<font postScriptName="NotoSansBengali-Regular" supportedAxes="wght">
NotoSansBengali-VF.ttf
</font>
- <font fallbackFor="serif" postScriptName="NotoSerifBengali-Regular">
+ <font fallbackFor="serif" postScriptName="NotoSerifBengali-Regular" supportedAxes="wght">
NotoSerifBengali-VF.ttf
</font>
</family>
@@ -354,7 +354,7 @@
<font postScriptName="NotoSansSinhala-Regular" supportedAxes="wght">
NotoSansSinhala-VF.ttf
</font>
- <font fallbackFor="serif" postScriptName="NotoSerifSinhala-Regular">
+ <font fallbackFor="serif" postScriptName="NotoSerifSinhala-Regular" supportedAxes="wght">
NotoSerifSinhala-VF.ttf
</font>
</family>
@@ -927,23 +927,23 @@
NotoSansMedefaidrin-VF.ttf
</font>
</family>
- <family lang="und-Soyo" supportedAxes="wght">
- <font postScriptName="NotoSansSoyombo-Regular">
+ <family lang="und-Soyo">
+ <font postScriptName="NotoSansSoyombo-Regular" supportedAxes="wght">
NotoSansSoyombo-VF.ttf
</font>
</family>
- <family lang="und-Takr" supportedAxes="wght">
- <font postScriptName="NotoSansTakri-Regular">
+ <family lang="und-Takr">
+ <font postScriptName="NotoSansTakri-Regular" supportedAxes="wght">
NotoSansTakri-VF.ttf
</font>
</family>
- <family lang="und-Hmnp" supportedAxes="wght">
- <font postScriptName="NotoSerifHmongNyiakeng-Regular">
+ <family lang="und-Hmnp">
+ <font postScriptName="NotoSerifHmongNyiakeng-Regular" supportedAxes="wght">
NotoSerifNyiakengPuachueHmong-VF.ttf
</font>
</family>
- <family lang="und-Yezi" supportedAxes="wght">
- <font postScriptName="NotoSerifYezidi-Regular">
+ <family lang="und-Yezi">
+ <font postScriptName="NotoSerifYezidi-Regular" supportedAxes="wght">
NotoSerifYezidi-VF.ttf
</font>
</family>
diff --git a/data/fonts/font_fallback_cjkvf.xml b/data/fonts/font_fallback_cjkvf.xml
index a4ee825..407d704 100644
--- a/data/fonts/font_fallback_cjkvf.xml
+++ b/data/fonts/font_fallback_cjkvf.xml
@@ -304,7 +304,7 @@
<font postScriptName="NotoSansBengali-Regular" supportedAxes="wght">
NotoSansBengali-VF.ttf
</font>
- <font fallbackFor="serif" postScriptName="NotoSerifBengali-Regular">
+ <font fallbackFor="serif" postScriptName="NotoSerifBengali-Regular" supportedAxes="wght">
NotoSerifBengali-VF.ttf
</font>
</family>
@@ -354,7 +354,7 @@
<font postScriptName="NotoSansSinhala-Regular" supportedAxes="wght">
NotoSansSinhala-VF.ttf
</font>
- <font fallbackFor="serif" postScriptName="NotoSerifSinhala-Regular">
+ <font fallbackFor="serif" postScriptName="NotoSerifSinhala-Regular" supportedAxes="wght">
NotoSerifSinhala-VF.ttf
</font>
</family>
@@ -943,23 +943,23 @@
NotoSansMedefaidrin-VF.ttf
</font>
</family>
- <family lang="und-Soyo" supportedAxes="wght">
- <font postScriptName="NotoSansSoyombo-Regular">
+ <family lang="und-Soyo">
+ <font postScriptName="NotoSansSoyombo-Regular" supportedAxes="wght">
NotoSansSoyombo-VF.ttf
</font>
</family>
- <family lang="und-Takr" supportedAxes="wght">
- <font postScriptName="NotoSansTakri-Regular">
+ <family lang="und-Takr">
+ <font postScriptName="NotoSansTakri-Regular" supportedAxes="wght">
NotoSansTakri-VF.ttf
</font>
</family>
- <family lang="und-Hmnp" supportedAxes="wght">
- <font postScriptName="NotoSerifHmongNyiakeng-Regular">
+ <family lang="und-Hmnp">
+ <font postScriptName="NotoSerifHmongNyiakeng-Regular" supportedAxes="wght">
NotoSerifNyiakengPuachueHmong-VF.ttf
</font>
</family>
- <family lang="und-Yezi" supportedAxes="wght">
- <font postScriptName="NotoSerifYezidi-Regular">
+ <family lang="und-Yezi">
+ <font postScriptName="NotoSerifYezidi-Regular" supportedAxes="wght">
NotoSerifYezidi-VF.ttf
</font>
</family>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index dabfeeb..3dc33c2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -2217,7 +2217,6 @@
// And since all children are removed, remove the summary.
removeCallback.accept(-1);
- // TODO: (b/145659174) remove references to mSuppressedGroupKeys once fully migrated
mBubbleData.addSummaryToSuppress(summary.getStatusBarNotification().getGroupKey(),
summary.getKey());
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index b3beb4a..2de545a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -695,6 +695,16 @@
return;
}
+ if (mSplitScreenOptional.isPresent()) {
+ // If pip activity will reparent to origin task case and if the origin task still
+ // under split root, apply exit split transaction to make it expand to fullscreen.
+ SplitScreenController split = mSplitScreenOptional.get();
+ if (split.isTaskInSplitScreen(mTaskInfo.lastParentTaskIdBeforePip)) {
+ split.prepareExitSplitScreen(wct, split.getStageOfTask(
+ mTaskInfo.lastParentTaskIdBeforePip),
+ SplitScreenController.EXIT_REASON_APP_FINISHED);
+ }
+ }
mPipTransitionController.startExitTransition(TRANSIT_EXIT_PIP, wct, destinationBounds);
return;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java
index b7b42c7..209fc39 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java
@@ -114,7 +114,6 @@
t.clear();
mMainExecutor.execute(() -> {
finishCallback.onTransitionFinished(wct);
- mRemote = null;
});
}
};
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindow.kt
index 5bc975b..b6bca7a 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindow.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindow.kt
@@ -83,5 +83,8 @@
@After
fun teardown() {
testApp.exit(wmHelper)
+ mailApp.exit(wmHelper)
+ newTasksApp.exit(wmHelper)
+ imeApp.exit(wmHelper)
}
}
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindowAndPip.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindowAndPip.kt
new file mode 100644
index 0000000..285ea13
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindowAndPip.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.scenarios
+
+import android.platform.test.annotations.Postsubmit
+import android.app.Instrumentation
+import android.tools.NavBar
+import android.tools.Rotation
+import android.tools.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
+import com.android.server.wm.flicker.helpers.ImeAppHelper
+import com.android.server.wm.flicker.helpers.MailAppHelper
+import com.android.server.wm.flicker.helpers.NewTasksAppHelper
+import com.android.server.wm.flicker.helpers.PipAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.window.flags.Flags
+import com.android.wm.shell.Utils
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+open class ResizeAppCornerMultiWindowAndPip
+@JvmOverloads
+constructor(val rotation: Rotation = Rotation.ROTATION_0,
+ val horizontalChange: Int = 50,
+ val verticalChange: Int = -50) {
+
+ private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val tapl = LauncherInstrumentation()
+ private val wmHelper = WindowManagerStateHelper(instrumentation)
+ private val device = UiDevice.getInstance(instrumentation)
+ private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation))
+ private val pipApp = PipAppHelper(instrumentation)
+ private val mailApp = DesktopModeAppHelper(MailAppHelper(instrumentation))
+ private val newTasksApp = DesktopModeAppHelper(NewTasksAppHelper(instrumentation))
+ private val imeApp = DesktopModeAppHelper(ImeAppHelper(instrumentation))
+
+ @Rule
+ @JvmField
+ val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation)
+
+ @Before
+ fun setup() {
+ Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
+ tapl.setEnableRotation(true)
+ tapl.setExpectedRotation(rotation.value)
+ // Set string extra to ensure the app is on PiP mode at launch
+ pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = mapOf("enter_pip" to "true"))
+ testApp.enterDesktopWithDrag(wmHelper, device)
+ mailApp.launchViaIntent(wmHelper)
+ newTasksApp.launchViaIntent(wmHelper)
+ imeApp.launchViaIntent(wmHelper)
+ }
+
+ @Test
+ open fun resizeAppWithCornerResize() {
+ imeApp.cornerResize(wmHelper,
+ device,
+ DesktopModeAppHelper.Corners.RIGHT_TOP,
+ horizontalChange,
+ verticalChange)
+ }
+
+ @After
+ fun teardown() {
+ testApp.exit(wmHelper)
+ pipApp.exit(wmHelper)
+ mailApp.exit(wmHelper)
+ newTasksApp.exit(wmHelper)
+ imeApp.exit(wmHelper)
+ }
+}
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index c36eda9..ca468fc 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -1027,7 +1027,7 @@
* <p>This method has no effect if the device implements a fixed volume policy
* as indicated by {@link #isVolumeFixed()}.
* <p>From N onward, ringer mode adjustments that would toggle Do Not Disturb are not allowed
- * unless the app has been granted Do Not Disturb Access.
+ * unless the app has been granted Notification Policy Access.
* See {@link NotificationManager#isNotificationPolicyAccessGranted()}.
*
* @param streamType The stream type to adjust. One of {@link #STREAM_VOICE_CALL},
@@ -1379,7 +1379,7 @@
* <p>This method has no effect if the device implements a fixed volume policy
* as indicated by {@link #isVolumeFixed()}.
* * <p>From N onward, ringer mode adjustments that would toggle Do Not Disturb are not allowed
- * unless the app has been granted Do Not Disturb Access.
+ * unless the app has been granted Notification Policy Access.
* See {@link NotificationManager#isNotificationPolicyAccessGranted()}.
* @param ringerMode The ringer mode, one of {@link #RINGER_MODE_NORMAL},
* {@link #RINGER_MODE_SILENT}, or {@link #RINGER_MODE_VIBRATE}.
@@ -1403,7 +1403,7 @@
* <p>This method has no effect if the device implements a fixed volume policy
* as indicated by {@link #isVolumeFixed()}.
* <p>From N onward, volume adjustments that would toggle Do Not Disturb are not allowed unless
- * the app has been granted Do Not Disturb Access.
+ * the app has been granted Notification Policy Access.
* See {@link NotificationManager#isNotificationPolicyAccessGranted()}.
* @param streamType The stream whose volume index should be set.
* @param index The volume index to set. See
@@ -8829,7 +8829,7 @@
* <p>This method has no effect if the device implements a fixed volume policy
* as indicated by {@link #isVolumeFixed()}.
* <p>From N onward, ringer mode adjustments that would toggle Do Not Disturb are not allowed
- * unless the app has been granted Do Not Disturb Access.
+ * unless the app has been granted Notification Policy Access.
* See {@link NotificationManager#isNotificationPolicyAccessGranted()}.
* <p>This API checks if the caller has the necessary permissions based on the provided
* component name, uid, and pid values.
@@ -8870,7 +8870,7 @@
* <p>This method has no effect if the device implements a fixed volume policy
* as indicated by {@link #isVolumeFixed()}.
* <p>From N onward, volume adjustments that would toggle Do Not Disturb are not allowed unless
- * the app has been granted Do Not Disturb Access.
+ * the app has been granted Notification Policy Access.
* See {@link NotificationManager#isNotificationPolicyAccessGranted()}.
* <p>This API checks if the caller has the necessary permissions based on the provided
* component name, uid, and pid values.
diff --git a/media/java/android/media/tv/ITvInputManager.aidl b/media/java/android/media/tv/ITvInputManager.aidl
index be93abb..87cb3e7 100644
--- a/media/java/android/media/tv/ITvInputManager.aidl
+++ b/media/java/android/media/tv/ITvInputManager.aidl
@@ -70,6 +70,7 @@
void releaseSession(in IBinder sessionToken, int userId);
int getClientPid(in String sessionId);
int getClientPriority(int useCase, in String sessionId);
+ int getClientUserId(in String sessionId);
void setMainSession(in IBinder sessionToken, int userId);
void setSurface(in IBinder sessionToken, in Surface surface, int userId);
diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java
index aed3e60e..25b6bfa 100644
--- a/media/java/android/media/tv/TvInputManager.java
+++ b/media/java/android/media/tv/TvInputManager.java
@@ -760,6 +760,14 @@
* @hide
*/
public static final int UNKNOWN_CLIENT_PID = -1;
+ /**
+ * An unknown state of the client userId gets from the TvInputManager. Client gets this value
+ * when query through {@link #getClientUserId(String sessionId)} fails.
+ *
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_KIDS_MODE_TVDB_SHARING)
+ public static final int UNKNOWN_CLIENT_USER_ID = -1;
/**
* Broadcast intent action when the user blocked content ratings change. For use with the
@@ -2510,6 +2518,21 @@
};
/**
+ * Get a the client user id when creating the session with the session id provided.
+ *
+ * @param sessionId a String of session id that is used to query the client user id.
+ * @return the client pid when created the session. Returns {@link #UNKNOWN_CLIENT_USER_ID}
+ * if the call fails.
+ *
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.TUNER_RESOURCE_ACCESS)
+ @FlaggedApi(Flags.FLAG_KIDS_MODE_TVDB_SHARING)
+ public int getClientUserId(@NonNull String sessionId) {
+ return getClientUserIdInternal(sessionId);
+ }
+
+ /**
* Returns a priority for the given use case type and the client's foreground or background
* status.
*
@@ -2599,6 +2622,18 @@
return clientPid;
}
+ @FlaggedApi(Flags.FLAG_KIDS_MODE_TVDB_SHARING)
+ private int getClientUserIdInternal(String sessionId) {
+ Preconditions.checkNotNull(sessionId);
+ int clientUserId = UNKNOWN_CLIENT_USER_ID;
+ try {
+ clientUserId = mService.getClientUserId(sessionId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ return clientUserId;
+ }
+
private int getClientPriorityInternal(int useCase, String sessionId) {
try {
return mService.getClientPriority(useCase, sessionId);
diff --git a/media/java/android/media/tv/flags/media_tv.aconfig b/media/java/android/media/tv/flags/media_tv.aconfig
index 3196ba1..d6e9e4e 100644
--- a/media/java/android/media/tv/flags/media_tv.aconfig
+++ b/media/java/android/media/tv/flags/media_tv.aconfig
@@ -31,4 +31,12 @@
namespace: "media_tv"
description: "Introduce ALWAYS_BOUND_TV_INPUT for TIS."
bug: "332201346"
+}
+
+flag {
+ name: "kids_mode_tvdb_sharing"
+ is_exported: true
+ namespace: "media_tv"
+ description: "Performance and Storage Optimization in Google TV Kids Mode."
+ bug: "288383796"
}
\ No newline at end of file
diff --git a/packages/PackageInstaller/TEST_MAPPING b/packages/PackageInstaller/TEST_MAPPING
index da6efee..717ec02 100644
--- a/packages/PackageInstaller/TEST_MAPPING
+++ b/packages/PackageInstaller/TEST_MAPPING
@@ -1,4 +1,39 @@
{
+ "presubmit": [
+ {
+ "name": "CtsPackageInstallerCUJInstallationTestCases",
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
+ "name": "CtsPackageInstallerCUJUninstallationTestCases",
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
+ "name": "CtsPackageInstallerCUJUpdateSelfTestCases",
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ }
+ ],
"postsubmit": [
{
"name": "CtsPackageInstallTestCases",
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 398c915..2e98c1f 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -376,6 +376,10 @@
<!-- Listen to (dis-)connection of external displays and enable / disable them. -->
<uses-permission android:name="android.permission.MANAGE_DISPLAYS" />
+ <!-- To be able to intercept meta key events, might need to be removed once b/358569822
+ is ready -->
+ <uses-permission android:name="android.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW" />
+
<protected-broadcast android:name="com.android.settingslib.action.REGISTER_SLICE_RECEIVER" />
<protected-broadcast android:name="com.android.settingslib.action.UNREGISTER_SLICE_RECEIVER" />
<protected-broadcast android:name="com.android.settings.flashlight.action.FLASHLIGHT_CHANGED" />
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 047c097..476fd8b 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -541,6 +541,16 @@
}
flag {
+ name: "clipboard_image_timeout"
+ namespace: "systemui"
+ description: "Wait for clipboard image to load before showing UI"
+ bug: "359864629"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "screenshot_action_dismiss_system_windows"
namespace: "systemui"
description: "Dismiss existing system windows when starting action from screenshot UI"
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index d15bda0..853dc6f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -60,6 +60,7 @@
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -631,7 +632,11 @@
modifier =
Modifier.weight(1f)
.fillMaxHeight()
- .padding(end = screenCornerRadius / 2f, bottom = navBarBottomHeight)
+ .padding(
+ end =
+ dimensionResource(R.dimen.notification_panel_margin_horizontal),
+ bottom = navBarBottomHeight
+ )
.then(brightnessMirrorShowingModifier)
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index 9792c28..1bc2e24 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -60,6 +60,7 @@
import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.whenever
+import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.flow.MutableStateFlow
@@ -2135,6 +2136,14 @@
@EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun glanceableHubToOccluded_communalKtfRefactor() =
testScope.runTest {
+ // GIVEN device is not dreaming
+ powerInteractor.setAwakeForTest()
+ keyguardRepository.setDreaming(false)
+ keyguardRepository.setDozeTransitionModel(
+ DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
+ )
+ advanceTimeBy(600.milliseconds)
+
// GIVEN a prior transition has run to GLANCEABLE_HUB
communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")
runCurrent()
@@ -2298,6 +2307,54 @@
coroutineContext.cancelChildren()
}
+ @Test
+ @BrokenWithSceneContainer(339465026)
+ @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+ fun glanceableHubToOccludedDoesNotTriggerWhenDreamStateChanges_communalKtfRefactor() =
+ testScope.runTest {
+ // GIVEN that we are dreaming and not dozing
+ powerInteractor.setAwakeForTest()
+ keyguardRepository.setDreaming(true)
+ keyguardRepository.setKeyguardOccluded(true)
+ keyguardRepository.setDozeTransitionModel(
+ DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
+ )
+ advanceTimeBy(700.milliseconds)
+ clearInvocations(transitionRepository)
+
+ // GIVEN a prior transition has run to GLANCEABLE_HUB
+ communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")
+ runCurrent()
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = CommunalSceneTransitionInteractor::class.simpleName,
+ from = KeyguardState.DREAMING,
+ to = KeyguardState.GLANCEABLE_HUB,
+ )
+ clearInvocations(transitionRepository)
+
+ // WHEN dream ends but we are still occluded
+ keyguardRepository.setDreaming(false)
+ runCurrent()
+ assertThat(transitionRepository).noTransitionsStarted()
+
+ // Simulate occlusion signal changing due to dream terminating and then occluding again
+ // due to a new activity starting a couple milliseconds later.
+ keyguardRepository.setKeyguardOccluded(false)
+ advanceTimeBy(10.milliseconds)
+ keyguardRepository.setKeyguardOccluded(true)
+ advanceTimeBy(200.milliseconds)
+
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = CommunalSceneTransitionInteractor::class.simpleName,
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.OCCLUDED,
+ )
+
+ coroutineContext.cancelChildren()
+ }
+
private suspend fun TestScope.runTransitionAndSetWakefulness(
from: KeyguardState,
to: KeyguardState
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/BaseActivatableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/BaseActivatableTest.kt
new file mode 100644
index 0000000..f6f58c9
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/BaseActivatableTest.kt
@@ -0,0 +1,328 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.lifecycle
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BaseActivatableTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+
+ private val underTest = FakeActivatable()
+
+ @Test
+ fun activate() =
+ testScope.runTest {
+ assertThat(underTest.isActive).isFalse()
+ assertThat(underTest.activationCount).isEqualTo(0)
+ assertThat(underTest.cancellationCount).isEqualTo(0)
+
+ underTest.activateIn(testScope)
+ runCurrent()
+ assertThat(underTest.isActive).isTrue()
+ assertThat(underTest.activationCount).isEqualTo(1)
+ assertThat(underTest.cancellationCount).isEqualTo(0)
+ }
+
+ @Test
+ fun activate_andCancel() =
+ testScope.runTest {
+ assertThat(underTest.isActive).isFalse()
+ assertThat(underTest.activationCount).isEqualTo(0)
+ assertThat(underTest.cancellationCount).isEqualTo(0)
+
+ val job = Job()
+ underTest.activateIn(testScope, context = job)
+ runCurrent()
+ assertThat(underTest.isActive).isTrue()
+ assertThat(underTest.activationCount).isEqualTo(1)
+ assertThat(underTest.cancellationCount).isEqualTo(0)
+
+ job.cancel()
+ runCurrent()
+ assertThat(underTest.isActive).isFalse()
+ assertThat(underTest.activationCount).isEqualTo(1)
+ assertThat(underTest.cancellationCount).isEqualTo(1)
+ }
+
+ @Test
+ fun activate_afterCancellation() =
+ testScope.runTest {
+ assertThat(underTest.isActive).isFalse()
+ assertThat(underTest.activationCount).isEqualTo(0)
+ assertThat(underTest.cancellationCount).isEqualTo(0)
+
+ val job = Job()
+ underTest.activateIn(testScope, context = job)
+ runCurrent()
+ assertThat(underTest.isActive).isTrue()
+ assertThat(underTest.activationCount).isEqualTo(1)
+ assertThat(underTest.cancellationCount).isEqualTo(0)
+
+ job.cancel()
+ runCurrent()
+ assertThat(underTest.isActive).isFalse()
+ assertThat(underTest.activationCount).isEqualTo(1)
+ assertThat(underTest.cancellationCount).isEqualTo(1)
+
+ underTest.activateIn(testScope)
+ runCurrent()
+ assertThat(underTest.isActive).isTrue()
+ assertThat(underTest.activationCount).isEqualTo(2)
+ assertThat(underTest.cancellationCount).isEqualTo(1)
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun activate_whileActive_throws() =
+ testScope.runTest {
+ assertThat(underTest.isActive).isFalse()
+ assertThat(underTest.activationCount).isEqualTo(0)
+ assertThat(underTest.cancellationCount).isEqualTo(0)
+
+ underTest.activateIn(testScope)
+ runCurrent()
+ assertThat(underTest.isActive).isTrue()
+ assertThat(underTest.activationCount).isEqualTo(1)
+ assertThat(underTest.cancellationCount).isEqualTo(0)
+
+ underTest.activateIn(testScope)
+ runCurrent()
+ }
+
+ @Test
+ fun addChild_beforeActive_activatesChildrenOnceActivated() =
+ testScope.runTest {
+ val child1 = FakeActivatable()
+ val child2 = FakeActivatable()
+ assertThat(child1.isActive).isFalse()
+ assertThat(child2.isActive).isFalse()
+
+ assertThat(underTest.isActive).isFalse()
+ underTest.addChild(child1)
+ underTest.addChild(child2)
+ assertThat(underTest.isActive).isFalse()
+ assertThat(child1.isActive).isFalse()
+ assertThat(child2.isActive).isFalse()
+
+ underTest.activateIn(this)
+ runCurrent()
+
+ assertThat(underTest.isActive).isTrue()
+ assertThat(child1.isActive).isTrue()
+ assertThat(child2.isActive).isTrue()
+ }
+
+ @Test
+ fun addChild_whileActive_activatesChildrenImmediately() =
+ testScope.runTest {
+ underTest.activateIn(this)
+ runCurrent()
+ assertThat(underTest.isActive).isTrue()
+
+ val child1 = FakeActivatable()
+ val child2 = FakeActivatable()
+ assertThat(child1.isActive).isFalse()
+ assertThat(child2.isActive).isFalse()
+
+ underTest.addChild(child1)
+ underTest.addChild(child2)
+ runCurrent()
+
+ assertThat(child1.isActive).isTrue()
+ assertThat(child2.isActive).isTrue()
+ }
+
+ @Test
+ fun addChild_afterCancellation_doesNotActivateChildren() =
+ testScope.runTest {
+ val job = Job()
+ underTest.activateIn(this, context = job)
+ runCurrent()
+ assertThat(underTest.isActive).isTrue()
+ job.cancel()
+ runCurrent()
+ assertThat(underTest.isActive).isFalse()
+
+ val child1 = FakeActivatable()
+ val child2 = FakeActivatable()
+ assertThat(child1.isActive).isFalse()
+ assertThat(child2.isActive).isFalse()
+
+ underTest.addChild(child1)
+ underTest.addChild(child2)
+ runCurrent()
+
+ assertThat(child1.isActive).isFalse()
+ assertThat(child2.isActive).isFalse()
+ }
+
+ @Test
+ fun activate_cancellation_cancelsCurrentChildren() =
+ testScope.runTest {
+ val job = Job()
+ underTest.activateIn(this, context = job)
+ runCurrent()
+ assertThat(underTest.isActive).isTrue()
+
+ val child1 = FakeActivatable()
+ val child2 = FakeActivatable()
+ assertThat(child1.isActive).isFalse()
+ assertThat(child2.isActive).isFalse()
+
+ underTest.addChild(child1)
+ underTest.addChild(child2)
+ runCurrent()
+
+ assertThat(child1.isActive).isTrue()
+ assertThat(child2.isActive).isTrue()
+
+ job.cancel()
+ runCurrent()
+ assertThat(underTest.isActive).isFalse()
+ assertThat(child1.isActive).isFalse()
+ assertThat(child2.isActive).isFalse()
+ }
+
+ @Test
+ fun activate_afterCancellation_reactivatesCurrentChildren() =
+ testScope.runTest {
+ val job = Job()
+ underTest.activateIn(this, context = job)
+ runCurrent()
+ assertThat(underTest.isActive).isTrue()
+
+ val child1 = FakeActivatable()
+ val child2 = FakeActivatable()
+ assertThat(child1.isActive).isFalse()
+ assertThat(child2.isActive).isFalse()
+
+ underTest.addChild(child1)
+ underTest.addChild(child2)
+ runCurrent()
+
+ assertThat(child1.isActive).isTrue()
+ assertThat(child2.isActive).isTrue()
+
+ job.cancel()
+ runCurrent()
+ assertThat(underTest.isActive).isFalse()
+ assertThat(child1.isActive).isFalse()
+ assertThat(child2.isActive).isFalse()
+
+ underTest.activateIn(this)
+ runCurrent()
+ assertThat(underTest.isActive).isTrue()
+ assertThat(child1.isActive).isTrue()
+ assertThat(child2.isActive).isTrue()
+ }
+
+ @Test
+ fun removeChild_beforeActive_neverActivatesChild() =
+ testScope.runTest {
+ val child1 = FakeActivatable()
+ val child2 = FakeActivatable()
+ assertThat(child1.isActive).isFalse()
+ assertThat(child2.isActive).isFalse()
+
+ assertThat(underTest.isActive).isFalse()
+ underTest.addChild(child1)
+ underTest.addChild(child2)
+ assertThat(underTest.isActive).isFalse()
+ assertThat(child1.isActive).isFalse()
+ assertThat(child2.isActive).isFalse()
+ }
+
+ @Test
+ fun removeChild_whileActive_cancelsChild() =
+ testScope.runTest {
+ val child1 = FakeActivatable()
+ val child2 = FakeActivatable()
+ assertThat(child1.isActive).isFalse()
+ assertThat(child2.isActive).isFalse()
+
+ assertThat(underTest.isActive).isFalse()
+ underTest.addChild(child1)
+ underTest.addChild(child2)
+ assertThat(underTest.isActive).isFalse()
+ assertThat(child1.isActive).isFalse()
+ assertThat(child2.isActive).isFalse()
+
+ underTest.activateIn(this)
+ runCurrent()
+ assertThat(underTest.isActive).isTrue()
+ assertThat(child1.isActive).isTrue()
+ assertThat(child2.isActive).isTrue()
+
+ underTest.removeChild(child1)
+ runCurrent()
+ assertThat(underTest.isActive).isTrue()
+ assertThat(child1.isActive).isFalse()
+ assertThat(child2.isActive).isTrue()
+ }
+
+ @Test
+ fun removeChild_afterCancellation_doesNotReactivateChildren() =
+ testScope.runTest {
+ val child1 = FakeActivatable()
+ val child2 = FakeActivatable()
+ assertThat(child1.isActive).isFalse()
+ assertThat(child2.isActive).isFalse()
+
+ assertThat(underTest.isActive).isFalse()
+ underTest.addChild(child1)
+ underTest.addChild(child2)
+ assertThat(underTest.isActive).isFalse()
+ assertThat(child1.isActive).isFalse()
+ assertThat(child2.isActive).isFalse()
+
+ val job = Job()
+ underTest.activateIn(this, context = job)
+ runCurrent()
+ assertThat(underTest.isActive).isTrue()
+ assertThat(child1.isActive).isTrue()
+ assertThat(child2.isActive).isTrue()
+
+ job.cancel()
+ runCurrent()
+ assertThat(underTest.isActive).isFalse()
+ assertThat(child1.isActive).isFalse()
+ assertThat(child2.isActive).isFalse()
+
+ underTest.removeChild(child1)
+ underTest.activateIn(this)
+ runCurrent()
+ assertThat(underTest.isActive).isTrue()
+ assertThat(child1.isActive).isFalse()
+ assertThat(child2.isActive).isTrue()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SafeActivatableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SafeActivatableTest.kt
deleted file mode 100644
index 9484821..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SafeActivatableTest.kt
+++ /dev/null
@@ -1,121 +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.
- */
-
-@file:OptIn(ExperimentalCoroutinesApi::class)
-
-package com.android.systemui.lifecycle
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.testKosmos
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class SafeActivatableTest : SysuiTestCase() {
-
- private val kosmos = testKosmos()
- private val testScope = kosmos.testScope
-
- private val underTest = FakeActivatable()
-
- @Test
- fun activate() =
- testScope.runTest {
- assertThat(underTest.isActive).isFalse()
- assertThat(underTest.activationCount).isEqualTo(0)
- assertThat(underTest.cancellationCount).isEqualTo(0)
-
- underTest.activateIn(testScope)
- runCurrent()
- assertThat(underTest.isActive).isTrue()
- assertThat(underTest.activationCount).isEqualTo(1)
- assertThat(underTest.cancellationCount).isEqualTo(0)
- }
-
- @Test
- fun activate_andCancel() =
- testScope.runTest {
- assertThat(underTest.isActive).isFalse()
- assertThat(underTest.activationCount).isEqualTo(0)
- assertThat(underTest.cancellationCount).isEqualTo(0)
-
- val job = Job()
- underTest.activateIn(testScope, context = job)
- runCurrent()
- assertThat(underTest.isActive).isTrue()
- assertThat(underTest.activationCount).isEqualTo(1)
- assertThat(underTest.cancellationCount).isEqualTo(0)
-
- job.cancel()
- runCurrent()
- assertThat(underTest.isActive).isFalse()
- assertThat(underTest.activationCount).isEqualTo(1)
- assertThat(underTest.cancellationCount).isEqualTo(1)
- }
-
- @Test
- fun activate_afterCancellation() =
- testScope.runTest {
- assertThat(underTest.isActive).isFalse()
- assertThat(underTest.activationCount).isEqualTo(0)
- assertThat(underTest.cancellationCount).isEqualTo(0)
-
- val job = Job()
- underTest.activateIn(testScope, context = job)
- runCurrent()
- assertThat(underTest.isActive).isTrue()
- assertThat(underTest.activationCount).isEqualTo(1)
- assertThat(underTest.cancellationCount).isEqualTo(0)
-
- job.cancel()
- runCurrent()
- assertThat(underTest.isActive).isFalse()
- assertThat(underTest.activationCount).isEqualTo(1)
- assertThat(underTest.cancellationCount).isEqualTo(1)
-
- underTest.activateIn(testScope)
- runCurrent()
- assertThat(underTest.isActive).isTrue()
- assertThat(underTest.activationCount).isEqualTo(2)
- assertThat(underTest.cancellationCount).isEqualTo(1)
- }
-
- @Test(expected = IllegalStateException::class)
- fun activate_whileActive_throws() =
- testScope.runTest {
- assertThat(underTest.isActive).isFalse()
- assertThat(underTest.activationCount).isEqualTo(0)
- assertThat(underTest.cancellationCount).isEqualTo(0)
-
- underTest.activateIn(testScope)
- runCurrent()
- assertThat(underTest.isActive).isTrue()
- assertThat(underTest.activationCount).isEqualTo(1)
- assertThat(underTest.cancellationCount).isEqualTo(0)
-
- underTest.activateIn(testScope)
- runCurrent()
- }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt
index dfc004a..c9869bdb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt
@@ -280,9 +280,12 @@
}
@Test
- fun isActiveFollowsPackageManagerAdapter() =
+ fun isActiveFollowsPackageManagerAdapter_user0() =
with(kosmos) {
testScope.runTest {
+ packageManagerAdapterFacade.setExclusiveForUser(0)
+
+ underTest.updateWithDefaults(UserHandle.of(0), TEST_DEFAULTS_1, true)
packageManagerAdapterFacade.setIsActive(false)
assertThat(underTest.isTileActive()).isFalse()
@@ -295,6 +298,7 @@
fun isToggleableFollowsPackageManagerAdapter() =
with(kosmos) {
testScope.runTest {
+ underTest.updateWithDefaults(UserHandle.of(0), TEST_DEFAULTS_1, true)
packageManagerAdapterFacade.setIsToggleable(false)
assertThat(underTest.isTileToggleable()).isFalse()
@@ -303,6 +307,66 @@
}
}
+ @Test
+ fun isActiveFollowsPackageManagerAdapter_user10_withAdapterForUser10() =
+ with(kosmos) {
+ testScope.runTest {
+ packageManagerAdapterFacade.setExclusiveForUser(10)
+
+ underTest.updateWithDefaults(UserHandle.of(10), TEST_DEFAULTS_1, true)
+ packageManagerAdapterFacade.setIsActive(false)
+ assertThat(underTest.isTileActive()).isFalse()
+
+ packageManagerAdapterFacade.setIsActive(true)
+ assertThat(underTest.isTileActive()).isTrue()
+ }
+ }
+
+ @Test
+ fun isToggleableFollowsPackageManagerAdapter_user10_withAdapterForUser10() =
+ with(kosmos) {
+ testScope.runTest {
+ packageManagerAdapterFacade.setExclusiveForUser(10)
+
+ underTest.updateWithDefaults(UserHandle.of(10), TEST_DEFAULTS_1, true)
+ packageManagerAdapterFacade.setIsToggleable(false)
+ assertThat(underTest.isTileToggleable()).isFalse()
+
+ packageManagerAdapterFacade.setIsToggleable(true)
+ assertThat(underTest.isTileToggleable()).isTrue()
+ }
+ }
+
+ @Test
+ fun isActiveDoesntFollowPackageManagerAdapter_user10() =
+ with(kosmos) {
+ testScope.runTest {
+ packageManagerAdapterFacade.setExclusiveForUser(0)
+
+ underTest.updateWithDefaults(UserHandle.of(10), TEST_DEFAULTS_1, true)
+ packageManagerAdapterFacade.setIsActive(false)
+ assertThat(underTest.isTileActive()).isFalse()
+
+ packageManagerAdapterFacade.setIsActive(true)
+ assertThat(underTest.isTileActive()).isFalse()
+ }
+ }
+
+ @Test
+ fun isToggleableDoesntFollowPackageManagerAdapter_user10() =
+ with(kosmos) {
+ testScope.runTest {
+ packageManagerAdapterFacade.setExclusiveForUser(0)
+
+ underTest.updateWithDefaults(UserHandle.of(10), TEST_DEFAULTS_1, true)
+ packageManagerAdapterFacade.setIsToggleable(false)
+ assertThat(underTest.isTileToggleable()).isFalse()
+
+ packageManagerAdapterFacade.setIsToggleable(true)
+ assertThat(underTest.isTileToggleable()).isFalse()
+ }
+ }
+
private companion object {
val TEST_COMPONENT = ComponentName("test.pkg", "test.cls")
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index bbb467f..64a13de 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -28,9 +28,7 @@
import com.android.internal.logging.uiEventLoggerFake
import com.android.internal.policy.IKeyguardDismissCallback
import com.android.systemui.SysuiTestCase
-import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
-import com.android.systemui.authentication.domain.interactor.authenticationInteractor
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
@@ -357,6 +355,7 @@
kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
SuccessFingerprintAuthenticationStatus(0, true)
)
+ runCurrent()
assertThat(currentSceneKey).isEqualTo(Scenes.QuickSettings)
assertThat(alternateBouncerVisible).isFalse()
@@ -507,6 +506,33 @@
}
@Test
+ fun hideAlternateBouncerAndNotifyDismissCancelledWhenDeviceSleeps() =
+ testScope.runTest {
+ val alternateBouncerVisible by
+ collectLastValue(bouncerRepository.alternateBouncerVisible)
+ val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
+ prepareState(
+ isDeviceUnlocked = false,
+ initialSceneKey = Scenes.Shade,
+ )
+ assertThat(currentSceneKey).isEqualTo(Scenes.Shade)
+ bouncerRepository.setAlternateVisible(true)
+ underTest.start()
+
+ // run all pending dismiss succeeded/cancelled calls from setup:
+ kosmos.fakeExecutor.runAllReady()
+
+ val dismissCallback: IKeyguardDismissCallback = mock()
+ kosmos.dismissCallbackRegistry.addCallback(dismissCallback)
+ powerInteractor.setAsleepForTest()
+ runCurrent()
+ kosmos.fakeExecutor.runAllReady()
+
+ assertThat(alternateBouncerVisible).isFalse()
+ verify(dismissCallback).onDismissCancelled()
+ }
+
+ @Test
fun switchToLockscreenWhenDeviceSleepsLocked() =
testScope.runTest {
val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
@@ -1644,19 +1670,27 @@
}
@Test
- fun notifyKeyguardDismissCallbacks_whenUnlocking_onDismissSucceeded() =
+ fun notifyKeyguardDismissCallbacks_whenUnlockingFromBouncer_onDismissSucceeded() =
testScope.runTest {
- val currentScene by collectLastValue(sceneInteractor.currentScene)
- prepareState()
+ val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
+ prepareState(
+ authenticationMethod = AuthenticationMethodModel.Pin,
+ isDeviceUnlocked = false,
+ initialSceneKey = Scenes.Bouncer,
+ )
+ assertThat(currentSceneKey).isEqualTo(Scenes.Bouncer)
underTest.start()
+
+ // run all pending dismiss succeeded/cancelled calls from setup:
+ kosmos.fakeExecutor.runAllReady()
+
val dismissCallback: IKeyguardDismissCallback = mock()
kosmos.dismissCallbackRegistry.addCallback(dismissCallback)
- // Switch to bouncer and unlock device:
- sceneInteractor.changeScene(Scenes.Bouncer, "")
- assertThat(currentScene).isEqualTo(Scenes.Bouncer)
- kosmos.authenticationInteractor.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)
- assertThat(currentScene).isEqualTo(Scenes.Gone)
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ SuccessFingerprintAuthenticationStatus(0, true)
+ )
+ runCurrent()
kosmos.fakeExecutor.runAllReady()
verify(dismissCallback).onDismissSucceeded()
@@ -1665,19 +1699,26 @@
@Test
fun notifyKeyguardDismissCallbacks_whenLeavingBouncer_onDismissCancelled() =
testScope.runTest {
+ val isUnlocked by collectLastValue(kosmos.deviceEntryInteractor.isUnlocked)
val currentScene by collectLastValue(sceneInteractor.currentScene)
prepareState()
underTest.start()
+
+ // run all pending dismiss succeeded/cancelled calls from setup:
+ kosmos.fakeExecutor.runAllReady()
+
val dismissCallback: IKeyguardDismissCallback = mock()
kosmos.dismissCallbackRegistry.addCallback(dismissCallback)
// Switch to bouncer:
sceneInteractor.changeScene(Scenes.Bouncer, "")
assertThat(currentScene).isEqualTo(Scenes.Bouncer)
+ runCurrent()
- // Return to lockscreen:
+ // Return to lockscreen when isUnlocked=false:
sceneInteractor.changeScene(Scenes.Lockscreen, "")
assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ assertThat(isUnlocked).isFalse()
runCurrent()
kosmos.fakeExecutor.runAllReady()
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index 04c6fa9..040af90 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
@@ -19,6 +19,7 @@
import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_SHOW_ACTIONS;
+import static com.android.systemui.Flags.clipboardImageTimeout;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_SHOWN;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_TAPPED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISSED_OTHER;
@@ -32,7 +33,6 @@
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TAP_OUTSIDE;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TIMED_OUT;
-import static com.android.systemui.flags.Flags.CLIPBOARD_IMAGE_TIMEOUT;
import static com.android.systemui.flags.Flags.CLIPBOARD_SHARED_TRANSITIONS;
import android.animation.Animator;
@@ -288,7 +288,7 @@
boolean shouldAnimate = !model.dataMatches(mClipboardModel) || wasExiting;
mClipboardModel = model;
mClipboardLogger.setClipSource(mClipboardModel.getSource());
- if (mFeatureFlags.isEnabled(CLIPBOARD_IMAGE_TIMEOUT)) {
+ if (clipboardImageTimeout()) {
if (shouldAnimate) {
reset();
mClipboardLogger.setClipSource(mClipboardModel.getSource());
@@ -452,7 +452,7 @@
mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_EXPANDED_FROM_MINIMIZED);
mIsMinimized = false;
}
- if (mFeatureFlags.isEnabled(CLIPBOARD_IMAGE_TIMEOUT)) {
+ if (clipboardImageTimeout()) {
setExpandedView(() -> animateIn());
} else {
setExpandedView();
@@ -522,7 +522,7 @@
mInputMonitor.getInputChannel(), Looper.getMainLooper()) {
@Override
public void onInputEvent(InputEvent event) {
- if ((!mFeatureFlags.isEnabled(CLIPBOARD_IMAGE_TIMEOUT) || mShowingUi)
+ if ((!clipboardImageTimeout() || mShowingUi)
&& event instanceof MotionEvent) {
MotionEvent motionEvent = (MotionEvent) event;
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
index 0aa50e0..199caa1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
@@ -37,16 +37,21 @@
import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
import com.android.systemui.util.kotlin.BooleanFlowOperators.noneOf
import com.android.systemui.util.kotlin.BooleanFlowOperators.not
+import com.android.systemui.util.kotlin.Utils.Companion.sampleFilter
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+@OptIn(FlowPreview::class)
@SysUISingleton
class FromGlanceableHubTransitionInteractor
@Inject
@@ -196,19 +201,26 @@
}
} else if (communalSceneKtfRefactor()) {
scope.launch {
- allOf(
+ combine(
keyguardInteractor.isKeyguardOccluded,
- noneOf(
- // Dream is a special-case of occluded, so filter out the dreaming
- // case here.
- keyguardInteractor.isDreaming,
- // When launching activities from widgets on the hub, we have a
- // custom occlusion animation.
- communalSceneInteractor.isLaunchingWidget,
- ),
+ keyguardInteractor.isAbleToDream
+ // Debounce the dreaming signal since there is a race condition between
+ // the occluded and dreaming signals. We therefore add a small delay
+ // to give enough time for occluded to flip to false when the dream
+ // ends, to avoid transitioning to OCCLUDED erroneously when exiting
+ // the dream.
+ .debounce(100.milliseconds),
+ ::Pair
)
- .filterRelevantKeyguardStateAnd { isOccludedAndNotDreamingNorLaunchingWidget ->
- isOccludedAndNotDreamingNorLaunchingWidget
+ .sampleFilter(
+ // When launching activities from widgets on the hub, we have a
+ // custom occlusion animation.
+ communalSceneInteractor.isLaunchingWidget,
+ ) { launchingWidget ->
+ !launchingWidget
+ }
+ .filterRelevantKeyguardStateAnd { (isOccluded, isDreaming) ->
+ isOccluded && !isDreaming
}
.collect { _ ->
communalSceneInteractor.changeScene(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt
index 1c445a7..7801c00 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt
@@ -17,15 +17,16 @@
package com.android.systemui.keyguard.domain.interactor
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.keyguard.shared.model.DismissAction
import com.android.systemui.keyguard.shared.model.KeyguardDone
-import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
+import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.domain.resolver.NotifShadeSceneFamilyResolver
import com.android.systemui.scene.domain.resolver.QuickSettingsSceneFamilyResolver
@@ -61,6 +62,8 @@
deviceEntryInteractor: DeviceEntryInteractor,
quickSettingsSceneFamilyResolver: QuickSettingsSceneFamilyResolver,
notifShadeSceneFamilyResolver: NotifShadeSceneFamilyResolver,
+ powerInteractor: PowerInteractor,
+ alternateBouncerInteractor: AlternateBouncerInteractor,
) {
val dismissAction: Flow<DismissAction> = repository.dismissAction
@@ -124,10 +127,12 @@
scene = Scenes.Bouncer,
stateWithoutSceneContainer = PRIMARY_BOUNCER
),
- transitionInteractor.isFinishedIn(state = ALTERNATE_BOUNCER),
+ alternateBouncerInteractor.isVisible,
isOnShadeWhileUnlocked,
- ) { isOnGone, isOnBouncer, isOnAltBouncer, isOnShadeWhileUnlocked ->
- !isOnGone && !isOnBouncer && !isOnAltBouncer && !isOnShadeWhileUnlocked
+ powerInteractor.isAsleep,
+ ) { isOnGone, isOnBouncer, isOnAltBouncer, isOnShadeWhileUnlocked, isAsleep ->
+ (!isOnGone && !isOnBouncer && !isOnAltBouncer && !isOnShadeWhileUnlocked) ||
+ isAsleep
}
.filter { it }
.sampleFilter(dismissAction) { it !is DismissAction.None }
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/BaseActivatable.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/BaseActivatable.kt
new file mode 100644
index 0000000..03476ec
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/lifecycle/BaseActivatable.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.lifecycle
+
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+
+/**
+ * A base [Activatable] with the following characteristics:
+ * 1. **Can be concurrently activated by no more than one owner.** A previous call to [activate]
+ * must be canceled before a new call to [activate] can be made. Trying to call [activate] while
+ * already active will fail with an error
+ * 2. **Can manage child [Activatable]s**. See [addChild] and [removeChild]. Added children
+ * automatically track the activation state of the parent such that when the parent is active,
+ * the children are active and vice-versa. Children are also retained such that deactivating the
+ * parent and reactivating it also cancels and reactivates the children.
+ */
+abstract class BaseActivatable : Activatable {
+
+ private val _isActive = AtomicBoolean(false)
+
+ var isActive: Boolean
+ get() = _isActive.get()
+ private set(value) {
+ _isActive.set(value)
+ }
+
+ final override suspend fun activate(): Nothing {
+ val allowed = _isActive.compareAndSet(false, true)
+ check(allowed) { "Cannot activate an already active activatable!" }
+
+ coroutineScope {
+ try {
+ launch { manageChildren() }
+ onActivated()
+ } finally {
+ isActive = false
+ }
+ }
+ }
+
+ /**
+ * Notifies that the [Activatable] has been activated.
+ *
+ * Serves as an entrypoint to kick off coroutine work that the object requires in order to keep
+ * its state fresh and/or perform side-effects.
+ *
+ * The method suspends and doesn't return until all work required by the object is finished. In
+ * most cases, it's expected for the work to remain ongoing forever so this method will forever
+ * suspend its caller until the coroutine that called it is canceled.
+ *
+ * Implementations could follow this pattern:
+ * ```kotlin
+ * override suspend fun onActivated(): Nothing {
+ * coroutineScope {
+ * launch { ... }
+ * launch { ... }
+ * launch { ... }
+ * }
+ * }
+ * ```
+ *
+ * @see activate
+ */
+ protected abstract suspend fun onActivated(): Nothing
+
+ private val newChildren = Channel<Activatable>(Channel.BUFFERED)
+ private val jobByChild: MutableMap<Activatable, Job> by lazy { mutableMapOf() }
+
+ private suspend fun manageChildren(): Nothing {
+ coroutineScope {
+ // Reactivate children that were added during a previous activation:
+ jobByChild.keys.forEach { child -> jobByChild[child] = launch { child.activate() } }
+
+ // Process requests to add more children:
+ newChildren.receiveAsFlow().collect { newChild ->
+ removeChildInternal(newChild)
+ jobByChild[newChild] = launch { newChild.activate() }
+ }
+
+ awaitCancellation()
+ }
+ }
+
+ fun addChild(child: Activatable) {
+ newChildren.trySend(child)
+ }
+
+ fun removeChild(child: Activatable) {
+ removeChildInternal(child)
+ }
+
+ private fun removeChildInternal(child: Activatable) {
+ jobByChild.remove(child)?.cancel()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/SafeActivatable.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/SafeActivatable.kt
deleted file mode 100644
index 4dd76f8..0000000
--- a/packages/SystemUI/src/com/android/systemui/lifecycle/SafeActivatable.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.lifecycle
-
-import java.util.concurrent.atomic.AtomicBoolean
-
-/**
- * An [Activatable] that can be concurrently activated by no more than one owner.
- *
- * A previous call to [activate] must be canceled before a new call to [activate] can be made.
- * Trying to call [activate] while already active will fail with an error.
- */
-abstract class SafeActivatable : Activatable {
-
- private val _isActive = AtomicBoolean(false)
-
- var isActive: Boolean
- get() = _isActive.get()
- private set(value) {
- _isActive.set(value)
- }
-
- final override suspend fun activate(): Nothing {
- val allowed = _isActive.compareAndSet(false, true)
- check(allowed) { "Cannot activate an already active activatable!" }
-
- try {
- onActivated()
- } finally {
- isActive = false
- }
- }
-
- /**
- * Notifies that the [Activatable] has been activated.
- *
- * Serves as an entrypoint to kick off coroutine work that the object requires in order to keep
- * its state fresh and/or perform side-effects.
- *
- * The method suspends and doesn't return until all work required by the object is finished. In
- * most cases, it's expected for the work to remain ongoing forever so this method will forever
- * suspend its caller until the coroutine that called it is canceled.
- *
- * Implementations could follow this pattern:
- * ```kotlin
- * override suspend fun onActivated(): Nothing {
- * coroutineScope {
- * launch { ... }
- * launch { ... }
- * launch { ... }
- * }
- * }
- * ```
- *
- * @see activate
- */
- protected abstract suspend fun onActivated(): Nothing
-}
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
index 2edde4a..104b076 100644
--- a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
@@ -23,7 +23,7 @@
import kotlinx.coroutines.launch
/** Base class for all System UI view-models. */
-abstract class SysUiViewModel : SafeActivatable() {
+abstract class SysUiViewModel : BaseActivatable() {
override suspend fun onActivated(): Nothing {
awaitCancellation()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/PackageManagerAdapter.java b/packages/SystemUI/src/com/android/systemui/qs/external/PackageManagerAdapter.java
index 6cf4441..28e4fd0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/PackageManagerAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/PackageManagerAdapter.java
@@ -26,6 +26,8 @@
import android.content.pm.ServiceInfo;
import android.os.RemoteException;
+import androidx.annotation.Nullable;
+
import javax.inject.Inject;
// Adapter that wraps calls to PackageManager or IPackageManager for {@link TileLifecycleManager}.
@@ -45,6 +47,7 @@
mIPackageManager = AppGlobals.getPackageManager();
}
+ @Nullable
public ServiceInfo getServiceInfo(ComponentName className, int flags, int userId)
throws RemoteException {
return mIPackageManager.getServiceInfo(className, flags, userId);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
index 96df728..cbcf68c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
@@ -188,10 +188,10 @@
public boolean isActiveTile() {
try {
ServiceInfo info = mPackageManagerAdapter.getServiceInfo(mIntent.getComponent(),
- META_DATA_QUERY_FLAGS);
- return info.metaData != null
+ META_DATA_QUERY_FLAGS, mUser.getIdentifier());
+ return info != null && info.metaData != null
&& info.metaData.getBoolean(TileService.META_DATA_ACTIVE_TILE, false);
- } catch (PackageManager.NameNotFoundException e) {
+ } catch (RemoteException e) {
return false;
}
}
@@ -206,10 +206,10 @@
public boolean isToggleableTile() {
try {
ServiceInfo info = mPackageManagerAdapter.getServiceInfo(mIntent.getComponent(),
- META_DATA_QUERY_FLAGS);
- return info.metaData != null
+ META_DATA_QUERY_FLAGS, mUser.getIdentifier());
+ return info != null && info.metaData != null
&& info.metaData.getBoolean(TileService.META_DATA_TOGGLEABLE_TILE, false);
- } catch (PackageManager.NameNotFoundException e) {
+ } catch (RemoteException e) {
return false;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepository.kt
index c932cee..0aaea8f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepository.kt
@@ -17,8 +17,8 @@
package com.android.systemui.qs.tiles.impl.custom.data.repository
import android.content.pm.PackageManager
-import android.content.pm.ServiceInfo
import android.graphics.drawable.Icon
+import android.os.RemoteException
import android.os.UserHandle
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
@@ -163,13 +163,14 @@
override suspend fun isTileActive(): Boolean =
withContext(backgroundContext) {
try {
- val info: ServiceInfo =
+ val info =
packageManagerAdapter.getServiceInfo(
tileSpec.componentName,
- META_DATA_QUERY_FLAGS
+ META_DATA_QUERY_FLAGS,
+ getCurrentTileWithUser()?.user?.identifier ?: UserHandle.USER_CURRENT,
)
- info.metaData?.getBoolean(TileService.META_DATA_ACTIVE_TILE, false) == true
- } catch (e: PackageManager.NameNotFoundException) {
+ info?.metaData?.getBoolean(TileService.META_DATA_ACTIVE_TILE, false) == true
+ } catch (e: RemoteException) {
false
}
}
@@ -177,13 +178,14 @@
override suspend fun isTileToggleable(): Boolean =
withContext(backgroundContext) {
try {
- val info: ServiceInfo =
+ val info =
packageManagerAdapter.getServiceInfo(
tileSpec.componentName,
- META_DATA_QUERY_FLAGS
+ META_DATA_QUERY_FLAGS,
+ getCurrentTileWithUser()?.user?.identifier ?: UserHandle.USER_CURRENT
)
- info.metaData?.getBoolean(TileService.META_DATA_TOGGLEABLE_TILE, false) == true
- } catch (e: PackageManager.NameNotFoundException) {
+ info?.metaData?.getBoolean(TileService.META_DATA_TOGGLEABLE_TILE, false) == true
+ } catch (e: RemoteException) {
false
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index e73664d..cc46216 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -152,7 +152,7 @@
hydrateBackStack()
resetShadeSessions()
handleKeyguardEnabledness()
- notifyKeyguardDismissCallbacks()
+ notifyKeyguardDismissCancelledCallbacks()
refreshLockscreenEnabled()
} else {
sceneLogger.logFrameworkEnabled(
@@ -379,8 +379,10 @@
when {
isAlternateBouncerVisible -> {
// When the device becomes unlocked when the alternate bouncer is
- // showing, always hide the alternate bouncer...
+ // showing, always hide the alternate bouncer and notify dismiss
+ // succeeded
alternateBouncerInteractor.hide()
+ dismissCallbackRegistry.notifyDismissSucceeded()
// ... and go to Gone or stay on the current scene
if (
@@ -394,9 +396,11 @@
null
}
}
- isOnPrimaryBouncer ->
+ isOnPrimaryBouncer -> {
// When the device becomes unlocked in primary Bouncer,
+ // notify dismiss succeeded and
// go to previous scene or Gone.
+ dismissCallbackRegistry.notifyDismissSucceeded()
if (
previousScene.value == Scenes.Lockscreen ||
!statusBarStateController.leaveOpenOnKeyguardHide()
@@ -410,6 +414,7 @@
"device was unlocked with primary bouncer showing," +
" from sceneKey=$prevScene"
}
+ }
isOnLockscreen ->
// The lockscreen should be dismissed automatically in 2 scenarios:
// 1. When face auth bypass is enabled and authentication happens while
@@ -468,6 +473,9 @@
applicationScope.launch {
powerInteractor.isAsleep.collect { isAsleep ->
if (isAsleep) {
+ alternateBouncerInteractor.hide()
+ dismissCallbackRegistry.notifyDismissCancelled()
+
switchToScene(
targetSceneKey = Scenes.Lockscreen,
loggingReason = "device is starting to sleep",
@@ -771,15 +779,23 @@
}
}
- private fun notifyKeyguardDismissCallbacks() {
+ private fun notifyKeyguardDismissCancelledCallbacks() {
applicationScope.launch {
- sceneInteractor.currentScene.pairwise().collect { (from, to) ->
- when {
- from != Scenes.Bouncer -> Unit
- to == Scenes.Gone -> dismissCallbackRegistry.notifyDismissSucceeded()
- else -> dismissCallbackRegistry.notifyDismissCancelled()
+ combine(
+ deviceEntryInteractor.isUnlocked,
+ sceneInteractor.currentScene.pairwise(),
+ ) { isUnlocked, (from, to) ->
+ when {
+ from != Scenes.Bouncer -> false
+ to != Scenes.Gone && !isUnlocked -> true
+ else -> false
+ }
}
- }
+ .collect { notifyKeyguardDismissCancelled ->
+ if (notifyKeyguardDismissCancelled) {
+ dismissCallbackRegistry.notifyDismissCancelled()
+ }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 65a59f5..c023b83 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -4536,6 +4536,13 @@
private final class StatusBarStateListener implements StateListener {
@Override
public void onStateChanged(int statusBarState) {
+ onStateChanged(statusBarState, false);
+ }
+
+ private void onStateChanged(
+ int statusBarState,
+ boolean animatingUnlockedShadeToKeyguardBypass
+ ) {
boolean goingToFullShade = mStatusBarStateController.goingToFullShade();
boolean keyguardFadingAway = mKeyguardStateController.isKeyguardFadingAway();
int oldState = mBarState;
@@ -4607,15 +4614,14 @@
// - getting notified again about the current SHADE or KEYGUARD state
final boolean animatingUnlockedShadeToKeyguard = oldState == SHADE
&& statusBarState == KEYGUARD
- && mScreenOffAnimationController.isKeyguardShowDelayed();
+ && mScreenOffAnimationController.isKeyguardShowDelayed()
+ //Bypasses animatingUnlockedShadeToKeyguard for b/337742708
+ && !animatingUnlockedShadeToKeyguardBypass;
if (!animatingUnlockedShadeToKeyguard) {
// Only make the status bar visible if we're not animating the screen off, since
// we only want to be showing the clock/notifications during the animation.
- if (keyguardShowing) {
- mShadeLog.v("Updating keyguard status bar state to visible");
- } else {
- mShadeLog.v("Updating keyguard status bar state to invisible");
- }
+ mShadeLog.logKeyguardStatudBarVisibiliy(keyguardShowing, isOnAod(),
+ animatingUnlockedShadeToKeyguardBypass, oldState, statusBarState);
mKeyguardStatusBarViewController.updateViewState(
/* alpha= */ 1f,
keyguardShowing ? View.VISIBLE : View.INVISIBLE);
@@ -4692,7 +4698,8 @@
.addTagListener(QS.TAG, mQsController.getQsFragmentListener());
if (!SceneContainerFlag.isEnabled()) {
mStatusBarStateController.addCallback(mStatusBarStateListener);
- mStatusBarStateListener.onStateChanged(mStatusBarStateController.getState());
+ // Bypass animatingUnlockedShadeToKeyguard in onStateChanged for b/337742708
+ mStatusBarStateListener.onStateChanged(mStatusBarStateController.getState(), true);
}
mConfigurationController.addCallback(mConfigurationListener);
// Theme might have changed between inflating this view and attaching it to the
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
index 66a310c..f1eaec8 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
@@ -411,4 +411,29 @@
}
)
}
+
+ fun logKeyguardStatudBarVisibiliy(
+ visibility: Boolean,
+ isOnAod: Boolean,
+ animatingUnlockedShadeToKeyguardBypass: Boolean,
+ oldShadeState: Int,
+ newShadeState: Int,
+ ) {
+ buffer.log(
+ TAG,
+ LogLevel.VERBOSE,
+ {
+ bool1 = visibility
+ bool2 = isOnAod
+ bool3 = animatingUnlockedShadeToKeyguardBypass
+ int1 = oldShadeState
+ int2 = newShadeState
+ },
+ {
+ "Setting keyguard status bar visibility to: $bool1, isOnAod: $bool2" +
+ "oldShadeState: $int1, newShadeState: $int2," +
+ "animatingUnlockedShadeToKeyguardBypass: $bool3"
+ }
+ )
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index de4d14d..0f93ff2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -545,6 +545,7 @@
@VisibleForTesting
void consumeFromAlternateBouncerTransitionSteps(TransitionStep step) {
+ SceneContainerFlag.assertInLegacyMode();
hideAlternateBouncer(false);
}
@@ -554,6 +555,7 @@
*/
@VisibleForTesting
void consumeKeyguardAuthenticatedBiometricsHandled(Unit handled) {
+ SceneContainerFlag.assertInLegacyMode();
if (mAlternateBouncerInteractor.isVisibleState()) {
hideAlternateBouncer(false);
}
@@ -981,7 +983,7 @@
} else {
showBouncerOrKeyguard(hideBouncerWhenShowing, isFalsingReset);
}
- if (hideBouncerWhenShowing) {
+ if (!SceneContainerFlag.isEnabled() && hideBouncerWhenShowing) {
hideAlternateBouncer(true);
}
mKeyguardUpdateManager.sendKeyguardReset();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
index 4f7749b..e1dcc52 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
@@ -21,7 +21,11 @@
import android.util.Log
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.android.compose.PlatformButton
@@ -88,7 +92,15 @@
@Composable
private fun ModesDialogContent(dialog: SystemUIDialog) {
AlertDialogContent(
- title = { Text(stringResource(R.string.zen_modes_dialog_title)) },
+ modifier = Modifier.semantics {
+ testTagsAsResourceId = true
+ },
+ title = {
+ Text(
+ modifier = Modifier.testTag("modes_title"),
+ text = stringResource(R.string.zen_modes_dialog_title)
+ )
+ },
content = { ModeTileGrid(viewModel.get()) },
neutralButton = {
PlatformOutlinedButton(onClick = { openSettings(dialog) }) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt
index 3b392c8..3fffd9f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt
@@ -32,6 +32,7 @@
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.android.systemui.common.ui.compose.Icon
@@ -70,12 +71,12 @@
Text(
viewModel.text,
fontWeight = FontWeight.W500,
- modifier = Modifier.tileMarquee()
+ modifier = Modifier.tileMarquee().testTag("name")
)
Text(
viewModel.subtext,
fontWeight = FontWeight.W400,
- modifier = Modifier.tileMarquee()
+ modifier = Modifier.tileMarquee().testTag("state")
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt
index ef9f8ff..ae0061b 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt
@@ -19,7 +19,7 @@
import android.util.IndentingPrintWriter
import com.android.systemui.Dumpable
import com.android.systemui.dump.DumpManager
-import com.android.systemui.lifecycle.SafeActivatable
+import com.android.systemui.lifecycle.BaseActivatable
import com.android.systemui.lifecycle.SysUiViewModel
import com.android.systemui.util.asIndenting
import com.android.systemui.util.printCollection
@@ -189,7 +189,7 @@
) : SimpleFlowDumper(), ActivatableFlowDumper {
private val registration =
- object : SafeActivatable() {
+ object : BaseActivatable() {
override suspend fun onActivated(): Nothing {
try {
dumpManager.registerCriticalDumpable(
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
index 45799b2..7385b82 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
@@ -115,7 +115,6 @@
private final Executor mSysuiUiBgExecutor;
private final Bubbles.SysuiProxy mSysuiProxy;
- // TODO (b/145659174): allow for multiple callbacks to support the "shadow" new notif pipeline
private final List<NotifCallback> mCallbacks = new ArrayList<>();
private final StatusBarWindowCallback mStatusBarWindowCallback;
private final Runnable mSensitiveStateChangedListener;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
index 32d059b..a0fe538b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
@@ -22,6 +22,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.flags.EnableSceneContainer
@@ -29,6 +30,10 @@
import com.android.systemui.keyguard.shared.model.DismissAction
import com.android.systemui.keyguard.shared.model.KeyguardDone
import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.data.repository.fakePowerRepository
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.power.shared.model.WakeSleepReason
+import com.android.systemui.power.shared.model.WakefulnessState
import com.android.systemui.scene.data.repository.Idle
import com.android.systemui.scene.data.repository.Transition
import com.android.systemui.scene.data.repository.setSceneTransition
@@ -82,6 +87,8 @@
deviceEntryInteractor = kosmos.deviceEntryInteractor,
quickSettingsSceneFamilyResolver = kosmos.quickSettingsSceneFamilyResolver,
notifShadeSceneFamilyResolver = kosmos.notifShadeSceneFamilyResolver,
+ powerInteractor = kosmos.powerInteractor,
+ alternateBouncerInteractor = kosmos.alternateBouncerInteractor,
)
}
@@ -234,6 +241,32 @@
}
@Test
+ fun resetDismissAction_onBouncer_OnAsleep() =
+ testScope.runTest {
+ kosmos.setSceneTransition(Idle(Scenes.Bouncer))
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.None
+ )
+ val resetDismissAction by collectLastValue(underTest.resetDismissAction)
+ keyguardRepository.setDismissAction(
+ DismissAction.RunAfterKeyguardGone(
+ dismissAction = {},
+ onCancelAction = {},
+ message = "message",
+ willAnimateOnLockscreen = true,
+ )
+ )
+ assertThat(resetDismissAction).isNull()
+ kosmos.fakePowerRepository.updateWakefulness(
+ rawState = WakefulnessState.ASLEEP,
+ lastWakeReason = WakeSleepReason.POWER_BUTTON,
+ lastSleepReason = WakeSleepReason.TIMEOUT,
+ powerButtonLaunchGestureTriggered = false,
+ )
+ assertThat(resetDismissAction).isEqualTo(Unit)
+ }
+
+ @Test
fun setDismissAction_callsCancelRunnableOnPreviousDismissAction() =
testScope.runTest {
val dismissAction by collectLastValue(underTest.dismissAction)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
index 68307b1..c1cf91d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
@@ -23,6 +23,8 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.systemui.Flags.FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX;
+import static com.google.common.truth.Truth.assertThat;
+
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
@@ -50,6 +52,7 @@
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.net.Uri;
import android.os.Bundle;
@@ -183,6 +186,40 @@
.thenReturn(defaultPackageInfo);
}
+ private void setPackageInstalledForUser(
+ boolean installed,
+ boolean active,
+ boolean toggleable,
+ int user
+ ) throws Exception {
+ ServiceInfo defaultServiceInfo = null;
+ if (installed) {
+ defaultServiceInfo = new ServiceInfo();
+ defaultServiceInfo.metaData = new Bundle();
+ defaultServiceInfo.metaData.putBoolean(TileService.META_DATA_ACTIVE_TILE, active);
+ defaultServiceInfo.metaData
+ .putBoolean(TileService.META_DATA_TOGGLEABLE_TILE, toggleable);
+ when(mMockPackageManagerAdapter.getServiceInfo(any(), anyInt(), eq(user)))
+ .thenReturn(defaultServiceInfo);
+ if (user == 0) {
+ when(mMockPackageManagerAdapter.getServiceInfo(any(), anyInt()))
+ .thenReturn(defaultServiceInfo);
+ }
+ PackageInfo defaultPackageInfo = new PackageInfo();
+ when(mMockPackageManagerAdapter.getPackageInfoAsUser(anyString(), anyInt(), eq(user)))
+ .thenReturn(defaultPackageInfo);
+ } else {
+ when(mMockPackageManagerAdapter.getServiceInfo(any(), anyInt(), eq(user)))
+ .thenReturn(null);
+ if (user == 0) {
+ when(mMockPackageManagerAdapter.getServiceInfo(any(), anyInt()))
+ .thenThrow(new PackageManager.NameNotFoundException());
+ }
+ when(mMockPackageManagerAdapter.getPackageInfoAsUser(anyString(), anyInt(), eq(user)))
+ .thenThrow(new PackageManager.NameNotFoundException());
+ }
+ }
+
private void verifyBind(int times) {
assertEquals(times > 0, mContext.isBound(mTileServiceComponentName));
}
@@ -557,6 +594,100 @@
verify(mockContext).unbindService(captor.getValue());
}
+ @Test
+ public void testIsActive_user0_packageInstalled() throws Exception {
+ setPackageInstalledForUser(true, true, false, 0);
+ mUser = UserHandle.of(0);
+
+ TileLifecycleManager manager = new TileLifecycleManager(mHandler, mWrappedContext,
+ mock(IQSService.class),
+ mMockPackageManagerAdapter,
+ mMockBroadcastDispatcher,
+ mTileServiceIntent,
+ mUser,
+ mActivityManager,
+ mDeviceIdleController,
+ mExecutor);
+
+ assertThat(manager.isActiveTile()).isTrue();
+ }
+
+ @Test
+ public void testIsActive_user10_packageInstalled_notForUser0() throws Exception {
+ setPackageInstalledForUser(true, true, false, 10);
+ setPackageInstalledForUser(false, false, false, 0);
+ mUser = UserHandle.of(10);
+
+ TileLifecycleManager manager = new TileLifecycleManager(mHandler, mWrappedContext,
+ mock(IQSService.class),
+ mMockPackageManagerAdapter,
+ mMockBroadcastDispatcher,
+ mTileServiceIntent,
+ mUser,
+ mActivityManager,
+ mDeviceIdleController,
+ mExecutor);
+
+ assertThat(manager.isActiveTile()).isTrue();
+ }
+
+ @Test
+ public void testIsToggleable_user0_packageInstalled() throws Exception {
+ setPackageInstalledForUser(true, false, true, 0);
+ mUser = UserHandle.of(0);
+
+ TileLifecycleManager manager = new TileLifecycleManager(mHandler, mWrappedContext,
+ mock(IQSService.class),
+ mMockPackageManagerAdapter,
+ mMockBroadcastDispatcher,
+ mTileServiceIntent,
+ mUser,
+ mActivityManager,
+ mDeviceIdleController,
+ mExecutor);
+
+ assertThat(manager.isToggleableTile()).isTrue();
+ }
+
+ @Test
+ public void testIsToggleable_user10_packageInstalled_notForUser0() throws Exception {
+ setPackageInstalledForUser(true, false, true, 10);
+ setPackageInstalledForUser(false, false, false, 0);
+ mUser = UserHandle.of(10);
+
+ TileLifecycleManager manager = new TileLifecycleManager(mHandler, mWrappedContext,
+ mock(IQSService.class),
+ mMockPackageManagerAdapter,
+ mMockBroadcastDispatcher,
+ mTileServiceIntent,
+ mUser,
+ mActivityManager,
+ mDeviceIdleController,
+ mExecutor);
+
+ assertThat(manager.isToggleableTile()).isTrue();
+ }
+
+ @Test
+ public void testIsToggleableActive_installedForDifferentUser() throws Exception {
+ setPackageInstalledForUser(true, false, false, 10);
+ setPackageInstalledForUser(false, true, true, 0);
+ mUser = UserHandle.of(10);
+
+ TileLifecycleManager manager = new TileLifecycleManager(mHandler, mWrappedContext,
+ mock(IQSService.class),
+ mMockPackageManagerAdapter,
+ mMockBroadcastDispatcher,
+ mTileServiceIntent,
+ mUser,
+ mActivityManager,
+ mDeviceIdleController,
+ mExecutor);
+
+ assertThat(manager.isToggleableTile()).isFalse();
+ assertThat(manager.isActiveTile()).isFalse();
+ }
+
private void mockChangeEnabled(long changeId, boolean enabled) {
doReturn(enabled).when(() -> CompatChanges.isChangeEnabled(eq(changeId), anyString(),
any(UserHandle.class)));
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt
index 957f092..27eadb1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt
@@ -16,10 +16,12 @@
package com.android.systemui.keyguard.domain.interactor
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.keyguard.data.repository.keyguardRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.domain.resolver.notifShadeSceneFamilyResolver
import com.android.systemui.scene.domain.resolver.quickSettingsSceneFamilyResolver
@@ -37,5 +39,7 @@
deviceEntryInteractor = deviceEntryInteractor,
quickSettingsSceneFamilyResolver = quickSettingsSceneFamilyResolver,
notifShadeSceneFamilyResolver = notifShadeSceneFamilyResolver,
+ powerInteractor = powerInteractor,
+ alternateBouncerInteractor = alternateBouncerInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeActivatable.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeActivatable.kt
index bcc7393..4c05939 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeActivatable.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeActivatable.kt
@@ -21,7 +21,7 @@
class FakeActivatable(
private val onActivation: () -> Unit = {},
private val onDeactivation: () -> Unit = {},
-) : SafeActivatable() {
+) : BaseActivatable() {
var activationCount = 0
var cancellationCount = 0
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt
index fa8d363..5ac7c39 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt
@@ -18,14 +18,19 @@
import android.content.ComponentName
import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.os.Bundle
+import android.os.UserHandle
import com.android.systemui.qs.external.PackageManagerAdapter
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
+import org.hamcrest.BaseMatcher
+import org.hamcrest.Description
import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.hamcrest.MockitoHamcrest.intThat
/**
* Facade for [PackageManagerAdapter] to provide a fake-like behaviour. You can create this class
@@ -36,29 +41,26 @@
* [com.android.systemui.qs.external.TileServiceManager.isToggleableTile] or
* [com.android.systemui.qs.external.TileServiceManager.isActiveTile] when the real objects are
* used.
+ *
+ * The user this is set up must be a real user (`user >= 0`) or [UserHandle.USER_ALL].
*/
class FakePackageManagerAdapterFacade(
val componentName: ComponentName,
val packageManagerAdapter: PackageManagerAdapter = mock {},
+ user: Int = UserHandle.USER_ALL,
) {
private var isToggleable: Boolean = false
private var isActive: Boolean = false
init {
- whenever(packageManagerAdapter.getServiceInfo(eq(componentName), any())).thenAnswer {
- createServiceInfo()
+ if (user == UserHandle.USER_ALL) {
+ setForAllUsers()
+ } else if (user >= 0) {
+ setExclusiveForUser(user)
+ } else {
+ throw IllegalArgumentException("User must be a real user or UserHandle.USER_ALL")
}
- whenever(
- packageManagerAdapter.getPackageInfoAsUser(
- eq(componentName.packageName),
- anyInt(),
- anyInt()
- )
- )
- .thenAnswer { PackageInfo().apply { packageName = componentName.packageName } }
- whenever(packageManagerAdapter.getServiceInfo(eq(componentName), anyInt(), anyInt()))
- .thenAnswer { createServiceInfo() }
}
private fun createServiceInfo(): ServiceInfo {
@@ -84,4 +86,67 @@
fun setIsToggleable(isToggleable: Boolean) {
this.isToggleable = isToggleable
}
+
+ fun setExclusiveForUser(newUser: Int) {
+ check(newUser >= 0)
+ val notEqualMatcher = NotEqualMatcher(newUser)
+ if (newUser == 0) {
+ whenever(packageManagerAdapter.getServiceInfo(eq(componentName), any())).thenAnswer {
+ createServiceInfo()
+ }
+ }
+ whenever(
+ packageManagerAdapter.getPackageInfoAsUser(
+ eq(componentName.packageName),
+ anyInt(),
+ eq(newUser)
+ )
+ )
+ .thenAnswer { PackageInfo().apply { packageName = componentName.packageName } }
+ whenever(
+ packageManagerAdapter.getPackageInfoAsUser(
+ eq(componentName.packageName),
+ anyInt(),
+ intThat(notEqualMatcher),
+ )
+ )
+ .thenThrow(PackageManager.NameNotFoundException())
+
+ whenever(
+ packageManagerAdapter.getServiceInfo(
+ eq(componentName),
+ anyInt(),
+ intThat(notEqualMatcher)
+ )
+ )
+ .thenAnswer { null }
+ whenever(packageManagerAdapter.getServiceInfo(eq(componentName), anyInt(), eq(newUser)))
+ .thenAnswer { createServiceInfo() }
+ }
+
+ fun setForAllUsers() {
+ whenever(packageManagerAdapter.getServiceInfo(eq(componentName), any())).thenAnswer {
+ createServiceInfo()
+ }
+ whenever(
+ packageManagerAdapter.getPackageInfoAsUser(
+ eq(componentName.packageName),
+ anyInt(),
+ anyInt()
+ )
+ )
+ .thenAnswer { PackageInfo().apply { packageName = componentName.packageName } }
+ whenever(packageManagerAdapter.getServiceInfo(eq(componentName), anyInt(), anyInt()))
+ .thenAnswer { createServiceInfo() }
+ }
+}
+
+private class NotEqualMatcher(private val notEqualValue: Int) : BaseMatcher<Int>() {
+ override fun describeTo(description: Description?) {
+ description?.appendText("!= $notEqualValue")
+ }
+
+ override fun matches(item: Any?): Boolean {
+ return (item as? Int)?.equals(notEqualValue)?.not() ?: true
+ }
}
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 6150343..58cd2e4 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -166,6 +166,14 @@
jarjar_rules: ":ravenwood-services-jarjar-rules",
}
+java_device_for_host {
+ name: "ravenwood-junit-impl-for-ravenizer",
+ libs: [
+ "ravenwood-junit-impl",
+ ],
+ visibility: [":__subpackages__"],
+}
+
// Separated out from ravenwood-junit-impl since it needs to compile
// against `module_current`
java_library {
diff --git a/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java b/ravenwood/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
similarity index 97%
rename from ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java
rename to ravenwood/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
index b477117..30abaa2 100644
--- a/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java
+++ b/ravenwood/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.ravenwood;
+package com.android.ravenwoodtest;
import android.platform.test.annotations.IgnoreUnderRavenwood;
import android.platform.test.ravenwood.RavenwoodRule;
diff --git a/ravenwood/resapk_test/test/com/android/ravenwood/resapk_test/RavenwoodResApkTest.java b/ravenwood/resapk_test/test/com/android/ravenwoodtest/resapk_test/RavenwoodResApkTest.java
similarity index 96%
rename from ravenwood/resapk_test/test/com/android/ravenwood/resapk_test/RavenwoodResApkTest.java
rename to ravenwood/resapk_test/test/com/android/ravenwoodtest/resapk_test/RavenwoodResApkTest.java
index 1029ed2..e547114 100644
--- a/ravenwood/resapk_test/test/com/android/ravenwood/resapk_test/RavenwoodResApkTest.java
+++ b/ravenwood/resapk_test/test/com/android/ravenwoodtest/resapk_test/RavenwoodResApkTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.ravenwood.resapk_test;
+package com.android.ravenwoodtest.resapk_test;
import static junit.framework.TestCase.assertTrue;
diff --git a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsConstantsTest.java b/ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsConstantsTest.java
similarity index 99%
rename from ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsConstantsTest.java
rename to ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsConstantsTest.java
index 3332e24..633ed4e 100644
--- a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsConstantsTest.java
+++ b/ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsConstantsTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.ravenwood.runtimetest;
+package com.android.ravenwoodtest.runtimetest;
// Copied from libcore/luni/src/test/java/libcore/android/system/OsConstantsTest.java
diff --git a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java b/ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsTest.java
similarity index 99%
rename from ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java
rename to ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsTest.java
index 05275b2..c2230c7 100644
--- a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java
+++ b/ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.ravenwood.runtimetest;
+package com.android.ravenwoodtest.runtimetest;
import static android.system.OsConstants.S_ISBLK;
import static android.system.OsConstants.S_ISCHR;
diff --git a/ravenwood/scripts/shrink-systemui-test b/ravenwood/scripts/shrink-systemui-test
new file mode 100755
index 0000000..8589c1d
--- /dev/null
+++ b/ravenwood/scripts/shrink-systemui-test
@@ -0,0 +1,131 @@
+#!/bin/bash
+# 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.
+
+set -e
+
+SCRIPT_NAME="${0##*/}"
+
+usage() {
+ cat <<"EOF"
+
+$SCRIPT_NAME: Shrink / unshrink SystemUiRavenTests.
+
+ SystemUiRavenTests has a lot of kotlin source files, so it's slow to build,
+ which is painful when you want to run it after updating ravenwood code
+ that SystemUiRavenTests depends on. (example: junit-src/)
+
+ This script basically removes the test files in SystemUI/multivalentTests
+ that don't have @EnabledOnRavenwood. But if we actaully remove them,
+ soong would re-generate the ninja file, which will take a long time,
+ so instead it'll truncate them.
+
+ This script will also tell git to ignore these files, so they won't shw up
+ in `git status`.
+ (Use `git ls-files -v | sed -ne "s/^[a-zS] //p"` to show ignored filse.)
+
+Usage:
+ $SCRIPT_NAME -s # Shrink the test files.
+
+ $SCRIPT_NAME -u # Undo it.
+
+EOF
+}
+
+TEST_PATH=${ANDROID_BUILD_TOP}/frameworks/base/packages/SystemUI/multivalentTests
+cd "$TEST_PATH"
+
+command=""
+case "$1" in
+ "-s") command=shrink ;;
+ "-u") command=unshrink ;;
+ *) usage ; exit 1 ;;
+esac
+
+
+echo "Listing test files...."
+files=( $(find . -name '*Test.kt' -o -name '*Test.java') )
+
+exemption='(BaseHeadsUpManagerTest)'
+
+shrink() {
+ local target=()
+ for file in ${files[@]}; do
+ # Check for exemption
+ if echo $file | egrep -q "$exemption"; then
+ echo " Skip exempted file"
+ continue
+ fi
+
+ echo "Checking $file"
+ if ! [[ -f $file ]] ; then
+ echo " Skip non regular file"
+ continue
+ fi
+
+ if ! [[ -s $file ]] ; then
+ echo " Skip empty file"
+ continue
+ fi
+
+ if grep -q '@EnabledOnRavenwood' $file ; then
+ echo " Skip ravenwood test file".
+ continue
+ fi
+
+ # It's a non ravenwood test file. Empty it.
+ : > $file
+
+ # Tell git to ignore the file
+
+ target+=($file)
+
+ echo " Emptied"
+
+ done
+ if (( ${#target[@]} == 0 )) ; then
+ echo "No files emptied."
+ return 0
+ fi
+
+ git update-index --skip-worktree ${target[@]}
+
+ echo "Emptied ${#target[@]} files"
+ return 0
+}
+
+unshrink() {
+ local target=()
+
+ # Collect empty files
+ for file in ${files[@]}; do
+ if [[ -s $file ]] ; then
+ continue
+ fi
+
+ target+=($file)
+ : > $file
+ done
+ if (( ${#target[@]} == 0 )) ; then
+ echo "No files to restore."
+ return 0
+ fi
+ # Un-ignore the files, and check out the original files
+ echo "Restoring ${#target[@]} files..."
+ git update-index --no-skip-worktree ${target[@]}
+ git checkout goog/main ${target[@]}
+ return 0
+}
+
+$command
diff --git a/ravenwood/tools/ravenizer-fake/Android.bp b/ravenwood/tools/ravenizer-fake/Android.bp
deleted file mode 100644
index 7e2c407..0000000
--- a/ravenwood/tools/ravenizer-fake/Android.bp
+++ /dev/null
@@ -1,14 +0,0 @@
-package {
- // See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "frameworks_base_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["frameworks_base_license"],
-}
-
-sh_binary_host {
- name: "ravenizer",
- src: "ravenizer",
- visibility: ["//visibility:public"],
-}
diff --git a/ravenwood/tools/ravenizer-fake/ravenizer b/ravenwood/tools/ravenizer-fake/ravenizer
deleted file mode 100755
index 84b3c8e..0000000
--- a/ravenwood/tools/ravenizer-fake/ravenizer
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/bin/bash
-# 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.
-
-# "Fake" ravenizer, which just copies the file.
-# We need it to add ravenizer support to Soong on AOSP,
-# when the actual ravenizer is not in AOSP yet.
-
-invalid_arg() {
- echo "Ravenizer(fake): invalid args" 1>&2
- exit 1
-}
-
-(( $# >= 4 )) || invalid_arg
-[[ "$1" == "--in-jar" ]] || invalid_arg
-[[ "$3" == "--out-jar" ]] || invalid_arg
-
-echo "Ravenizer(fake): copiyng $2 to $4"
-
-cp "$2" "$4"
diff --git a/ravenwood/tools/ravenizer/Android.bp b/ravenwood/tools/ravenizer/Android.bp
new file mode 100644
index 0000000..2892d07
--- /dev/null
+++ b/ravenwood/tools/ravenizer/Android.bp
@@ -0,0 +1,25 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_binary_host {
+ name: "ravenizer",
+ main_class: "com.android.platform.test.ravenwood.ravenizer.RavenizerMain",
+ srcs: ["src/**/*.kt"],
+ static_libs: [
+ "hoststubgen-lib",
+ "ow2-asm",
+ "ow2-asm-analysis",
+ "ow2-asm-commons",
+ "ow2-asm-tree",
+ "ow2-asm-util",
+ "junit",
+ "ravenwood-junit-impl-for-ravenizer",
+ ],
+ 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
new file mode 100644
index 0000000..da9c7d9
--- /dev/null
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
@@ -0,0 +1,212 @@
+/*
+ * 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.platform.test.ravenwood.ravenizer
+
+import com.android.hoststubgen.GeneralUserErrorException
+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.TestRunnerRewritingAdapter
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.ClassVisitor
+import org.objectweb.asm.ClassWriter
+import org.objectweb.asm.util.CheckClassAdapter
+import java.io.BufferedInputStream
+import java.io.BufferedOutputStream
+import java.io.FileOutputStream
+import java.io.InputStream
+import java.io.OutputStream
+import java.util.zip.ZipEntry
+import java.util.zip.ZipFile
+import java.util.zip.ZipOutputStream
+
+/**
+ * Various stats on Ravenizer.
+ */
+data class RavenizerStats(
+ /** Total end-to-end time. */
+ var totalTime: Double = .0,
+
+ /** Time took to build [ClasNodes] */
+ var loadStructureTime: Double = .0,
+
+ /** 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 copying class files without modification. */
+ var totalCopyTime: Double = .0,
+
+ /** # of entries in the input jar file */
+ var totalEntiries: Int = 0,
+
+ /** # of *.class files in the input jar file */
+ var totalClasses: Int = 0,
+
+ /** # of *.class files that have been processed. */
+ var processedClasses: Int = 0,
+) {
+ override fun toString(): String {
+ return """
+ RavenizerStats{
+ totalTime=$totalTime,
+ loadStructureTime=$loadStructureTime,
+ totalProcessTime=$totalProcessTime,
+ totalConversionTime=$totalConversionTime,
+ totalCopyTime=$totalCopyTime,
+ totalEntiries=$totalEntiries,
+ totalClasses=$totalClasses,
+ processedClasses=$processedClasses,
+ }
+ """.trimIndent()
+ }
+}
+
+/**
+ * Main class.
+ */
+class Ravenizer(val options: RavenizerOptions) {
+ fun run() {
+ val stats = RavenizerStats()
+ stats.totalTime = log.nTime {
+ process(options.inJar.get, options.outJar.get, stats)
+ }
+ log.i(stats.toString())
+ }
+
+ private fun process(inJar: String, outJar: String, stats: RavenizerStats) {
+ var allClasses = ClassNodes.loadClassStructures(inJar) {
+ time -> stats.loadStructureTime = time
+ }
+
+ stats.totalProcessTime = log.iTime("$executableName processing $inJar") {
+ ZipFile(inJar).use { inZip ->
+ val inEntries = inZip.entries()
+
+ stats.totalEntiries = inZip.size()
+
+ ZipOutputStream(BufferedOutputStream(FileOutputStream(outJar))).use { outZip ->
+ while (inEntries.hasMoreElements()) {
+ val entry = inEntries.nextElement()
+
+ if (entry.name.endsWith(".dex")) {
+ // Seems like it's an ART jar file. We can't process it.
+ // It's a fatal error.
+ throw GeneralUserErrorException(
+ "$inJar is not a desktop jar file. It contains a *.dex file."
+ )
+ }
+
+ val className = zipEntryNameToClassName(entry.name)
+
+ if (className != null) {
+ stats.totalClasses += 1
+ }
+
+ if (className != null && shouldProcessClass(allClasses, className)) {
+ stats.processedClasses += 1
+ processSingleClass(inZip, entry, outZip, allClasses, stats)
+ } else {
+ // Too slow, let's use merge_zips to bring back the original classes.
+ copyZipEntry(inZip, entry, outZip, stats)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Copy a single ZIP entry to the output.
+ */
+ private fun copyZipEntry(
+ inZip: ZipFile,
+ entry: ZipEntry,
+ out: ZipOutputStream,
+ stats: RavenizerStats,
+ ) {
+ stats.totalCopyTime += log.nTime {
+ inZip.getInputStream(entry).use { ins ->
+ // Copy unknown entries as is to the impl out. (but not to the stub out.)
+ val outEntry = ZipEntry(entry.name)
+ outEntry.method = 0
+ outEntry.size = entry.size
+ outEntry.crc = entry.crc
+ out.putNextEntry(outEntry)
+
+ ins.transferTo(out)
+
+ out.closeEntry()
+ }
+ }
+ }
+
+ private fun processSingleClass(
+ inZip: ZipFile,
+ entry: ZipEntry,
+ outZip: ZipOutputStream,
+ allClasses: ClassNodes,
+ stats: RavenizerStats,
+ ) {
+ val newEntry = ZipEntry(entry.name)
+ outZip.putNextEntry(newEntry)
+
+ BufferedInputStream(inZip.getInputStream(entry)).use { bis ->
+ processSingleClass(entry, bis, outZip, allClasses, stats)
+ }
+ outZip.closeEntry()
+ }
+
+ /**
+ * Whether a class needs to be processed. This must be kept in sync with [processSingleClass].
+ */
+ private fun shouldProcessClass(classes: ClassNodes, classInternalName: String): Boolean {
+ return TestRunnerRewritingAdapter.shouldProcess(classes, classInternalName)
+ }
+
+ private fun processSingleClass(
+ entry: ZipEntry,
+ input: InputStream,
+ output: OutputStream,
+ allClasses: ClassNodes,
+ stats: RavenizerStats,
+ ) {
+ val cr = ClassReader(input)
+
+ lateinit var data: ByteArray
+ stats.totalConversionTime += log.vTime("Modify ${entry.name}") {
+ val flags = ClassWriter.COMPUTE_MAXS
+ val cw = ClassWriter(flags)
+ var outVisitor: ClassVisitor = cw
+
+ val enableChecker = false
+ if (enableChecker) {
+ outVisitor = CheckClassAdapter(outVisitor)
+ }
+
+ // This must be kept in sync with shouldProcessClass.
+ outVisitor = TestRunnerRewritingAdapter(allClasses, outVisitor)
+
+ cr.accept(outVisitor, ClassReader.EXPAND_FRAMES)
+
+ data = cw.toByteArray()
+ }
+ output.write(data)
+ }
+}
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
new file mode 100644
index 0000000..ff41818
--- /dev/null
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+@file:JvmName("RavenizerMain")
+
+package com.android.platform.test.ravenwood.ravenizer
+
+import com.android.hoststubgen.LogLevel
+import com.android.hoststubgen.executableName
+import com.android.hoststubgen.log
+import com.android.hoststubgen.runMainWithBoilerplate
+
+/**
+ * Entry point.
+ */
+fun main(args: Array<String>) {
+ executableName = "Ravenizer"
+ log.setConsoleLogLevel(LogLevel.Info)
+
+ runMainWithBoilerplate {
+ val options = RavenizerOptions.parseArgs(args)
+
+ log.i("$executableName started")
+ log.v("Options: $options")
+
+ // Run.
+ Ravenizer(options).run()
+ }
+}
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
new file mode 100644
index 0000000..e85e3be
--- /dev/null
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.platform.test.ravenwood.ravenizer
+
+import com.android.hoststubgen.ArgIterator
+import com.android.hoststubgen.ArgumentsException
+import com.android.hoststubgen.SetOnce
+import com.android.hoststubgen.ensureFileExists
+import com.android.hoststubgen.log
+
+class RavenizerOptions(
+ /** Input jar file*/
+ var inJar: SetOnce<String> = SetOnce(""),
+
+ /** Output jar file */
+ var outJar: SetOnce<String> = SetOnce(""),
+) {
+ companion object {
+ fun parseArgs(args: Array<String>): RavenizerOptions {
+ val ret = RavenizerOptions()
+ val ai = ArgIterator.withAtFiles(args)
+
+ while (true) {
+ val arg = ai.nextArgOptional()
+ if (arg == null) {
+ break
+ }
+
+ fun nextArg(): String = ai.nextArgRequired(arg)
+
+ if (log.maybeHandleCommandLineArg(arg) { nextArg() }) {
+ continue
+ }
+ try {
+ when (arg) {
+ // TODO: Write help
+ "-h", "--help" -> TODO("Help is not implemented yet")
+
+ "--in-jar" -> ret.inJar.set(nextArg()).ensureFileExists()
+ "--out-jar" -> ret.outJar.set(nextArg())
+
+ else -> throw ArgumentsException("Unknown option: $arg")
+ }
+ } catch (e: SetOnce.SetMoreThanOnceException) {
+ throw ArgumentsException("Duplicate or conflicting argument found: $arg")
+ }
+ }
+
+ if (!ret.inJar.isSet) {
+ throw ArgumentsException("Required option missing: --in-jar")
+ }
+ if (!ret.outJar.isSet) {
+ throw ArgumentsException("Required option missing: --out-jar")
+ }
+ return ret
+ }
+ }
+
+ override fun toString(): String {
+ return """
+ RavenizerOptions{
+ inJar=$inJar,
+ outJar=$outJar,
+ }
+ """.trimIndent()
+ }
+}
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
new file mode 100644
index 0000000..0018648
--- /dev/null
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.platform.test.ravenwood.ravenizer
+
+import com.android.hoststubgen.asm.ClassNodes
+import com.android.hoststubgen.asm.findAnyAnnotation
+import org.objectweb.asm.Type
+
+val junitTestMethodType = Type.getType(org.junit.Test::class.java)
+val junitRunWithType = Type.getType(org.junit.runner.RunWith::class.java)
+
+val junitTestMethodDescriptor = junitTestMethodType.descriptor
+val junitRunWithDescriptor = junitRunWithType.descriptor
+
+val junitTestMethodDescriptors = setOf<String>(junitTestMethodDescriptor)
+val junitRunWithDescriptors = setOf<String>(junitRunWithDescriptor)
+
+/**
+ * Returns true, if a test looks like it's a test class which needs to be processed.
+ */
+fun isTestLookingClass(classes: ClassNodes, className: String): Boolean {
+ // Similar to com.android.tradefed.lite.HostUtils.testLoadClass(), except it's more lenient,
+ // and accept non-public and/or abstract classes.
+ // HostUtils also checks "Suppress" or "SuiteClasses" but this one doesn't.
+ // TODO: SuiteClasses may need to be supported.
+
+ val cn = classes.findClass(className) ?: return false
+
+ if (cn.findAnyAnnotation(junitRunWithDescriptors) != null) {
+ return true
+ }
+ cn.methods?.forEach { method ->
+ if (method.findAnyAnnotation(junitTestMethodDescriptors) != null) {
+ return true
+ }
+ }
+ if (cn.superName == null) {
+ return false
+ }
+ return isTestLookingClass(classes, cn.superName)
+}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/TestRunnerRewritingAdapter.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/TestRunnerRewritingAdapter.kt
new file mode 100644
index 0000000..c539908
--- /dev/null
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/TestRunnerRewritingAdapter.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.platform.test.ravenwood.ravenizer.adapter
+
+import com.android.hoststubgen.asm.ClassNodes
+import com.android.hoststubgen.visitors.OPCODE_VERSION
+import com.android.platform.test.ravenwood.ravenizer.isTestLookingClass
+import org.objectweb.asm.ClassVisitor
+
+/**
+ * Class visitor to rewrite the test runner for Ravenwood
+ *
+ * TODO: Implement it.
+ */
+class TestRunnerRewritingAdapter(
+ protected val classes: ClassNodes,
+ nextVisitor: ClassVisitor,
+) : ClassVisitor(OPCODE_VERSION, nextVisitor) {
+ companion object {
+ /**
+ * Returns true if a target class is interesting to this adapter.
+ */
+ fun shouldProcess(classes: ClassNodes, className: String): Boolean {
+ return isTestLookingClass(classes, className)
+ }
+ }
+}
diff --git a/services/accessibility/Android.bp b/services/accessibility/Android.bp
index efa1397..3d7ad0b 100644
--- a/services/accessibility/Android.bp
+++ b/services/accessibility/Android.bp
@@ -35,7 +35,7 @@
"androidx.annotation_annotation",
],
static_libs: [
- "a11ychecker-protos-java-proto-lite",
+ "accessibility_protos_lite",
"com_android_server_accessibility_flags_lib",
"//frameworks/base/packages/SystemUI/aconfig:com_android_systemui_flags_lib",
],
@@ -71,17 +71,6 @@
aconfig_declarations: "com_android_server_accessibility_flags",
}
-java_library_static {
- name: "a11ychecker-protos-java-proto-lite",
- proto: {
- type: "lite",
- canonical_path_from_root: false,
- },
- srcs: [
- "java/**/a11ychecker/proto/*.proto",
- ],
-}
-
genrule {
name: "statslog-accessibility-java-gen",
tools: ["stats-log-api-gen"],
diff --git a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManager.java b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManager.java
index f7a59a4b..83f57b2 100644
--- a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManager.java
@@ -30,7 +30,6 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.accessibility.Flags;
-import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckResultReported;
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset;
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck;
@@ -58,7 +57,7 @@
private final PackageManager mPackageManager;
private final Set<AccessibilityHierarchyCheck> mHierarchyChecks;
private final ATFHierarchyBuilder mATFHierarchyBuilder;
- private final Set<AccessibilityCheckResultReported> mCachedResults = new HashSet<>();
+ private final Set<AndroidAccessibilityCheckerResult> mCachedResults = new HashSet<>();
@VisibleForTesting
final A11yCheckerTimer mTimer = new A11yCheckerTimer();
@@ -85,14 +84,14 @@
* logging. Returns the check results for the given nodes.
*/
@RequiresPermission(allOf = {android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})
- public Set<AccessibilityCheckResultReported> maybeRunA11yChecker(
+ public Set<AndroidAccessibilityCheckerResult> maybeRunA11yChecker(
List<AccessibilityNodeInfo> nodes, @Nullable String sourceEventClassName,
ComponentName a11yServiceComponentName, @UserIdInt int userId) {
if (!shouldRunA11yChecker()) {
return Set.of();
}
- Set<AccessibilityCheckResultReported> allResults = new HashSet<>();
+ Set<AndroidAccessibilityCheckerResult> allResults = new HashSet<>();
String defaultBrowserName = mPackageManager.getDefaultBrowserPackageNameAsUser(userId);
try {
@@ -104,7 +103,7 @@
continue;
}
List<AccessibilityHierarchyCheckResult> checkResults = runChecksOnNode(nodeInfo);
- Set<AccessibilityCheckResultReported> filteredResults =
+ Set<AndroidAccessibilityCheckerResult> filteredResults =
AccessibilityCheckerUtils.processResults(nodeInfo, checkResults,
sourceEventClassName, mPackageManager, a11yServiceComponentName);
allResults.addAll(filteredResults);
@@ -127,7 +126,7 @@
return checkResults;
}
- public Set<AccessibilityCheckResultReported> getCachedResults() {
+ public Set<AndroidAccessibilityCheckerResult> getCachedResults() {
return Collections.unmodifiableSet(mCachedResults);
}
diff --git a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerStatsdLogger.java b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerStatsdLogger.java
index 1b3ec5a..fa0bb59 100644
--- a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerStatsdLogger.java
+++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerStatsdLogger.java
@@ -16,10 +16,9 @@
package com.android.server.accessibility.a11ychecker;
+import android.text.TextUtils;
import android.util.Slog;
-import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckResultReported;
-
import java.util.Set;
@@ -35,11 +34,11 @@
/**
* Writes results to statsd.
*/
- public static void logResults(Set<AccessibilityCheckResultReported> results) {
- Slog.i(LOG_TAG, String.format("Writing %d AccessibilityCheckResultReported events",
+ public static void logResults(Set<AndroidAccessibilityCheckerResult> results) {
+ Slog.i(LOG_TAG, TextUtils.formatSimple("Writing %d AccessibilityCheckResultReported events",
results.size()));
- for (AccessibilityCheckResultReported result : results) {
+ for (AndroidAccessibilityCheckerResult result : results) {
AccessibilityCheckerStatsLog.write(ATOM_ID,
result.getPackageName(),
result.getAppVersionCode(),
diff --git a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java
index fa0fed2..eb24b02 100644
--- a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java
+++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java
@@ -17,6 +17,8 @@
package com.android.server.accessibility.a11ychecker;
+import android.accessibility.AccessibilityCheckClass;
+import android.accessibility.AccessibilityCheckResultType;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.pm.PackageInfo;
@@ -25,9 +27,6 @@
import android.view.accessibility.AccessibilityNodeInfo;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckClass;
-import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckResultReported;
-import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckResultType;
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult;
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck;
@@ -92,7 +91,7 @@
AccessibilityCheckClass.TRAVERSAL_ORDER_CHECK));
// LINT.ThenChange(/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto)
- static Set<AccessibilityCheckResultReported> processResults(
+ static Set<AndroidAccessibilityCheckerResult> processResults(
AccessibilityNodeInfo nodeInfo,
List<AccessibilityHierarchyCheckResult> checkResults,
@Nullable String activityClassName,
@@ -103,16 +102,16 @@
if (nodePath == null) {
return Set.of();
}
- AccessibilityCheckResultReported.Builder builder;
+ AndroidAccessibilityCheckerResult.Builder commonBuilder;
try {
- builder = AccessibilityCheckResultReported.newBuilder()
+ commonBuilder = AndroidAccessibilityCheckerResult.newBuilder()
.setPackageName(appPackageName)
.setAppVersionCode(getAppVersionCode(packageManager, appPackageName))
.setUiElementPath(nodePath)
.setActivityName(
getActivityName(packageManager, appPackageName, activityClassName))
.setWindowTitle(getWindowTitle(nodeInfo))
- .setSourceComponentName(a11yServiceComponentName.flattenToString())
+ .setSourceComponentName(a11yServiceComponentName)
.setSourceVersionCode(
getAppVersionCode(packageManager,
a11yServiceComponentName.getPackageName()));
@@ -126,7 +125,8 @@
== AccessibilityCheckResult.AccessibilityCheckResultType.ERROR
|| checkResult.getType()
== AccessibilityCheckResult.AccessibilityCheckResultType.WARNING)
- .map(checkResult -> builder.setResultCheckClass(
+ .map(checkResult -> new AndroidAccessibilityCheckerResult.Builder(
+ commonBuilder).setResultCheckClass(
getCheckClass(checkResult)).setResultType(
getCheckResultType(checkResult)).setResultId(
checkResult.getResultId()).build())
@@ -188,9 +188,9 @@
private static AccessibilityCheckResultType getCheckResultType(
AccessibilityHierarchyCheckResult checkResult) {
return switch (checkResult.getType()) {
- case ERROR -> AccessibilityCheckResultType.ERROR;
- case WARNING -> AccessibilityCheckResultType.WARNING;
- default -> AccessibilityCheckResultType.UNKNOWN_RESULT_TYPE;
+ case ERROR -> AccessibilityCheckResultType.ERROR_CHECK_RESULT_TYPE;
+ case WARNING -> AccessibilityCheckResultType.WARNING_CHECK_RESULT_TYPE;
+ default -> AccessibilityCheckResultType.UNKNOWN_CHECK_RESULT_TYPE;
};
}
diff --git a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AndroidAccessibilityCheckerResult.java b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AndroidAccessibilityCheckerResult.java
new file mode 100644
index 0000000..c9cd9fe
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AndroidAccessibilityCheckerResult.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility.a11ychecker;
+
+import android.accessibility.AccessibilityCheckClass;
+import android.accessibility.AccessibilityCheckResultType;
+import android.content.ComponentName;
+import android.text.TextUtils;
+
+public class AndroidAccessibilityCheckerResult implements Cloneable {
+ // Package name of the app containing the checked View.
+ private String mPackageName;
+ // Version code of the app containing the checked View.
+ private long mAppVersionCode;
+ // The path of the View starting from the root element in the window. Each element is
+ // represented by the View's resource id, when available, or the View's class name.
+ private String mUiElementPath;
+ // Class name of the activity containing the checked View.
+ private String mActivityName;
+ // Title of the window containing the checked View.
+ private String mWindowTitle;
+ // The component name of the app running the AccessibilityService which provided the a11y node.
+ private String mSourceComponentName;
+ // Version code of the app running the AccessibilityService that provided the a11y node.
+ private long mSourceVersionCode;
+ // Class Name of the AccessibilityCheck that produced the result.
+ private AccessibilityCheckClass mResultCheckClass;
+ // Result type of the AccessibilityCheckResult.
+ private AccessibilityCheckResultType mResultType;
+ // Result ID of the AccessibilityCheckResult.
+ private int mResultId;
+
+ static final class Builder {
+ private final AndroidAccessibilityCheckerResult mInstance;
+
+ Builder() {
+ mInstance = new AndroidAccessibilityCheckerResult();
+ }
+
+ Builder(Builder otherBuilder) {
+ mInstance = otherBuilder.mInstance.clone();
+ }
+
+ public Builder setPackageName(String packageName) {
+ mInstance.mPackageName = packageName;
+ return this;
+ }
+
+ public Builder setAppVersionCode(long versionCode) {
+ mInstance.mAppVersionCode = versionCode;
+ return this;
+ }
+
+ public Builder setUiElementPath(String uiElementPath) {
+ mInstance.mUiElementPath = uiElementPath;
+ return this;
+ }
+
+ public Builder setActivityName(String activityName) {
+ mInstance.mActivityName = activityName;
+ return this;
+ }
+
+ public Builder setWindowTitle(String windowTitle) {
+ mInstance.mWindowTitle = windowTitle;
+ return this;
+ }
+
+ public Builder setSourceComponentName(ComponentName componentName) {
+ mInstance.mSourceComponentName = componentName.flattenToString();
+ return this;
+ }
+
+ public Builder setSourceVersionCode(long versionCode) {
+ mInstance.mSourceVersionCode = versionCode;
+ return this;
+ }
+
+ public Builder setResultCheckClass(AccessibilityCheckClass checkClass) {
+ mInstance.mResultCheckClass = checkClass;
+ return this;
+ }
+
+ public Builder setResultType(AccessibilityCheckResultType resultType) {
+ mInstance.mResultType = resultType;
+ return this;
+ }
+
+ public Builder setResultId(int resultId) {
+ mInstance.mResultId = resultId;
+ return this;
+ }
+
+ public AndroidAccessibilityCheckerResult build() {
+ // TODO: assert all fields are set, etc
+ return mInstance;
+ }
+ }
+
+ static Builder newBuilder() {
+ return new Builder();
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ public long getAppVersionCode() {
+ return mAppVersionCode;
+ }
+
+ public String getUiElementPath() {
+ return mUiElementPath;
+ }
+
+ public String getActivityName() {
+ return mActivityName;
+ }
+
+ public String getWindowTitle() {
+ return mWindowTitle;
+ }
+
+ public String getSourceComponentName() {
+ return mSourceComponentName;
+ }
+
+ public long getSourceVersionCode() {
+ return mSourceVersionCode;
+ }
+
+ public AccessibilityCheckClass getResultCheckClass() {
+ return mResultCheckClass;
+ }
+
+ public AccessibilityCheckResultType getResultType() {
+ return mResultType;
+ }
+
+ public int getResultId() {
+ return mResultId;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof AndroidAccessibilityCheckerResult)) {
+ return false;
+ }
+ AndroidAccessibilityCheckerResult otherResult = (AndroidAccessibilityCheckerResult) other;
+ return mPackageName.equals(otherResult.mPackageName)
+ && mAppVersionCode == otherResult.mAppVersionCode
+ && mUiElementPath.equals(otherResult.mUiElementPath)
+ && mActivityName.equals(otherResult.mActivityName)
+ && mWindowTitle.equals(otherResult.mWindowTitle)
+ && mSourceComponentName.equals(otherResult.mSourceComponentName)
+ && mSourceVersionCode == otherResult.mSourceVersionCode
+ && mResultCheckClass.equals(otherResult.mResultCheckClass)
+ && mResultType.equals(otherResult.mResultType)
+ && mResultId == otherResult.mResultId;
+ }
+
+ @Override
+ public int hashCode() {
+ return toString().hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return TextUtils.formatSimple("%s:%d:%s:%s:%s:%s:%d:%s:%s:%d", mPackageName,
+ mAppVersionCode, mUiElementPath, mActivityName, mWindowTitle, mSourceComponentName,
+ mSourceVersionCode, mResultCheckClass.name(), mResultType.name(), mResultId);
+ }
+
+ @Override
+ public AndroidAccessibilityCheckerResult clone() {
+ try {
+ return (AndroidAccessibilityCheckerResult) super.clone();
+ } catch (CloneNotSupportedException e) {
+ return null;
+ }
+ }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto b/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto
deleted file mode 100644
index 8beed4a..0000000
--- a/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto
+++ /dev/null
@@ -1,73 +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.
- */
-syntax = "proto2";
-package android.accessibility;
-
-option java_package = "com.android.server.accessibility.a11ychecker";
-option java_outer_classname = "A11yCheckerProto";
-
-// TODO(b/326385939): remove and replace usage with the atom extension proto, when submitted.
-/** Logs the result of an AccessibilityCheck. */
-message AccessibilityCheckResultReported {
- // Package name of the app containing the checked View.
- optional string package_name = 1;
- // Version code of the app containing the checked View.
- optional int64 app_version_code = 2;
- // The path of the View starting from the root element in the window. Each element is
- // represented by the View's resource id, when available, or the View's class name.
- optional string ui_element_path = 3;
- // Class name of the activity containing the checked View.
- optional string activity_name = 4;
- // Title of the window containing the checked View.
- optional string window_title = 5;
- // The flattened component name of the app running the AccessibilityService which provided the a11y node.
- optional string source_component_name = 6;
- // Version code of the app running the AccessibilityService that provided the a11y node.
- optional int64 source_version_code = 7;
- // Class Name of the AccessibilityCheck that produced the result.
- optional AccessibilityCheckClass result_check_class = 8;
- // Result type of the AccessibilityCheckResult.
- optional AccessibilityCheckResultType result_type = 9;
- // Result ID of the AccessibilityCheckResult.
- optional int32 result_id = 10;
-}
-
-/** The AccessibilityCheck class. */
-// LINT.IfChange
-enum AccessibilityCheckClass {
- UNKNOWN_CHECK = 0;
- CLASS_NAME_CHECK = 1;
- CLICKABLE_SPAN_CHECK = 2;
- DUPLICATE_CLICKABLE_BOUNDS_CHECK = 3;
- DUPLICATE_SPEAKABLE_TEXT_CHECK = 4;
- EDITABLE_CONTENT_DESC_CHECK = 5;
- IMAGE_CONTRAST_CHECK = 6;
- LINK_PURPOSE_UNCLEAR_CHECK = 7;
- REDUNDANT_DESCRIPTION_CHECK = 8;
- SPEAKABLE_TEXT_PRESENT_CHECK = 9;
- TEXT_CONTRAST_CHECK = 10;
- TEXT_SIZE_CHECK = 11;
- TOUCH_TARGET_SIZE_CHECK = 12;
- TRAVERSAL_ORDER_CHECK = 13;
-}
-// LINT.ThenChange(/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java)
-
-/** The type of AccessibilityCheckResult */
-enum AccessibilityCheckResultType {
- UNKNOWN_RESULT_TYPE = 0;
- ERROR = 1;
- WARNING = 2;
-}
diff --git a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
index 44c8d1c..28a0b28 100644
--- a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
+++ b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
@@ -231,7 +231,7 @@
if (!isExternalDisplayAllowed()) {
Slog.w(TAG, "handleExternalDisplayConnectedLocked: External display can not be used"
+ " because it is currently not allowed.");
- mDisplayNotificationManager.onHighTemperatureExternalDisplayNotAllowed();
+ mHandler.post(mDisplayNotificationManager::onHighTemperatureExternalDisplayNotAllowed);
return;
}
@@ -329,7 +329,7 @@
if (!isExternalDisplayAllowed()) {
Slog.w(TAG, "External display is currently not allowed and is getting disabled.");
- mDisplayNotificationManager.onHighTemperatureExternalDisplayNotAllowed();
+ mHandler.post(mDisplayNotificationManager::onHighTemperatureExternalDisplayNotAllowed);
}
mLogicalDisplayMapper.setDisplayEnabledLocked(logicalDisplay, /*enabled=*/ false);
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 154710f..81c30dd 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -1225,11 +1225,15 @@
boolean avrSystemAudioMode = HdmiUtils.parseCommandParamSystemAudioStatus(message);
// Set System Audio Mode according to TV's settings.
// Handle <System Audio Mode Status> here only when
- // SystemAudioAutoInitiationAction timeout
+ // SystemAudioAutoInitiationAction timeout.
+ // If AVR reports SAM on and it is in standby, the action SystemAudioActionFromTv
+ // triggers a <SAM Request> that will wake-up the AVR.
HdmiDeviceInfo avr = getAvrDeviceInfo();
if (avr == null) {
setSystemAudioMode(false);
- } else if (avrSystemAudioMode != tvSystemAudioMode) {
+ } else if (avrSystemAudioMode != tvSystemAudioMode
+ || (avrSystemAudioMode && avr.getDevicePowerStatus()
+ == HdmiControlManager.POWER_STATUS_STANDBY)) {
addAndStartAction(new SystemAudioActionFromTv(this, avr.getLogicalAddress(),
tvSystemAudioMode, null));
} else {
diff --git a/services/core/java/com/android/server/hdmi/SystemAudioAutoInitiationAction.java b/services/core/java/com/android/server/hdmi/SystemAudioAutoInitiationAction.java
index 56e538b..028637b 100644
--- a/services/core/java/com/android/server/hdmi/SystemAudioAutoInitiationAction.java
+++ b/services/core/java/com/android/server/hdmi/SystemAudioAutoInitiationAction.java
@@ -16,7 +16,9 @@
package com.android.server.hdmi;
+import android.hardware.hdmi.HdmiControlManager;
import android.hardware.tv.cec.V1_0.SendMessageResult;
+import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.hdmi.HdmiControlService.SendMessageCallback;
@@ -89,8 +91,13 @@
// If System Audio Control feature is enabled, turn on system audio mode when new AVR is
// detected. Otherwise, turn off system audio mode.
+ // If AVR reports SAM on and it is in standby, the action SystemAudioActionFromTv
+ // triggers a <SAM Request> that will wake-up the AVR.
boolean targetSystemAudioMode = tv().isSystemAudioControlFeatureEnabled();
- if (currentSystemAudioMode != targetSystemAudioMode) {
+ if (currentSystemAudioMode != targetSystemAudioMode
+ || (currentSystemAudioMode && tv().getAvrDeviceInfo() != null
+ && tv().getAvrDeviceInfo().getDevicePowerStatus()
+ == HdmiControlManager.POWER_STATUS_STANDBY)) {
// Start System Audio Control feature actions only if necessary.
addAndStartAction(
new SystemAudioActionFromTv(tv(), mAvrAddress, targetSystemAudioMode, null));
diff --git a/services/core/java/com/android/server/pm/TEST_MAPPING b/services/core/java/com/android/server/pm/TEST_MAPPING
index 2c13bd0..18d2390 100644
--- a/services/core/java/com/android/server/pm/TEST_MAPPING
+++ b/services/core/java/com/android/server/pm/TEST_MAPPING
@@ -104,6 +104,54 @@
"include-filter": "android.appsecurity.cts.EphemeralTest#testGetSearchableInfo"
}
]
+ },
+ {
+ "name": "CtsPackageInstallerCUJInstallationTestCases",
+ "file_patterns": [
+ "core/java/.*Install.*",
+ "services/core/.*Install.*",
+ "services/core/java/com/android/server/pm/.*"
+ ],
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
+ "name": "CtsPackageInstallerCUJUninstallationTestCases",
+ "file_patterns": [
+ "core/java/.*Install.*",
+ "services/core/.*Install.*",
+ "services/core/java/com/android/server/pm/.*"
+ ],
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
+ "name": "CtsPackageInstallerCUJUpdateSelfTestCases",
+ "file_patterns": [
+ "core/java/.*Install.*",
+ "services/core/.*Install.*",
+ "services/core/java/com/android/server/pm/.*"
+ ],
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
}
],
"presubmit-large":[
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index aa56e8d..934feb3 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -3352,6 +3352,12 @@
mConsumedKeysForDevice.put(deviceId, consumedKeys);
}
+ // TODO(b/358569822) Remove below once we have nicer API for listening to shortcuts
+ if ((event.isMetaPressed() || KeyEvent.isMetaKey(keyCode))
+ && shouldInterceptShortcuts(focusedToken)) {
+ return keyNotConsumed;
+ }
+
if (interceptSystemKeysAndShortcuts(focusedToken, event)
&& event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
consumedKeys.add(keyCode);
@@ -3416,7 +3422,7 @@
return handleHomeShortcuts(focusedToken, event);
case KeyEvent.KEYCODE_RECENT_APPS:
if (firstDown) {
- showRecentApps(false /* triggeredFromAltTab */);
+ toggleRecentApps();
notifyKeyGestureCompleted(event,
KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS);
}
@@ -3842,6 +3848,15 @@
return (metaState & KeyEvent.META_META_ON) != 0;
}
+ private boolean shouldInterceptShortcuts(IBinder focusedToken) {
+ KeyInterceptionInfo info =
+ mWindowManagerInternal.getKeyInterceptionInfoFromToken(focusedToken);
+ boolean hasInterceptWindowFlag = (info.layoutParamsPrivateFlags
+ & WindowManager.LayoutParams.PRIVATE_FLAG_ALLOW_ACTION_KEY_EVENTS) != 0;
+ return hasInterceptWindowFlag && mButtonOverridePermissionChecker.canAppOverrideSystemKey(
+ mContext, info.windowOwnerUid);
+ }
+
/**
* In this function, we check whether a system key should be sent to the application. We also
* detect the key gesture on this key, even if the key will be sent to the app. The gesture
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsService.java b/services/core/java/com/android/server/powerstats/PowerStatsService.java
index 1ca267e99..89fa9b6 100644
--- a/services/core/java/com/android/server/powerstats/PowerStatsService.java
+++ b/services/core/java/com/android/server/powerstats/PowerStatsService.java
@@ -304,12 +304,22 @@
@Override
public void onStart() {
if (getPowerStatsHal().isInitialized()) {
- mPowerStatsInternal = new LocalService();
- publishLocalService(PowerStatsInternal.class, mPowerStatsInternal);
+ publishLocalService(PowerStatsInternal.class, getPowerStatsInternal());
}
publishBinderService(Context.POWER_STATS_SERVICE, mService);
}
+ /**
+ * Returns the PowerStatsInternal associated with this service, maybe creating it if needed.
+ */
+ @VisibleForTesting
+ public PowerStatsInternal getPowerStatsInternal() {
+ if (mPowerStatsInternal == null) {
+ mPowerStatsInternal = new LocalService();
+ }
+ return mPowerStatsInternal;
+ }
+
private void onSystemServicesReady() {
mPullAtomCallback = mInjector.createStatsPullerImpl(mContext, mPowerStatsInternal);
mDeviceConfigListener.startListening();
@@ -456,7 +466,13 @@
private void getEnergyConsumedAsync(CompletableFuture<EnergyConsumerResult[]> future,
int[] energyConsumerIds) {
- EnergyConsumerResult[] results = getPowerStatsHal().getEnergyConsumed(energyConsumerIds);
+ EnergyConsumerResult[] results;
+ try {
+ results = getPowerStatsHal().getEnergyConsumed(energyConsumerIds);
+ } catch (Exception e) {
+ future.completeExceptionally(e);
+ return;
+ }
// STOPSHIP(253292374): Remove once missing EnergyConsumer results issue is resolved.
EnergyConsumer[] energyConsumers = getEnergyConsumerInfo();
@@ -523,12 +539,20 @@
private void getStateResidencyAsync(CompletableFuture<StateResidencyResult[]> future,
int[] powerEntityIds) {
- future.complete(getPowerStatsHal().getStateResidency(powerEntityIds));
+ try {
+ future.complete(getPowerStatsHal().getStateResidency(powerEntityIds));
+ } catch (Exception e) {
+ future.completeExceptionally(e);
+ }
}
private void readEnergyMeterAsync(CompletableFuture<EnergyMeasurement[]> future,
int[] channelIds) {
- future.complete(getPowerStatsHal().readEnergyMeter(channelIds));
+ try {
+ future.complete(getPowerStatsHal().readEnergyMeter(channelIds));
+ } catch (Exception e) {
+ future.completeExceptionally(e);
+ }
}
private static class PowerMonitorState {
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java
index 6721893..c543b6d 100644
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -19,6 +19,7 @@
import static android.media.AudioManager.DEVICE_NONE;
import static android.media.tv.TvInputManager.INPUT_STATE_CONNECTED;
import static android.media.tv.TvInputManager.INPUT_STATE_CONNECTED_STANDBY;
+import static android.media.tv.flags.Flags.kidsModeTvdbSharing;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -805,6 +806,19 @@
}
}
+ private boolean isServiceSingleUser(ComponentName component) {
+ try {
+ ServiceInfo serviceInfo = getContext().getPackageManager()
+ .getServiceInfo(component, 0);
+ // Check if the single-user flag is present
+ return (serviceInfo.flags & ServiceInfo.FLAG_SINGLE_USER) != 0;
+ } catch (PackageManager.NameNotFoundException e) {
+ // Handle the case where the service is not found
+ Slog.e(TAG, "Service not found: " + component, e);
+ return false;
+ }
+ }
+
@GuardedBy("mLock")
private void abortPendingCreateSessionRequestsLocked(ServiceState serviceState,
String inputId, int userId) {
@@ -2840,6 +2854,26 @@
}
@Override
+ public int getClientUserId(String sessionId) {
+ ensureTunerResourceAccessPermission();
+ int clientUserId = TvInputManager.UNKNOWN_CLIENT_USER_ID;
+
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ clientUserId = getClientUserIdLocked(sessionId);
+ } catch (ClientUserIdNotFoundException e) {
+ Slog.e(TAG, "error in getClientUserId", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ return clientUserId;
+ }
+
+ @Override
public int getClientPriority(int useCase, String sessionId) {
ensureTunerResourceAccessPermission();
final int callingPid = Binder.getCallingPid();
@@ -2924,6 +2958,16 @@
return mSessionIdToSessionStateMap.get(sessionId).callingPid;
}
+ @GuardedBy("mLock")
+ private int getClientUserIdLocked(String sessionId) throws ClientUserIdNotFoundException {
+ SessionState sessionState = mSessionIdToSessionStateMap.get(sessionId);
+ if (sessionState == null) {
+ throw new ClientUserIdNotFoundException(
+ "Client UserId not found with sessionId " + sessionId);
+ }
+ return sessionState.userId;
+ }
+
private void ensureTunerResourceAccessPermission() {
if (mContext.checkCallingPermission(
android.Manifest.permission.TUNER_RESOURCE_ACCESS)
@@ -3495,11 +3539,15 @@
"bindServiceAsUser(service=" + serviceState.component + ", userId=" + userId
+ ")");
}
+ int bindUserId = userId;
+ if (kidsModeTvdbSharing() && isServiceSingleUser(serviceState.component)) {
+ bindUserId = UserHandle.USER_SYSTEM;
+ }
Intent i =
new Intent(TvInputService.SERVICE_INTERFACE).setComponent(serviceState.component);
serviceState.bound = mContext.bindServiceAsUser(i, serviceState.connection,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
- new UserHandle(userId));
+ new UserHandle(bindUserId));
if (!serviceState.bound) {
Slog.e(TAG, "failed to bind " + serviceState.component + " for userId " + userId);
mContext.unbindService(serviceState.connection);
@@ -4700,4 +4748,10 @@
super(name);
}
}
+
+ private static class ClientUserIdNotFoundException extends IllegalArgumentException {
+ ClientUserIdNotFoundException(String name) {
+ super(name);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/wm/DimmerAnimationHelper.java b/services/core/java/com/android/server/wm/DimmerAnimationHelper.java
index 37e4449..3dba57f 100644
--- a/services/core/java/com/android/server/wm/DimmerAnimationHelper.java
+++ b/services/core/java/com/android/server/wm/DimmerAnimationHelper.java
@@ -55,11 +55,10 @@
Change() {}
- void copyFrom(@NonNull Change other) {
+ Change(@NonNull Change other) {
mAlpha = other.mAlpha;
mBlurRadius = other.mBlurRadius;
mDimmingContainer = other.mDimmingContainer;
- mGeometryParent = other.mGeometryParent;
mRelativeLayer = other.mRelativeLayer;
}
@@ -84,8 +83,8 @@
}
}
- private final Change mCurrentProperties = new Change();
- private final Change mRequestedProperties = new Change();
+ private Change mCurrentProperties = new Change();
+ private Change mRequestedProperties = new Change();
private AnimationSpec mAlphaAnimationSpec;
private final AnimationAdapterFactory mAnimationAdapterFactory;
@@ -129,7 +128,7 @@
+ "call adjustRelativeLayer?");
return;
}
- if (mRequestedProperties.mDimmingContainer.getSurfaceControl() == null) {
+ if (mRequestedProperties.mDimmingContainer.mSurfaceControl == null) {
Log.w(TAG, "container " + mRequestedProperties.mDimmingContainer
+ "does not have a surface");
dim.remove(t);
@@ -155,35 +154,35 @@
"%s skipping animation and directly setting alpha=%f, blur=%d",
dim, mRequestedProperties.mAlpha,
mRequestedProperties.mBlurRadius);
- mCurrentProperties.copyFrom(mRequestedProperties);
- setCurrentAlphaBlur(dim.mDimSurface, t);
+ setAlphaBlur(dim.mDimSurface, mRequestedProperties.mAlpha,
+ mRequestedProperties.mBlurRadius, t);
dim.mSkipAnimation = false;
} else {
- Change startProperties = mCurrentProperties;
- mCurrentProperties.copyFrom(mRequestedProperties);
- startAnimation(t, dim, startProperties, mRequestedProperties);
+ startAnimation(t, dim);
}
+
} else if (!dim.isDimming()) {
// We are not dimming, so we tried the exit animation but the alpha is already 0,
// therefore, let's just remove this surface
dim.remove(t);
}
+ mCurrentProperties = new Change(mRequestedProperties);
}
private void startAnimation(
- @NonNull SurfaceControl.Transaction t, @NonNull Dimmer.DimState dim,
- @NonNull Change from, @NonNull Change to) {
+ @NonNull SurfaceControl.Transaction t, @NonNull Dimmer.DimState dim) {
ProtoLog.v(WM_DEBUG_DIMMER, "Starting animation on %s", dim);
- mAlphaAnimationSpec = getRequestedAnimationSpec(from, to);
+ mAlphaAnimationSpec = getRequestedAnimationSpec();
mLocalAnimationAdapter = mAnimationAdapterFactory.get(mAlphaAnimationSpec,
dim.mHostContainer.mWmService.mSurfaceAnimationRunner);
- float targetAlpha = to.mAlpha;
+ float targetAlpha = mRequestedProperties.mAlpha;
+ int targetBlur = mRequestedProperties.mBlurRadius;
mLocalAnimationAdapter.startAnimation(dim.mDimSurface, t,
ANIMATION_TYPE_DIMMER, /* finishCallback */ (type, animator) -> {
synchronized (dim.mHostContainer.mWmService.mGlobalLock) {
- setCurrentAlphaBlur(dim.mDimSurface, t);
+ setAlphaBlur(dim.mDimSurface, targetAlpha, targetBlur, t);
if (targetAlpha == 0f && !dim.isDimming()) {
dim.remove(t);
}
@@ -208,15 +207,15 @@
}
@NonNull
- private static AnimationSpec getRequestedAnimationSpec(Change from, Change to) {
- final float startAlpha = Math.max(from.mAlpha, 0f);
- final int startBlur = Math.max(from.mBlurRadius, 0);
- long duration = (long) (getDimDuration(to.mDimmingContainer)
- * Math.abs(to.mAlpha - startAlpha));
+ private AnimationSpec getRequestedAnimationSpec() {
+ final float startAlpha = Math.max(mCurrentProperties.mAlpha, 0f);
+ final int startBlur = Math.max(mCurrentProperties.mBlurRadius, 0);
+ long duration = (long) (getDimDuration(mRequestedProperties.mDimmingContainer)
+ * Math.abs(mRequestedProperties.mAlpha - startAlpha));
final AnimationSpec spec = new AnimationSpec(
- new AnimationSpec.AnimationExtremes<>(startAlpha, to.mAlpha),
- new AnimationSpec.AnimationExtremes<>(startBlur, to.mBlurRadius),
+ new AnimationSpec.AnimationExtremes<>(startAlpha, mRequestedProperties.mAlpha),
+ new AnimationSpec.AnimationExtremes<>(startBlur, mRequestedProperties.mBlurRadius),
duration
);
ProtoLog.v(WM_DEBUG_DIMMER, "Dim animation requested: %s", spec);
@@ -226,7 +225,7 @@
/**
* Change the geometry and relative parent of this dim layer
*/
- static void reparent(@NonNull SurfaceControl dimLayer,
+ void reparent(@NonNull SurfaceControl dimLayer,
@Nullable SurfaceControl newGeometryParent,
@NonNull SurfaceControl relativeParent,
int relativePosition,
@@ -241,16 +240,17 @@
}
}
- void setCurrentAlphaBlur(@NonNull SurfaceControl sc, @NonNull SurfaceControl.Transaction t) {
+ void setAlphaBlur(@NonNull SurfaceControl sc, float alpha, int blur,
+ @NonNull SurfaceControl.Transaction t) {
try {
- t.setAlpha(sc, mCurrentProperties.mAlpha);
- t.setBackgroundBlurRadius(sc, mCurrentProperties.mBlurRadius);
+ t.setAlpha(sc, alpha);
+ t.setBackgroundBlurRadius(sc, blur);
} catch (NullPointerException e) {
Log.w(TAG , "Tried to change look of dim " + sc + " after remove", e);
}
}
- private static long getDimDuration(@NonNull WindowContainer<?> container) {
+ private long getDimDuration(@NonNull WindowContainer<?> container) {
// Use the same duration as the animation on the WindowContainer
AnimationAdapter animationAdapter = container.mSurfaceAnimator.getAnimation();
final float durationScale = container.mWmService.getTransitionAnimationScaleLocked();
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 86285fb..129931e 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -857,8 +857,8 @@
return false;
}
if (w.mAttrs.type == TYPE_INPUT_METHOD_DIALOG && mImeLayeringTarget != null
- && !mImeLayeringTarget.isRequestedVisible(ime())
- && !mImeLayeringTarget.isVisibleRequested()) {
+ && !(mImeLayeringTarget.isRequestedVisible(ime())
+ && mImeLayeringTarget.isVisibleRequested())) {
return false;
}
diff --git a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
index dab3978..8332b8b 100644
--- a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
+++ b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
@@ -46,12 +46,12 @@
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.art.ArtManagerLocal;
+import com.android.server.profcollect.Utils;
import com.android.server.wm.ActivityMetricsLaunchObserver;
import com.android.server.wm.ActivityMetricsLaunchObserverRegistry;
import com.android.server.wm.ActivityTaskManagerInternal;
import java.util.Arrays;
-import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
/**
@@ -280,11 +280,7 @@
return;
}
- // Sample for a fraction of app launches.
- int traceFrequency = DeviceConfig.getInt(DeviceConfig.NAMESPACE_PROFCOLLECT_NATIVE_BOOT,
- "applaunch_trace_freq", 2);
- int randomNum = ThreadLocalRandom.current().nextInt(100);
- if (randomNum < traceFrequency) {
+ if (Utils.withFrequency("applaunch_trace_freq", 2)) {
BackgroundThread.get().getThreadHandler().post(() -> {
try {
mIProfcollect.trace_system("applaunch");
@@ -318,12 +314,7 @@
if (mIProfcollect == null) {
return;
}
- // Sample for a fraction of dex2oat runs.
- final int traceFrequency =
- DeviceConfig.getInt(DeviceConfig.NAMESPACE_PROFCOLLECT_NATIVE_BOOT,
- "dex2oat_trace_freq", 25);
- int randomNum = ThreadLocalRandom.current().nextInt(100);
- if (randomNum < traceFrequency) {
+ if (Utils.withFrequency("dex2oat_trace_freq", 25)) {
// Dex2oat could take a while before it starts. Add a short delay before start tracing.
BackgroundThread.get().getThreadHandler().postDelayed(() -> {
try {
@@ -393,27 +384,22 @@
if (Arrays.asList(cameraSkipPackages).contains(packageId)) {
return;
}
- // Sample for a fraction of camera events.
- final int traceFrequency =
- DeviceConfig.getInt(DeviceConfig.NAMESPACE_PROFCOLLECT_NATIVE_BOOT,
- "camera_trace_freq", 10);
- int randomNum = ThreadLocalRandom.current().nextInt(100);
- if (randomNum >= traceFrequency) {
- return;
- }
- final int traceDuration = 5000;
- final String traceTag = "camera";
- BackgroundThread.get().getThreadHandler().post(() -> {
- if (mIProfcollect == null) {
- return;
- }
- try {
- mIProfcollect.trace_process(traceTag, "android.hardware.camera.provider",
+ if (Utils.withFrequency("camera_trace_freq", 10)) {
+ final int traceDuration = 5000;
+ final String traceTag = "camera";
+ BackgroundThread.get().getThreadHandler().post(() -> {
+ if (mIProfcollect == null) {
+ return;
+ }
+ try {
+ mIProfcollect.trace_process(traceTag,
+ "android.hardware.camera.provider",
traceDuration);
- } catch (RemoteException e) {
- Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage());
- }
- });
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage());
+ }
+ });
+ }
}
}, null);
}
diff --git a/services/profcollect/src/com/android/server/profcollect/Utils.java b/services/profcollect/src/com/android/server/profcollect/Utils.java
new file mode 100644
index 0000000..d5ef14c
--- /dev/null
+++ b/services/profcollect/src/com/android/server/profcollect/Utils.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.profcollect;
+
+import android.provider.DeviceConfig;
+
+import java.util.concurrent.ThreadLocalRandom;
+
+public final class Utils {
+
+ public static boolean withFrequency(String configName, int defaultFrequency) {
+ int threshold = DeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_PROFCOLLECT_NATIVE_BOOT, configName, defaultFrequency);
+ int randomNum = ThreadLocalRandom.current().nextInt(100);
+ return randomNum < threshold;
+ }
+
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java
index 82acaf8..f728168 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java
@@ -258,6 +258,7 @@
when(mMockedLogicalDisplay.isEnabledLocked()).thenReturn(false);
mExternalDisplayPolicy.handleExternalDisplayConnectedLocked(mMockedLogicalDisplay);
+ mHandler.flush();
verify(mMockedInjector, never()).sendExternalDisplayEventLocked(any(), anyInt());
verify(mMockedDisplayNotificationManager, times(2))
.onHighTemperatureExternalDisplayNotAllowed();
diff --git a/services/tests/powerstatstests/src/com/android/server/powerstats/PowerStatsServiceTest.java b/services/tests/powerstatstests/src/com/android/server/powerstats/PowerStatsServiceTest.java
index 53e3143..115cdf6 100644
--- a/services/tests/powerstatstests/src/com/android/server/powerstats/PowerStatsServiceTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/powerstats/PowerStatsServiceTest.java
@@ -74,6 +74,8 @@
import java.util.Arrays;
import java.util.Map;
import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
/**
@@ -221,6 +223,7 @@
};
public static final class TestPowerStatsHALWrapper implements IPowerStatsHALWrapper {
+ public RuntimeException exception;
public EnergyConsumerResult[] energyConsumerResults;
public EnergyMeasurement[] energyMeasurements;
@@ -243,6 +246,9 @@
@Override
public StateResidencyResult[] getStateResidency(int[] powerEntityIds) {
+ if (exception != null) {
+ throw exception;
+ }
StateResidencyResult[] stateResidencyResultList =
new StateResidencyResult[POWER_ENTITY_COUNT];
for (int i = 0; i < stateResidencyResultList.length; i++) {
@@ -294,6 +300,9 @@
@Override
public EnergyConsumerResult[] getEnergyConsumed(int[] energyConsumerIds) {
+ if (exception != null) {
+ throw exception;
+ }
return energyConsumerResults;
}
@@ -322,6 +331,9 @@
@Override
public EnergyMeasurement[] readEnergyMeter(int[] channelIds) {
+ if (exception != null) {
+ throw exception;
+ }
return energyMeasurements;
}
@@ -1222,4 +1234,31 @@
assertThrows(NullPointerException.class, () -> iPowerStatsService.getPowerMonitorReadings(
new int[] {0}, null));
}
+
+ @Test
+ public void getEnergyConsumedAsync_halException() {
+ mPowerStatsHALWrapper.exception = new IllegalArgumentException();
+ CompletableFuture<EnergyConsumerResult[]> future =
+ mService.getPowerStatsInternal().getEnergyConsumedAsync(new int[]{1});
+ ExecutionException exception = assertThrows(ExecutionException.class, future::get);
+ assertThat(exception.getCause()).isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ public void getStateResidencyAsync_halException() {
+ mPowerStatsHALWrapper.exception = new IllegalArgumentException();
+ CompletableFuture<StateResidencyResult[]> future =
+ mService.getPowerStatsInternal().getStateResidencyAsync(new int[]{1});
+ ExecutionException exception = assertThrows(ExecutionException.class, future::get);
+ assertThat(exception.getCause()).isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ public void readEnergyMeterAsync_halException() {
+ mPowerStatsHALWrapper.exception = new IllegalArgumentException();
+ CompletableFuture<EnergyMeasurement[]> future =
+ mService.getPowerStatsInternal().readEnergyMeterAsync(new int[]{1});
+ ExecutionException exception = assertThrows(ExecutionException.class, future::get);
+ assertThat(exception.getCause()).isInstanceOf(IllegalArgumentException.class);
+ }
}
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 701c350..ace4b15 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -36,8 +36,8 @@
"-Werror",
],
static_libs: [
- "a11ychecker-protos-java-proto-lite",
"aatf",
+ "accessibility_protos_lite",
"cts-input-lib",
"frameworks-base-testutils",
"services.accessibility",
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManagerTest.java
index c1b3929..5ee86ff 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManagerTest.java
@@ -23,7 +23,7 @@
import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME;
import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_ACTIVITY_NAME;
import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_DEFAULT_BROWSER;
-import static com.android.server.accessibility.a11ychecker.TestUtils.createAtom;
+import static com.android.server.accessibility.a11ychecker.TestUtils.createResult;
import static com.android.server.accessibility.a11ychecker.TestUtils.getMockPackageManagerWithInstalledApps;
import static com.google.common.truth.Truth.assertThat;
@@ -32,6 +32,8 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
+import android.accessibility.AccessibilityCheckClass;
+import android.accessibility.AccessibilityCheckResultType;
import android.content.ComponentName;
import android.content.pm.PackageManager;
import android.platform.test.annotations.DisableFlags;
@@ -112,19 +114,19 @@
.setViewIdResourceName("node2")
.build();
- Set<A11yCheckerProto.AccessibilityCheckResultReported> results =
+ Set<AndroidAccessibilityCheckerResult> results =
mAccessibilityCheckerManager.maybeRunA11yChecker(
List.of(mockNodeInfo1, mockNodeInfo2), QUALIFIED_TEST_ACTIVITY_NAME,
new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
TEST_A11Y_SERVICE_CLASS_NAME), /*userId=*/ 0);
assertThat(results).containsExactly(
- createAtom(/*viewIdResourceName=*/ "node1", TEST_ACTIVITY_NAME,
- A11yCheckerProto.AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK,
- A11yCheckerProto.AccessibilityCheckResultType.ERROR, /*resultId=*/ 2),
- createAtom(/*viewIdResourceName=*/ "node2", TEST_ACTIVITY_NAME,
- A11yCheckerProto.AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK,
- A11yCheckerProto.AccessibilityCheckResultType.ERROR, /*resultId=*/ 2)
+ createResult(/*viewIdResourceName=*/ "node1", TEST_ACTIVITY_NAME,
+ AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK,
+ AccessibilityCheckResultType.ERROR_CHECK_RESULT_TYPE, /*resultId=*/ 2),
+ createResult(/*viewIdResourceName=*/ "node2", TEST_ACTIVITY_NAME,
+ AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK,
+ AccessibilityCheckResultType.ERROR_CHECK_RESULT_TYPE, /*resultId=*/ 2)
);
}
@@ -137,7 +139,7 @@
.setViewIdResourceName("node1")
.build();
- Set<A11yCheckerProto.AccessibilityCheckResultReported> results =
+ Set<AndroidAccessibilityCheckerResult> results =
mAccessibilityCheckerManager.maybeRunA11yChecker(
List.of(mockNodeInfo), QUALIFIED_TEST_ACTIVITY_NAME,
new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
@@ -158,16 +160,17 @@
.setViewIdResourceName("node1")
.build();
- Set<A11yCheckerProto.AccessibilityCheckResultReported> results =
+ Set<AndroidAccessibilityCheckerResult> results =
mAccessibilityCheckerManager.maybeRunA11yChecker(
List.of(mockNodeInfo, mockNodeInfoDuplicate), QUALIFIED_TEST_ACTIVITY_NAME,
new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
TEST_A11Y_SERVICE_CLASS_NAME), /*userId=*/ 0);
assertThat(results).containsExactly(
- createAtom(/*viewIdResourceName=*/ "node1", TEST_ACTIVITY_NAME,
- A11yCheckerProto.AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK,
- A11yCheckerProto.AccessibilityCheckResultType.ERROR, /*resultId=*/ 2)
+ createResult(/*viewIdResourceName=*/ "node1", TEST_ACTIVITY_NAME,
+ AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK,
+ AccessibilityCheckResultType.ERROR_CHECK_RESULT_TYPE, /*resultId=*/
+ 2)
);
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java
index 5b4e72e..4ec2fb9 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java
@@ -21,13 +21,15 @@
import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME;
import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_ACTIVITY_NAME;
import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_APP_PACKAGE_NAME;
-import static com.android.server.accessibility.a11ychecker.TestUtils.createAtom;
+import static com.android.server.accessibility.a11ychecker.TestUtils.createResult;
import static com.android.server.accessibility.a11ychecker.TestUtils.getMockPackageManagerWithInstalledApps;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.when;
+import android.accessibility.AccessibilityCheckClass;
+import android.accessibility.AccessibilityCheckResultType;
import android.content.ComponentName;
import android.content.pm.PackageManager;
import android.view.accessibility.AccessibilityNodeInfo;
@@ -87,7 +89,7 @@
AccessibilityCheckResult.AccessibilityCheckResultType.NOT_RUN, null, 5,
null);
- Set<A11yCheckerProto.AccessibilityCheckResultReported> atoms =
+ Set<AndroidAccessibilityCheckerResult> results =
AccessibilityCheckerUtils.processResults(
mockNodeInfo,
List.of(result1, result2, result3, result4),
@@ -96,13 +98,13 @@
new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
TEST_A11Y_SERVICE_CLASS_NAME));
- assertThat(atoms).containsExactly(
- createAtom("TargetNode", "",
- A11yCheckerProto.AccessibilityCheckClass.SPEAKABLE_TEXT_PRESENT_CHECK,
- A11yCheckerProto.AccessibilityCheckResultType.WARNING, 1),
- createAtom("TargetNode", "",
- A11yCheckerProto.AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK,
- A11yCheckerProto.AccessibilityCheckResultType.ERROR, 2)
+ assertThat(results).containsExactly(
+ createResult("TargetNode", "",
+ AccessibilityCheckClass.SPEAKABLE_TEXT_PRESENT_CHECK,
+ AccessibilityCheckResultType.WARNING_CHECK_RESULT_TYPE, 1),
+ createResult("TargetNode", "",
+ AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK,
+ AccessibilityCheckResultType.ERROR_CHECK_RESULT_TYPE, 2)
);
}
@@ -126,7 +128,7 @@
TouchTargetSizeCheck.class,
AccessibilityCheckResult.AccessibilityCheckResultType.ERROR, null, 2, null);
- Set<A11yCheckerProto.AccessibilityCheckResultReported> atoms =
+ Set<AndroidAccessibilityCheckerResult> results =
AccessibilityCheckerUtils.processResults(
mockNodeInfo,
List.of(result1, result2),
@@ -135,7 +137,7 @@
new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
TEST_A11Y_SERVICE_CLASS_NAME));
- assertThat(atoms).isEmpty();
+ assertThat(results).isEmpty();
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/TestUtils.java b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/TestUtils.java
index acf64b6..8e0b2ed 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/TestUtils.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/TestUtils.java
@@ -20,6 +20,8 @@
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.when;
+import android.accessibility.AccessibilityCheckClass;
+import android.accessibility.AccessibilityCheckResultType;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.pm.ActivityInfo;
@@ -90,20 +92,20 @@
return accessibilityEvent;
}
- static A11yCheckerProto.AccessibilityCheckResultReported createAtom(
+ static AndroidAccessibilityCheckerResult createResult(
String viewIdResourceName,
String activityName,
- A11yCheckerProto.AccessibilityCheckClass checkClass,
- A11yCheckerProto.AccessibilityCheckResultType resultType,
+ AccessibilityCheckClass checkClass,
+ AccessibilityCheckResultType resultType,
int resultId) {
- return A11yCheckerProto.AccessibilityCheckResultReported.newBuilder()
+ return AndroidAccessibilityCheckerResult.newBuilder()
.setPackageName(TEST_APP_PACKAGE_NAME)
.setAppVersionCode(TEST_APP_VERSION_CODE)
.setUiElementPath(TEST_APP_PACKAGE_NAME + ":" + viewIdResourceName)
.setWindowTitle(TEST_WINDOW_TITLE)
.setActivityName(activityName)
.setSourceComponentName(new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
- TEST_A11Y_SERVICE_CLASS_NAME).flattenToString())
+ TEST_A11Y_SERVICE_CLASS_NAME))
.setSourceVersionCode(TEST_A11Y_SERVICE_SOURCE_VERSION_CODE)
.setResultCheckClass(checkClass)
.setResultType(resultType)
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index 2b93ccb..a7e8a00 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -2148,6 +2148,40 @@
.hasSize(1);
}
+
+ @Test
+ public void handleReportAudioStatus_SamOnAvrStandby_startSystemAudioActionFromTv() {
+ mHdmiControlService.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL,
+ HdmiControlManager.SYSTEM_AUDIO_CONTROL_ENABLED);
+ // Emulate Audio device on port 0x1000 (does not support ARC)
+ mNativeWrapper.setPortConnectionStatus(1, true);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ HdmiCecMessage reportPowerStatus =
+ HdmiCecMessageBuilder.buildReportPowerStatus(ADDR_AUDIO_SYSTEM, ADDR_TV,
+ HdmiControlManager.POWER_STATUS_STANDBY);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mNativeWrapper.onCecMessage(reportPowerStatus);
+ mTestLooper.dispatchAll();
+ assertThat(mHdmiCecLocalDeviceTv.getActions(SystemAudioActionFromTv.class)).hasSize(0);
+
+ HdmiCecFeatureAction systemAudioAutoInitiationAction =
+ new SystemAudioAutoInitiationAction(mHdmiCecLocalDeviceTv, ADDR_AUDIO_SYSTEM);
+ mHdmiCecLocalDeviceTv.addAndStartAction(systemAudioAutoInitiationAction);
+ HdmiCecMessage reportSystemAudioMode =
+ HdmiCecMessageBuilder.buildReportSystemAudioMode(
+ ADDR_AUDIO_SYSTEM,
+ mHdmiCecLocalDeviceTv.getDeviceInfo().getLogicalAddress(),
+ true);
+ mHdmiControlService.handleCecCommand(reportSystemAudioMode);
+ mTestLooper.dispatchAll();
+
+ // SAM must be on; ARC must be off
+ assertThat(mHdmiCecLocalDeviceTv.getActions(SystemAudioActionFromTv.class)).hasSize(1);
+ }
+
protected static class MockTvDevice extends HdmiCecLocalDeviceTv {
MockTvDevice(HdmiControlService service) {
super(service);
diff --git a/services/tests/wmtests/src/com/android/server/policy/MetaKeyEventsInterceptionTests.java b/services/tests/wmtests/src/com/android/server/policy/MetaKeyEventsInterceptionTests.java
new file mode 100644
index 0000000..b979335
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/policy/MetaKeyEventsInterceptionTests.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.policy;
+
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_ALLOW_ACTION_KEY_EVENTS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.view.KeyEvent;
+import android.view.WindowManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.policy.KeyInterceptionInfo;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Testing {@link PhoneWindowManager} functionality of letting app intercepting key events
+ * containing META.
+ */
+@SmallTest
+public class MetaKeyEventsInterceptionTests extends ShortcutKeyTestBase {
+
+ private static final List<KeyEvent> META_KEY_EVENTS = Arrays.asList(
+ new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_META_LEFT),
+ new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_META_RIGHT),
+ new KeyEvent(/* downTime= */ 0, /* eventTime= */
+ 0, /* action= */ 0, /* code= */ 0, /* repeat= */ 0,
+ /* metaState= */ KeyEvent.META_META_ON));
+
+ @Before
+ public void setUp() {
+ setUpPhoneWindowManager();
+ }
+
+ @Test
+ public void doesntInterceptMetaKeyEvents_whenWindowAskedForIt() {
+ mPhoneWindowManager.overrideFocusedWindowButtonOverridePermission(/* granted= */ true);
+ setWindowKeyInterceptionWithPrivateFlags(PRIVATE_FLAG_ALLOW_ACTION_KEY_EVENTS);
+
+ META_KEY_EVENTS.forEach(keyEvent -> {
+ assertKeyInterceptionResult(keyEvent, /* intercepted= */ false);
+ });
+ }
+
+ @Test
+ public void interceptsMetaKeyEvents_whenWindowDoesntHaveFlagSet() {
+ mPhoneWindowManager.overrideFocusedWindowButtonOverridePermission(/* granted= */ true);
+ setWindowKeyInterceptionWithPrivateFlags(0);
+
+ META_KEY_EVENTS.forEach(keyEvent -> {
+ assertKeyInterceptionResult(keyEvent, /* intercepted= */ true);
+ });
+ }
+
+ @Test
+ public void interceptsMetaKeyEvents_whenWindowDoesntHavePermission() {
+ mPhoneWindowManager.overrideFocusedWindowButtonOverridePermission(/* granted= */ false);
+ setWindowKeyInterceptionWithPrivateFlags(PRIVATE_FLAG_ALLOW_ACTION_KEY_EVENTS);
+
+ META_KEY_EVENTS.forEach(keyEvent -> {
+ assertKeyInterceptionResult(keyEvent, /* intercepted= */ true);
+ });
+ }
+
+ private void setWindowKeyInterceptionWithPrivateFlags(int privateFlags) {
+ KeyInterceptionInfo info = new KeyInterceptionInfo(
+ WindowManager.LayoutParams.TYPE_APPLICATION, privateFlags, "title", 0);
+ mPhoneWindowManager.overrideWindowKeyInterceptionInfo(info);
+ }
+
+ private void assertKeyInterceptionResult(KeyEvent keyEvent, boolean intercepted) {
+ long result = mPhoneWindowManager.interceptKeyBeforeDispatching(keyEvent);
+ int expected = intercepted ? -1 : 0;
+ assertThat(result).isEqualTo(expected);
+ }
+}
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 43b065d..79c7ac1 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -52,6 +52,7 @@
import static org.mockito.Mockito.description;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
import android.app.ActivityManagerInternal;
@@ -613,6 +614,10 @@
.when(mButtonOverridePermissionChecker).canAppOverrideSystemKey(any(), anyInt());
}
+ void overrideWindowKeyInterceptionInfo(KeyInterceptionInfo info) {
+ when(mWindowManagerInternal.getKeyInterceptionInfoFromToken(any())).thenReturn(info);
+ }
+
void overrideKeyEventPolicyFlags(int flags) {
mKeyEventPolicyFlags = flags;
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index ec5e51e..58e919d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -2796,15 +2796,17 @@
final WindowState imeAppTarget =
createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, "imeAppTarget");
mDisplayContent.setImeLayeringTarget(imeAppTarget);
- spyOn(imeAppTarget);
- doReturn(true).when(imeAppTarget).isRequestedVisible(ime());
+ imeAppTarget.setRequestedVisibleTypes(ime());
assertEquals(imeMenuDialog, mDisplayContent.findFocusedWindow());
// Verify imeMenuDialog doesn't be focused window if the next IME target is closing.
final WindowState nextImeAppTarget =
createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, "nextImeAppTarget");
makeWindowVisibleAndDrawn(nextImeAppTarget);
- nextImeAppTarget.mActivityRecord.commitVisibility(false, false);
+ // Even if the app still requests IME, the ime dialog should not gain focus if the target
+ // app is invisible.
+ nextImeAppTarget.setRequestedVisibleTypes(ime());
+ nextImeAppTarget.mActivityRecord.setVisibility(false);
mDisplayContent.setImeLayeringTarget(nextImeAppTarget);
assertNotEquals(imeMenuDialog, mDisplayContent.findFocusedWindow());
}
diff --git a/tests/UiBench/src/com/android/test/uibench/ResizeHWLayerActivity.java b/tests/UiBench/src/com/android/test/uibench/ResizeHWLayerActivity.java
index 80d495d..cb26edc 100644
--- a/tests/UiBench/src/com/android/test/uibench/ResizeHWLayerActivity.java
+++ b/tests/UiBench/src/com/android/test/uibench/ResizeHWLayerActivity.java
@@ -30,6 +30,8 @@
*/
public class ResizeHWLayerActivity extends AppCompatActivity {
+ private ValueAnimator mAnimator;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -43,10 +45,10 @@
PropertyValuesHolder pvhWidth = PropertyValuesHolder.ofInt("width", width, 1);
PropertyValuesHolder pvhHeight = PropertyValuesHolder.ofInt("height", height, 1);
final LayoutParams params = child.getLayoutParams();
- ValueAnimator animator = ValueAnimator.ofPropertyValuesHolder(pvhWidth, pvhHeight);
- animator.setRepeatMode(ValueAnimator.REVERSE);
- animator.setRepeatCount(ValueAnimator.INFINITE);
- animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ mAnimator = ValueAnimator.ofPropertyValuesHolder(pvhWidth, pvhHeight);
+ mAnimator.setRepeatMode(ValueAnimator.REVERSE);
+ mAnimator.setRepeatCount(ValueAnimator.INFINITE);
+ mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
params.width = (Integer)valueAnimator.getAnimatedValue("width");
@@ -54,7 +56,15 @@
child.requestLayout();
}
});
- animator.start();
+ mAnimator.start();
setContentView(child);
}
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (mAnimator != null) {
+ mAnimator.cancel();
+ }
+ }
}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/Exceptions.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/Exceptions.kt
index 910bf59..f59e143 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/Exceptions.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/Exceptions.kt
@@ -15,6 +15,8 @@
*/
package com.android.hoststubgen
+import java.io.File
+
/**
* We will not print the stack trace for exceptions implementing it.
*/
@@ -49,4 +51,22 @@
/**
* We use this for general "user" errors.
*/
-class HostStubGenUserErrorException(message: String) : Exception(message), UserErrorException
+class GeneralUserErrorException(message: String) : Exception(message), UserErrorException
+
+/** Base exception class for invalid command line arguments. */
+open class ArgumentsException(message: String?) : Exception(message), UserErrorException
+
+/** Thrown when the same annotation is used with different annotation arguments. */
+class DuplicateAnnotationException(annotationName: String?) :
+ ArgumentsException("Duplicate annotation specified: '$annotationName'")
+
+/** Thrown when an input file does not exist. */
+class InputFileNotFoundException(filename: String) :
+ ArgumentsException("File '$filename' not found")
+
+fun String.ensureFileExists(): String {
+ if (!File(this).exists()) {
+ throw InputFileNotFoundException(this)
+ }
+ return this
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenErrors.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenErrors.kt
index 6b01d48..a218c55 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenErrors.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenErrors.kt
@@ -19,6 +19,6 @@
open fun onErrorFound(message: String) {
// TODO: For now, we just throw as soon as any error is found, but eventually we should keep
// all errors and print them at the end.
- throw HostStubGenUserErrorException(message)
+ throw GeneralUserErrorException(message)
}
}
\ No newline at end of file
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenLogger.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenLogger.kt
index fcdf824..4bcee40 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenLogger.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenLogger.kt
@@ -89,6 +89,8 @@
addPrinter(StreamPrinter(level, PrintWriter(BufferedOutputStream(
FileOutputStream(logFilename)))))
+ log.i("Log file set: $logFilename for $level")
+
return this
}
@@ -122,6 +124,9 @@
}
fun println(level: LogLevel, message: String) {
+ if (message.isEmpty()) {
+ return // Don't print an empty message.
+ }
printers.forEach {
if (it.logLevel.ordinal >= level.ordinal) {
it.println(level, indent, message)
@@ -185,31 +190,45 @@
println(LogLevel.Debug, format, *args)
}
- inline fun <T> logTime(level: LogLevel, message: String, block: () -> T): T {
+ inline fun <T> logTime(level: LogLevel, message: String, block: () -> T): Double {
+ var ret: Double = -1.0
val start = System.currentTimeMillis()
try {
- return block()
+ block()
} finally {
val end = System.currentTimeMillis()
+ ret = (end - start) / 1000.0
if (isEnabled(level)) {
println(level,
String.format("%s: took %.1f second(s).", message, (end - start) / 1000.0))
}
}
+ return ret
}
- inline fun <T> iTime(message: String, block: () -> T): T {
+ /** Do an "i" log with how long it took. */
+ inline fun <T> iTime(message: String, block: () -> T): Double {
return logTime(LogLevel.Info, message, block)
}
- inline fun <T> vTime(message: String, block: () -> T): T {
+ /** Do a "v" log with how long it took. */
+ inline fun <T> vTime(message: String, block: () -> T): Double {
return logTime(LogLevel.Verbose, message, block)
}
- inline fun <T> dTime(message: String, block: () -> T): T {
+ /** Do a "d" log with how long it took. */
+ inline fun <T> dTime(message: String, block: () -> T): Double {
return logTime(LogLevel.Debug, message, block)
}
+ /**
+ * Similar to the other "xTime" methods, but the message is not supposed to be printed.
+ * It's only used to measure the duration with the same interface as other log methods.
+ */
+ inline fun <T> nTime(block: () -> T): Double {
+ return logTime(LogLevel.Debug, "", block)
+ }
+
inline fun forVerbose(block: () -> Unit) {
if (isEnabled(LogLevel.Verbose)) {
block()
@@ -253,6 +272,21 @@
}
}
}
+
+ /**
+ * Handle log-related command line arguments.
+ */
+ fun maybeHandleCommandLineArg(currentArg: String, nextArgProvider: () -> String): Boolean {
+ when (currentArg) {
+ "-v", "--verbose" -> setConsoleLogLevel(LogLevel.Verbose)
+ "-d", "--debug" -> setConsoleLogLevel(LogLevel.Debug)
+ "-q", "--quiet" -> setConsoleLogLevel(LogLevel.None)
+ "--verbose-log" -> addFilePrinter(LogLevel.Verbose, nextArgProvider())
+ "--debug-log" -> addFilePrinter(LogLevel.Debug, nextArgProvider())
+ else -> return false
+ }
+ return true
+ }
}
private interface LogPrinter {
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenMain.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenMain.kt
index 45e7e30..8506466 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenMain.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenMain.kt
@@ -24,20 +24,32 @@
*/
fun main(args: Array<String>) {
executableName = "HostStubGen"
+ runMainWithBoilerplate {
+ // Parse the command line arguments.
+ var clanupOnError = false
+ try {
+ val options = HostStubGenOptions.parseArgs(args)
+ clanupOnError = options.cleanUpOnError.get
+ log.v("$executableName started")
+ log.v("Options: $options")
+
+ // Run.
+ HostStubGen(options).run()
+ } catch (e: Throwable) {
+ if (clanupOnError) {
+ TODO("Remove output jars here")
+ }
+ throw e
+ }
+ }
+}
+
+inline fun runMainWithBoilerplate(realMain: () -> Unit) {
var success = false
- var clanupOnError = false
try {
- // Parse the command line arguments.
- val options = HostStubGenOptions.parseArgs(args)
- clanupOnError = options.cleanUpOnError.get
-
- log.v("$executableName started")
- log.v("Options: $options")
-
- // Run.
- HostStubGen(options).run()
+ realMain()
success = true
} catch (e: Throwable) {
@@ -45,9 +57,6 @@
if (e !is UserErrorException) {
e.printStackTrace(PrintWriter(log.getWriter(LogLevel.Error)))
}
- if (clanupOnError) {
- TODO("Remove output jars here")
- }
} finally {
log.i("$executableName finished")
log.flush()
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
index 2f833a8..f88b107 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
@@ -17,20 +17,17 @@
import com.android.hoststubgen.filters.FilterPolicy
import java.io.BufferedReader
-import java.io.File
import java.io.FileReader
/**
* A single value that can only set once.
*/
-class SetOnce<T>(
- private var value: T,
-) {
+open class SetOnce<T>(private var value: T) {
class SetMoreThanOnceException : Exception()
private var set = false
- fun set(v: T) {
+ fun set(v: T): T {
if (set) {
throw SetMoreThanOnceException()
}
@@ -39,6 +36,7 @@
}
set = true
value = v
+ return v
}
val get: T
@@ -59,6 +57,16 @@
}
}
+class IntSetOnce(value: Int) : SetOnce<Int>(value) {
+ fun set(v: String): Int {
+ try {
+ return this.set(v.toInt())
+ } catch (e: NumberFormatException) {
+ throw ArgumentsException("Invalid integer $v")
+ }
+ }
+}
+
/**
* Options that can be set from command line arguments.
*/
@@ -113,18 +121,11 @@
var apiListFile: SetOnce<String?> = SetOnce(null),
- var numShards: SetOnce<Int> = SetOnce(1),
- var shard: SetOnce<Int> = SetOnce(0),
+ var numShards: IntSetOnce = IntSetOnce(1),
+ var shard: IntSetOnce = IntSetOnce(0),
) {
companion object {
- private fun String.ensureFileExists(): String {
- if (!File(this).exists()) {
- throw InputFileNotFoundException(this)
- }
- return this
- }
-
private fun parsePackageRedirect(fromColonTo: String): Pair<String, String> {
val colon = fromColonTo.indexOf(':')
if ((colon < 1) || (colon + 1 >= fromColonTo.length)) {
@@ -137,7 +138,7 @@
fun parseArgs(args: Array<String>): HostStubGenOptions {
val ret = HostStubGenOptions()
- val ai = ArgIterator(expandAtFiles(args))
+ val ai = ArgIterator.withAtFiles(args)
var allAnnotations = mutableSetOf<String>()
@@ -148,11 +149,6 @@
return name
}
- fun setLogFile(level: LogLevel, filename: String) {
- log.addFilePrinter(level, filename)
- log.i("$level log file: $filename")
- }
-
while (true) {
val arg = ai.nextArgOptional()
if (arg == null) {
@@ -161,33 +157,23 @@
// Define some shorthands...
fun nextArg(): String = ai.nextArgRequired(arg)
- fun SetOnce<String>.setNextStringArg(): String = nextArg().also { this.set(it) }
- fun SetOnce<String?>.setNextStringArg(): String = nextArg().also { this.set(it) }
fun MutableSet<String>.addUniqueAnnotationArg(): String =
nextArg().also { this += ensureUniqueAnnotation(it) }
- fun SetOnce<Int>.setNextIntArg(): String = nextArg().also {
- try {
- this.set(it.toInt())
- } catch (e: NumberFormatException) {
- throw ArgumentsException("Invalid integer for $arg: $it")
- }
- }
+ if (log.maybeHandleCommandLineArg(arg) { nextArg() }) {
+ continue
+ }
try {
when (arg) {
// TODO: Write help
"-h", "--help" -> TODO("Help is not implemented yet")
- "-v", "--verbose" -> log.setConsoleLogLevel(LogLevel.Verbose)
- "-d", "--debug" -> log.setConsoleLogLevel(LogLevel.Debug)
- "-q", "--quiet" -> log.setConsoleLogLevel(LogLevel.None)
-
- "--in-jar" -> ret.inJar.setNextStringArg().ensureFileExists()
- "--out-stub-jar" -> ret.outStubJar.setNextStringArg()
- "--out-impl-jar" -> ret.outImplJar.setNextStringArg()
+ "--in-jar" -> ret.inJar.set(nextArg()).ensureFileExists()
+ "--out-stub-jar" -> ret.outStubJar.set(nextArg())
+ "--out-impl-jar" -> ret.outImplJar.set(nextArg())
"--policy-override-file" ->
- ret.policyOverrideFile.setNextStringArg().ensureFileExists()
+ ret.policyOverrideFile.set(nextArg())!!.ensureFileExists()
"--clean-up-on-error" -> ret.cleanUpOnError.set(true)
"--no-clean-up-on-error" -> ret.cleanUpOnError.set(false)
@@ -231,19 +217,19 @@
ret.packageRedirects += parsePackageRedirect(nextArg())
"--annotation-allowed-classes-file" ->
- ret.annotationAllowedClassesFile.setNextStringArg()
+ ret.annotationAllowedClassesFile.set(nextArg())
"--default-class-load-hook" ->
- ret.defaultClassLoadHook.setNextStringArg()
+ ret.defaultClassLoadHook.set(nextArg())
"--default-method-call-hook" ->
- ret.defaultMethodCallHook.setNextStringArg()
+ ret.defaultMethodCallHook.set(nextArg())
"--intersect-stub-jar" ->
ret.intersectStubJars += nextArg().ensureFileExists()
"--gen-keep-all-file" ->
- ret.inputJarAsKeepAllFile.setNextStringArg()
+ ret.inputJarAsKeepAllFile.set(nextArg())
// Following options are for debugging.
"--enable-class-checker" -> ret.enableClassChecker.set(true)
@@ -261,16 +247,21 @@
"--no-non-stub-method-check" ->
ret.enableNonStubMethodCallDetection.set(false)
- "--gen-input-dump-file" -> ret.inputJarDumpFile.setNextStringArg()
+ "--gen-input-dump-file" -> ret.inputJarDumpFile.set(nextArg())
- "--verbose-log" -> setLogFile(LogLevel.Verbose, nextArg())
- "--debug-log" -> setLogFile(LogLevel.Debug, nextArg())
+ "--stats-file" -> ret.statsFile.set(nextArg())
+ "--supported-api-list-file" -> ret.apiListFile.set(nextArg())
- "--stats-file" -> ret.statsFile.setNextStringArg()
- "--supported-api-list-file" -> ret.apiListFile.setNextStringArg()
-
- "--num-shards" -> ret.numShards.setNextIntArg()
- "--shard-index" -> ret.shard.setNextIntArg()
+ "--num-shards" -> ret.numShards.set(nextArg()).also {
+ if (it < 1) {
+ throw ArgumentsException("$arg must be positive integer")
+ }
+ }
+ "--shard-index" -> ret.shard.set(nextArg()).also {
+ if (it < 0) {
+ throw ArgumentsException("$arg must be positive integer or zero")
+ }
+ }
else -> throw ArgumentsException("Unknown option: $arg")
}
@@ -286,6 +277,15 @@
log.w("Neither --out-stub-jar nor --out-impl-jar is set." +
" $executableName will not generate jar files.")
}
+ if (ret.numShards.isSet != ret.shard.isSet) {
+ throw ArgumentsException("--num-shards and --shard-index must be used together")
+ }
+
+ if (ret.numShards.isSet) {
+ if (ret.shard.get >= ret.numShards.get) {
+ throw ArgumentsException("--shard-index must be smaller than --num-shards")
+ }
+ }
if (ret.enableNonStubMethodCallDetection.get) {
log.w("--enable-non-stub-method-check is not fully implemented yet." +
@@ -294,87 +294,6 @@
return ret
}
-
- /**
- * Scan the arguments, and if any of them starts with an `@`, then load from the file
- * and use its content as arguments.
- *
- * In this file, each line is treated as a single argument.
- *
- * The file can contain '#' as comments.
- */
- private fun expandAtFiles(args: Array<String>): List<String> {
- val ret = mutableListOf<String>()
-
- args.forEach { arg ->
- if (!arg.startsWith('@')) {
- ret += arg
- return@forEach
- }
- // Read from the file, and add each line to the result.
- val filename = arg.substring(1).ensureFileExists()
-
- log.v("Expanding options file $filename")
-
- BufferedReader(FileReader(filename)).use { reader ->
- while (true) {
- var line = reader.readLine()
- if (line == null) {
- break // EOF
- }
-
- line = normalizeTextLine(line)
- if (line.isNotEmpty()) {
- ret += line
- }
- }
- }
- }
- return ret
- }
- }
-
- open class ArgumentsException(message: String?) : Exception(message), UserErrorException
-
- /** Thrown when the same annotation is used with different annotation arguments. */
- class DuplicateAnnotationException(annotationName: String?) :
- ArgumentsException("Duplicate annotation specified: '$annotationName'")
-
- /** Thrown when an input file does not exist. */
- class InputFileNotFoundException(filename: String) :
- ArgumentsException("File '$filename' not found")
-
- private class ArgIterator(
- private val args: List<String>,
- private var currentIndex: Int = -1
- ) {
- val current: String
- get() = args.get(currentIndex)
-
- /**
- * Get the next argument, or [null] if there's no more arguments.
- */
- fun nextArgOptional(): String? {
- if ((currentIndex + 1) >= args.size) {
- return null
- }
- return args.get(++currentIndex)
- }
-
- /**
- * Get the next argument, or throw if
- */
- fun nextArgRequired(argName: String): String {
- nextArgOptional().let {
- if (it == null) {
- throw ArgumentsException("Missing parameter for option $argName")
- }
- if (it.isEmpty()) {
- throw ArgumentsException("Parameter can't be empty for option $argName")
- }
- return it
- }
- }
}
override fun toString(): String {
@@ -415,3 +334,80 @@
""".trimIndent()
}
}
+
+class ArgIterator(
+ private val args: List<String>,
+ private var currentIndex: Int = -1
+) {
+ val current: String
+ get() = args.get(currentIndex)
+
+ /**
+ * Get the next argument, or [null] if there's no more arguments.
+ */
+ fun nextArgOptional(): String? {
+ if ((currentIndex + 1) >= args.size) {
+ return null
+ }
+ return args.get(++currentIndex)
+ }
+
+ /**
+ * Get the next argument, or throw if
+ */
+ fun nextArgRequired(argName: String): String {
+ nextArgOptional().let {
+ if (it == null) {
+ throw ArgumentsException("Missing parameter for option $argName")
+ }
+ if (it.isEmpty()) {
+ throw ArgumentsException("Parameter can't be empty for option $argName")
+ }
+ return it
+ }
+ }
+
+ companion object {
+ fun withAtFiles(args: Array<String>): ArgIterator {
+ return ArgIterator(expandAtFiles(args))
+ }
+ }
+}
+
+/**
+ * Scan the arguments, and if any of them starts with an `@`, then load from the file
+ * and use its content as arguments.
+ *
+ * In this file, each line is treated as a single argument.
+ *
+ * The file can contain '#' as comments.
+ */
+private fun expandAtFiles(args: Array<String>): List<String> {
+ val ret = mutableListOf<String>()
+
+ args.forEach { arg ->
+ if (!arg.startsWith('@')) {
+ ret += arg
+ return@forEach
+ }
+ // Read from the file, and add each line to the result.
+ val filename = arg.substring(1).ensureFileExists()
+
+ log.v("Expanding options file $filename")
+
+ BufferedReader(FileReader(filename)).use { reader ->
+ while (true) {
+ var line = reader.readLine()
+ if (line == null) {
+ break // EOF
+ }
+
+ line = normalizeTextLine(line)
+ if (line.isNotEmpty()) {
+ ret += line
+ }
+ }
+ }
+ }
+ return ret
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt
index f219dac..6cf2143 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt
@@ -58,7 +58,24 @@
return null
}
-fun findAnnotationValueAsString(an: AnnotationNode, propertyName: String): String? {
+fun ClassNode.findAnyAnnotation(set: Set<String>): AnnotationNode? {
+ return findAnyAnnotation(set, this.visibleAnnotations, this.invisibleAnnotations)
+}
+
+fun MethodNode.findAnyAnnotation(set: Set<String>): AnnotationNode? {
+ return findAnyAnnotation(set, this.visibleAnnotations, this.invisibleAnnotations)
+}
+
+fun FieldNode.findAnyAnnotation(set: Set<String>): AnnotationNode? {
+ return findAnyAnnotation(set, this.visibleAnnotations, this.invisibleAnnotations)
+}
+
+fun <T> findAnnotationValueAsObject(
+ an: AnnotationNode,
+ propertyName: String,
+ expectedTypeHumanReadableName: String,
+ converter: (Any?) -> T?,
+): T? {
for (i in 0..(an.values?.size ?: 0) - 2 step 2) {
val name = an.values[i]
@@ -66,16 +83,30 @@
continue
}
val value = an.values[i + 1]
- if (value is String) {
- return value
+ if (value == null) {
+ return null
}
- throw ClassParseException(
- "The type of '$name' in annotation \"${an.desc}\" must be String" +
- ", but is ${value?.javaClass?.canonicalName}")
+
+ try {
+ return converter(value)
+ } catch (e: ClassCastException) {
+ throw ClassParseException(
+ "The type of '$propertyName' in annotation @${an.desc} must be " +
+ "$expectedTypeHumanReadableName, but is ${value?.javaClass?.canonicalName}")
+ }
}
return null
}
+fun findAnnotationValueAsString(an: AnnotationNode, propertyName: String): String? {
+ return findAnnotationValueAsObject(an, propertyName, "String", {it as String})
+}
+
+fun findAnnotationValueAsType(an: AnnotationNode, propertyName: String): Type? {
+ return findAnnotationValueAsObject(an, propertyName, "Class", {it as Type})
+}
+
+
val periodOrSlash = charArrayOf('.', '/')
fun getPackageNameFromFullClassName(fullClassName: String): String {
@@ -125,6 +156,24 @@
return Pair(name.substring(0, pos), name.substring(pos + 1))
}
+fun String.startsWithAny(vararg prefixes: String): Boolean {
+ prefixes.forEach {
+ if (this.startsWith(it)) {
+ return true
+ }
+ }
+ return false
+}
+
+fun String.endsWithAny(vararg suffixes: String): Boolean {
+ suffixes.forEach {
+ if (this.endsWith(it)) {
+ return true
+ }
+ }
+ return false
+}
+
fun String.toJvmClassName(): String {
return this.replace('.', '/')
}
@@ -137,6 +186,14 @@
return this.replace('/', '.')
}
+fun zipEntryNameToClassName(entryFilename: String): String? {
+ val suffix = ".class"
+ if (!entryFilename.endsWith(suffix)) {
+ return null
+ }
+ return entryFilename.substring(0, entryFilename.length - suffix.length)
+}
+
private val numericalInnerClassName = """.*\$\d+$""".toRegex()
fun isAnonymousInnerClass(cn: ClassNode): Boolean {
@@ -278,6 +335,14 @@
return (this.access and Opcodes.ACC_STATIC) != 0
}
+fun MethodNode.isPublic(): Boolean {
+ return (this.access and Opcodes.ACC_PUBLIC) != 0
+}
+
+fun MethodNode.isSpecial(): Boolean {
+ return CTOR_NAME == this.name || CLASS_INITIALIZER_NAME == this.name
+}
+
fun FieldNode.isEnum(): Boolean {
return (this.access and Opcodes.ACC_ENUM) != 0
}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/ClassNodes.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/ClassNodes.kt
index 2607df6..e2647eb 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/ClassNodes.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/ClassNodes.kt
@@ -27,6 +27,11 @@
import java.io.BufferedInputStream
import java.io.PrintWriter
import java.util.Arrays
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+import java.util.function.Consumer
+import java.util.zip.ZipEntry
import java.util.zip.ZipFile
/**
@@ -183,10 +188,43 @@
/**
* Load all the classes, without code.
*/
- fun loadClassStructures(inJar: String): ClassNodes {
- log.iTime("Reading class structure from $inJar") {
- val allClasses = ClassNodes()
+ fun loadClassStructures(
+ inJar: String,
+ timeCollector: Consumer<Double>? = null,
+ ): ClassNodes {
+ val allClasses = ClassNodes()
+ // Load classes in parallel.
+ val executor = Executors.newFixedThreadPool(4)
+
+ // First exception defected.
+ val exception = AtomicReference<Throwable>()
+
+ // Called on a BG thread. Read a single jar entry and add it to [allClasses].
+ fun parseClass(inZip: ZipFile, entry: ZipEntry) {
+ try {
+ inZip.getInputStream(entry).use { ins ->
+ val cr = ClassReader(BufferedInputStream(ins))
+ val cn = ClassNode()
+ cr.accept(
+ cn, ClassReader.SKIP_CODE
+ or ClassReader.SKIP_DEBUG
+ or ClassReader.SKIP_FRAMES
+ )
+ synchronized(allClasses) {
+ if (!allClasses.addClass(cn)) {
+ log.w("Duplicate class found: ${cn.name}")
+ }
+ }
+ }
+ } catch (e: Throwable) {
+ log.e("Failed to load class: $e")
+ exception.compareAndSet(null, e)
+ }
+ }
+
+ // Actually open the jar and read it on worker threads.
+ val time = log.iTime("Reading class structure from $inJar") {
log.withIndent {
ZipFile(inJar).use { inZip ->
val inEntries = inZip.entries()
@@ -194,40 +232,42 @@
while (inEntries.hasMoreElements()) {
val entry = inEntries.nextElement()
- BufferedInputStream(inZip.getInputStream(entry)).use { bis ->
- if (entry.name.endsWith(".class")) {
- val cr = ClassReader(bis)
- val cn = ClassNode()
- cr.accept(
- cn, ClassReader.SKIP_CODE
- or ClassReader.SKIP_DEBUG
- or ClassReader.SKIP_FRAMES
- )
- if (!allClasses.addClass(cn)) {
- log.w("Duplicate class found: ${cn.name}")
- }
- } else if (entry.name.endsWith(".dex")) {
- // Seems like it's an ART jar file. We can't process it.
- // It's a fatal error.
- throw InvalidJarFileException(
- "$inJar is not a desktop jar file."
- + " It contains a *.dex file."
- )
- } else {
- // Unknown file type. Skip.
- while (bis.available() > 0) {
- bis.skip((1024 * 1024).toLong())
- }
+ if (entry.name.endsWith(".class")) {
+ executor.submit {
+ parseClass(inZip, entry)
}
+ } else if (entry.name.endsWith(".dex")) {
+ // Seems like it's an ART jar file. We can't process it.
+ // It's a fatal error.
+ throw InvalidJarFileException(
+ "$inJar is not a desktop jar file."
+ + " It contains a *.dex file."
+ )
+ } else {
+ // Unknown file type. Skip.
}
}
+ // Wait for all the work to complete. (must do it before closing the zip)
+ log.i("Waiting for all loaders to finish...")
+ executor.shutdown()
+ executor.awaitTermination(5, TimeUnit.MINUTES)
+ log.i("All loaders to finished.")
}
}
+
+ // If any exception is detected, throw it.
+ exception.get()?.let {
+ throw it
+ }
+
if (allClasses.size == 0) {
log.w("$inJar contains no *.class files.")
+ } else {
+ log.i("Loaded ${allClasses.size} classes from $inJar.")
}
- return allClasses
}
+ timeCollector?.accept(time)
+ return allClasses
}
}
}
\ No newline at end of file