summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java6
-rw-r--r--core/api/current.txt47
-rw-r--r--core/api/test-current.txt8
-rw-r--r--core/java/android/app/Notification.java820
-rw-r--r--core/java/android/app/appfunctions/AppFunctionManager.java81
-rw-r--r--core/java/android/app/appfunctions/AppFunctionService.java64
-rw-r--r--core/java/android/app/appfunctions/IAppFunctionManager.aidl5
-rw-r--r--core/java/android/app/appfunctions/IAppFunctionService.aidl4
-rw-r--r--core/java/android/app/appfunctions/ICancellationCallback.aidl24
-rw-r--r--core/java/android/hardware/camera2/CameraManager.java10
-rw-r--r--core/java/android/provider/Settings.java10
-rw-r--r--core/java/android/security/responsible_apis_flags.aconfig11
-rw-r--r--core/java/android/view/WindowLayout.java17
-rw-r--r--core/proto/android/server/vibrator/vibratormanagerservice.proto4
-rw-r--r--core/res/res/values/config.xml5
-rw-r--r--core/res/res/values/dimens.xml6
-rw-r--r--core/res/res/values/symbols.xml4
-rw-r--r--core/tests/coretests/src/android/app/NotificationTest.java303
-rw-r--r--core/tests/coretests/src/android/view/WindowLayoutTests.java15
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java270
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java12
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/Android.bp2
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt341
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java3
-rw-r--r--libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml8
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java30
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IFocusTransitionListener.aidl28
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl6
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt16
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt29
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java142
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java42
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt80
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt138
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java4
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java155
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java12
-rw-r--r--libs/appfunctions/api/current.txt6
-rw-r--r--libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java42
-rw-r--r--libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java37
-rw-r--r--packages/SettingsLib/Android.bp1
-rw-r--r--packages/SettingsLib/CardPreference/Android.bp33
-rw-r--r--packages/SettingsLib/CardPreference/AndroidManifest.xml23
-rw-r--r--packages/SettingsLib/CardPreference/res/layout/settingslib_expressive_preference_card.xml88
-rw-r--r--packages/SettingsLib/CardPreference/res/values/styles_expressive.xml30
-rw-r--r--packages/SettingsLib/CardPreference/src/com/android/settingslib/widget/CardPreference.kt58
-rw-r--r--packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml11
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt3
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt2
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt36
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt80
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java4
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt14
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.aidl19
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.kt60
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt2
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl4
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IGetDeviceSettingsConfigCallback.aidl24
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt91
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatusTest.kt51
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt38
-rw-r--r--packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java1
-rw-r--r--packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java1
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig10
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt24
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt9
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt11
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt17
-rw-r--r--packages/SystemUI/shared/Android.bp1
-rw-r--r--packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java10
-rw-r--r--packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt35
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java31
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarConntectedDisplays.kt61
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt35
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt35
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalism.kt (renamed from packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt)10
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt65
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt33
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java29
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt14
-rw-r--r--services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java31
-rw-r--r--services/core/java/android/os/BatteryStatsInternal.java10
-rw-r--r--services/core/java/com/android/server/StorageManagerService.java4
-rw-r--r--services/core/java/com/android/server/am/BatteryStatsService.java11
-rw-r--r--services/core/java/com/android/server/appop/AppOpsService.java3
-rw-r--r--services/core/java/com/android/server/display/DisplayManagerService.java21
-rw-r--r--services/core/java/com/android/server/flags/pinner.aconfig13
-rw-r--r--services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java7
-rw-r--r--services/core/java/com/android/server/notification/NotificationManagerService.java9
-rw-r--r--services/core/java/com/android/server/notification/NotificationRecord.java56
-rw-r--r--services/core/java/com/android/server/notification/PermissionHelper.java21
-rw-r--r--services/core/java/com/android/server/notification/PreferencesHelper.java11
-rw-r--r--services/core/java/com/android/server/notification/flags.aconfig17
-rw-r--r--services/core/java/com/android/server/pinner/PinRangeSource.java27
-rw-r--r--services/core/java/com/android/server/pinner/PinRangeSourceStatic.java37
-rw-r--r--services/core/java/com/android/server/pinner/PinRangeSourceStream.java43
-rw-r--r--services/core/java/com/android/server/pinner/PinnedFile.java61
-rw-r--r--services/core/java/com/android/server/pinner/PinnerService.java (renamed from services/core/java/com/android/server/PinnerService.java)572
-rw-r--r--services/core/java/com/android/server/pinner/PinnerUtils.java75
-rw-r--r--services/core/java/com/android/server/pm/DexOptHelper.java2
-rw-r--r--services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java6
-rw-r--r--services/core/java/com/android/server/stats/pull/StatsPullAtomService.java4
-rw-r--r--services/core/java/com/android/server/vibrator/VibratorController.java62
-rw-r--r--services/core/java/com/android/server/vibrator/VibratorManagerService.java8
-rw-r--r--services/core/java/com/android/server/webkit/SystemImpl.java8
-rw-r--r--services/core/java/com/android/server/wm/AppCompatCameraOverrides.java2
-rw-r--r--services/core/java/com/android/server/wm/AppCompatCameraPolicy.java5
-rw-r--r--services/core/java/com/android/server/wm/BackgroundActivityStartController.java15
-rw-r--r--services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java2
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerService.java4
-rw-r--r--services/core/java/com/android/server/wm/WindowState.java7
-rw-r--r--services/java/com/android/server/SystemServer.java1
-rw-r--r--services/tests/powerstatstests/res/xml/irq_device_map_3.xml3
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java87
-rw-r--r--services/tests/servicestests/Android.bp2
-rw-r--r--services/tests/servicestests/src/com/android/server/PinnerServiceTest.java138
-rw-r--r--services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java26
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java37
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java84
-rw-r--r--services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java42
-rw-r--r--services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java23
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java10
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java18
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java14
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java8
140 files changed, 4712 insertions, 996 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 0f3b1c366fb0..033da2df9bf6 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -4944,10 +4944,14 @@ public class AlarmManagerService extends SystemService {
@Override
public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (action == null) {
+ return;
+ }
final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
synchronized (mLock) {
String pkgList[] = null;
- switch (intent.getAction()) {
+ switch (action) {
case Intent.ACTION_QUERY_PACKAGE_RESTART:
pkgList = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES);
for (String packageName : pkgList) {
diff --git a/core/api/current.txt b/core/api/current.txt
index 8eb881139b34..836d4e99ce45 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -6854,6 +6854,47 @@ package android.app {
method public android.app.Notification.MessagingStyle.Message setData(String, android.net.Uri);
}
+ @FlaggedApi("android.app.api_rich_ongoing") public static class Notification.ProgressStyle extends android.app.Notification.Style {
+ ctor public Notification.ProgressStyle();
+ method @NonNull public android.app.Notification.ProgressStyle addProgressSegment(@NonNull android.app.Notification.ProgressStyle.Segment);
+ method @NonNull public android.app.Notification.ProgressStyle addProgressStep(@NonNull android.app.Notification.ProgressStyle.Step);
+ method public int getProgress();
+ method @Nullable public android.graphics.drawable.Icon getProgressEndIcon();
+ method public int getProgressMax();
+ method @NonNull public java.util.List<android.app.Notification.ProgressStyle.Segment> getProgressSegments();
+ method @Nullable public android.graphics.drawable.Icon getProgressStartIcon();
+ method @NonNull public java.util.List<android.app.Notification.ProgressStyle.Step> getProgressSteps();
+ method @Nullable public android.graphics.drawable.Icon getProgressTrackerIcon();
+ method public boolean isProgressIndeterminate();
+ method public boolean isStyledByProgress();
+ method @NonNull public android.app.Notification.ProgressStyle setProgress(int);
+ method @NonNull public android.app.Notification.ProgressStyle setProgressEndIcon(@Nullable android.graphics.drawable.Icon);
+ method @NonNull public android.app.Notification.ProgressStyle setProgressIndeterminate(boolean);
+ method @NonNull public android.app.Notification.ProgressStyle setProgressSegments(@NonNull java.util.List<android.app.Notification.ProgressStyle.Segment>);
+ method @NonNull public android.app.Notification.ProgressStyle setProgressStartIcon(@Nullable android.graphics.drawable.Icon);
+ method @NonNull public android.app.Notification.ProgressStyle setProgressSteps(@NonNull java.util.List<android.app.Notification.ProgressStyle.Step>);
+ method @NonNull public android.app.Notification.ProgressStyle setProgressTrackerIcon(@Nullable android.graphics.drawable.Icon);
+ method @NonNull public android.app.Notification.ProgressStyle setStyledByProgress(boolean);
+ }
+
+ public static final class Notification.ProgressStyle.Segment {
+ ctor public Notification.ProgressStyle.Segment(int);
+ method @ColorInt public int getColor();
+ method public int getLength();
+ method public int getStableId();
+ method @NonNull public android.app.Notification.ProgressStyle.Segment setColor(@ColorInt int);
+ method @NonNull public android.app.Notification.ProgressStyle.Segment setStableId(int);
+ }
+
+ public static final class Notification.ProgressStyle.Step {
+ ctor public Notification.ProgressStyle.Step(int);
+ method @ColorInt public int getColor();
+ method public int getPosition();
+ method public int getStableId();
+ method @NonNull public android.app.Notification.ProgressStyle.Step setColor(@ColorInt int);
+ method @NonNull public android.app.Notification.ProgressStyle.Step setStableId(int);
+ }
+
public abstract static class Notification.Style {
ctor @Deprecated public Notification.Style();
method public android.app.Notification build();
@@ -8732,13 +8773,15 @@ package android.app.admin {
package android.app.appfunctions {
@FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class AppFunctionManager {
- method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
+ method @Deprecated @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
+ method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
}
@FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public abstract class AppFunctionService extends android.app.Service {
ctor public AppFunctionService();
method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
- method @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
+ method @Deprecated @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
+ method @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
}
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 6511c214ee52..0a10920154b8 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1614,15 +1614,15 @@ package android.hardware.camera2 {
public final class CameraManager {
method @NonNull public android.hardware.camera2.CameraCharacteristics getCameraCharacteristics(@NonNull String, boolean) throws android.hardware.camera2.CameraAccessException;
method public String[] getCameraIdListNoLazy() throws android.hardware.camera2.CameraAccessException;
- method @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static int getRotationOverrideInternal(@Nullable android.content.Context, @Nullable android.content.pm.PackageManager, @Nullable String);
+ method @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static int getRotationOverrideInternal(@Nullable android.content.Context, @Nullable android.content.pm.PackageManager, @Nullable String);
method @RequiresPermission(android.Manifest.permission.CAMERA) public void openCamera(@NonNull String, boolean, @Nullable android.os.Handler, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException;
method @RequiresPermission(allOf={android.Manifest.permission.SYSTEM_CAMERA, android.Manifest.permission.CAMERA}) public void openCamera(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException;
method public static boolean shouldOverrideToPortrait(@Nullable android.content.pm.PackageManager, @Nullable String);
field public static final String LANDSCAPE_TO_PORTRAIT_PROP = "camera.enable_landscape_to_portrait";
field public static final long OVERRIDE_CAMERA_LANDSCAPE_TO_PORTRAIT = 250678880L; // 0xef10e60L
- field @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static final int ROTATION_OVERRIDE_NONE = 0; // 0x0
- field @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static final int ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT = 1; // 0x1
- field @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static final int ROTATION_OVERRIDE_ROTATION_ONLY = 2; // 0x2
+ field @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static final int ROTATION_OVERRIDE_NONE = 0; // 0x0
+ field @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static final int ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT = 1; // 0x1
+ field @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static final int ROTATION_OVERRIDE_ROTATION_ONLY = 2; // 0x2
}
public abstract static class CameraManager.AvailabilityCallback {
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 392a1f113c23..a39f216d033e 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -121,7 +121,6 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@@ -783,10 +782,32 @@ public class Notification implements Parcelable
@FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
public static final int FLAG_PROMOTED_ONGOING = 0x00040000;
- private static final List<Class<? extends Style>> PLATFORM_STYLE_CLASSES = Arrays.asList(
- BigTextStyle.class, BigPictureStyle.class, InboxStyle.class, MediaStyle.class,
- DecoratedCustomViewStyle.class, DecoratedMediaCustomViewStyle.class,
- MessagingStyle.class, CallStyle.class);
+ private static final Set<Class<? extends Style>> PLATFORM_STYLE_CLASSES = Set.of(
+ BigTextStyle.class,
+ BigPictureStyle.class,
+ InboxStyle.class,
+ MediaStyle.class,
+ DecoratedCustomViewStyle.class,
+ DecoratedMediaCustomViewStyle.class,
+ MessagingStyle.class,
+ CallStyle.class
+ );
+
+ private static boolean isPlatformStyle(Style style) {
+ if (style == null) {
+ return false;
+ }
+
+ if (PLATFORM_STYLE_CLASSES.contains(style.getClass())) {
+ return true;
+ }
+
+ if (Flags.apiRichOngoing()) {
+ return style.getClass() == ProgressStyle.class;
+ }
+
+ return false;
+ }
/** @hide */
@IntDef(flag = true, prefix = {"FLAG_"}, value = {
@@ -1620,6 +1641,66 @@ public class Notification implements Parcelable
public static final String EXTRA_COLORIZED = "android.colorized";
/**
+ * {@link #extras} key: an arraylist of {@link android.app.Notification.ProgressStyle.Segment}
+ * bundles provided by a
+ * {@link android.app.Notification.ProgressStyle} notification as supplied to
+ * {@link ProgressStyle#setProgressSegments}
+ * or {@link ProgressStyle#addProgressSegment(ProgressStyle.Segment)}.
+ * This extra is a parcelable array list of bundles.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+ public static final String EXTRA_PROGRESS_SEGMENTS = "android.progressSegments";
+
+ /**
+ * {@link #extras} key: an arraylist of {@link android.app.Notification.ProgressStyle.Step}
+ * bundles provided by a
+ * {@link android.app.Notification.ProgressStyle} notification as supplied to
+ * {@link ProgressStyle#setProgressSteps}
+ * or {@link ProgressStyle#addProgressStep(ProgressStyle.Step)}.
+ * This extra is a parcelable array list of bundles.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+ public static final String EXTRA_PROGRESS_STEPS = "android.progressSteps";
+
+ /**
+ * {@link #extras} key: whether the progress bar should be styled by its progress as
+ * supplied to {@link ProgressStyle#setStyledByProgress}.
+ * This extra is a boolean.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+ public static final String EXTRA_STYLED_BY_PROGRESS = "android.styledByProgress";
+
+ /**
+ * {@link #extras} key: this is an {@link Icon} of an image to be
+ * shown as progress bar progress tracker icon in {@link ProgressStyle}, supplied to
+ *{@link ProgressStyle#setProgressTrackerIcon(Icon)}.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+ public static final String EXTRA_PROGRESS_TRACKER_ICON = "android.progressTrackerIcon";
+
+ /**
+ * {@link #extras} key: this is an {@link Icon} of an image to be
+ * shown at the beginning of the progress bar in {@link ProgressStyle}, supplied to
+ *{@link ProgressStyle#setProgressStartIcon(Icon)}.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+ public static final String EXTRA_PROGRESS_START_ICON = "android.progressStartIcon";
+
+ /**
+ * {@link #extras} key: this is an {@link Icon} of an image to be
+ * shown at the end of the progress bar in {@link ProgressStyle}, supplied to
+ *{@link ProgressStyle#setProgressEndIcon(Icon)}.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+ public static final String EXTRA_PROGRESS_END_ICON = "android.progressEndIcon";
+
+ /**
* @hide
*/
public static final String EXTRA_BUILDER_APPLICATION_INFO = "android.appInfo";
@@ -3072,6 +3153,9 @@ public class Notification implements Parcelable
if (Flags.apiRichOngoing()) {
visitIconUri(visitor, extras.getParcelable(EXTRA_ENROUTE_OVERLAY_ICON, Icon.class));
+ visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_TRACKER_ICON, Icon.class));
+ visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_START_ICON, Icon.class));
+ visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_END_ICON, Icon.class));
}
if (mBubbleMetadata != null) {
@@ -6630,7 +6714,7 @@ public class Notification implements Parcelable
// Custom views which come from a platform style class are safe, and thus do not need to
// be wrapped. Any subclass of those styles has the opportunity to make arbitrary
// changes to the RemoteViews, and thus can't be trusted as a fully vetted view.
- if (fromStyle && PLATFORM_STYLE_CLASSES.contains(mStyle.getClass())) {
+ if (fromStyle && isPlatformStyle(mStyle)) {
return false;
}
return mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.S;
@@ -7971,6 +8055,12 @@ public class Notification implements Parcelable
return innerClass;
}
}
+
+ if (Flags.apiRichOngoing()) {
+ if (templateClass.equals(ProgressStyle.class.getName())) {
+ return ProgressStyle.class;
+ }
+ }
return null;
}
@@ -11220,6 +11310,724 @@ public class Notification implements Parcelable
}
}
+
+ /**
+ * A Notification Style used to to define a notification whose expanded state includes
+ * a highly customizable progress bar with segments, steps, a custom tracker icon,
+ * and custom icons at the start and end of the progress bar.
+ *
+ * This style is suggested for use cases where the app is showing a tracker to the
+ * user of a thing they are interested in: the location of a car on its way
+ * to pick them up, food being delivered, or their own progress in a navigation
+ * journey.
+ *
+ * To use this style with your Notification, feed it to
+ * {@link Notification.Builder#setStyle(android.app.Notification.Style)} like so:
+ * <pre class="prettyprint">
+ * new Notification.Builder(context)
+ * .setSmallIcon(R.drawable.ic_notification)
+ * .setColor(Color.GREEN)
+ * .setColorized(true)
+ * .setContentTitle("Arrive 10:08 AM").
+ * .setContentText("Dominique Ansel Bakery Soho")
+ * .addAction(new Notification.Action("Exit navigation",...))
+ * .setStyle(new Notification.ProgressStyle()
+ * .setStyledByProgress(false)
+ * .setProgress(456)
+ * .setProgressTrackerIcon(Icon.createWithResource(R.drawable.ic_driving_tracker))
+ * .addProgressSegment(new Segment(41).setColor(Color.BLACK))
+ * .addProgressSegment(new Segment(552).setColor(Color.YELLOW))
+ * .addProgressSegment(new Segment(253).setColor(Color.YELLOW))
+ * .addProgressSegment(new Segment(94).setColor(Color.BLUE))
+ * .addProgressStep(new Step(60).setColor(Color.RED))
+ * .addProgressStep(new Step(560).setColor(Color.YELLOW))
+ * )
+ * </pre>
+ *
+ *
+ *
+ * NOTE: The progress bar layout will be mirrored for RTL layout.
+ * NOTE: The extras set by {@link Notification.Builder#setProgress} will be overridden by
+ * the values set on this style object when the notification is built.
+ *
+ */
+ @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+ public static class ProgressStyle extends Notification.Style {
+ private static final String KEY_ELEMENT_STABLE_ID = "stableId";
+ private static final String KEY_ELEMENT_COLOR = "colorInt";
+ private static final String KEY_SEGMENT_LENGTH = "length";
+ private static final String KEY_STEP_POSITION = "position";
+
+ private static final int MAX_PROGRESS_SEGMENT_LIMIT = 15;
+ private static final int MAX_PROGRESS_STEP_LIMIT = 5;
+ private static final int DEFAULT_PROGRESS_MAX = 100;
+
+ private List<Segment> mProgressSegments = new ArrayList<>();
+ private List<Step> mProgressSteps = new ArrayList<>();
+
+ private int mProgress = 0;
+
+ private boolean mIndeterminate;
+
+ private boolean mIsStyledByProgress = true;
+
+ @Nullable
+ private Icon mTrackerIcon;
+ @Nullable
+ private Icon mStartIcon;
+ @Nullable
+ private Icon mEndIcon;
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean areNotificationsVisiblyDifferent(Style other) {
+ if (other == null || getClass() != other.getClass()) {
+ return true;
+ }
+
+ final ProgressStyle progressStyle = (ProgressStyle) other;
+
+ /**
+ * @see #setProgressIndeterminate
+ */
+ if (!Objects.equals(mIndeterminate, progressStyle.mIndeterminate)) {
+ return true;
+ }
+ boolean nonIndeterminateCheckResult = false;
+ if (!mIndeterminate) {
+ nonIndeterminateCheckResult = !Objects.equals(mProgress, progressStyle.mProgress)
+ || !Objects.equals(mIsStyledByProgress, progressStyle.mIsStyledByProgress)
+ || !Objects.equals(mProgressSegments, progressStyle.mProgressSegments)
+ || !Objects.equals(mProgressSteps, progressStyle.mProgressSteps)
+ || !Objects.equals(mTrackerIcon, progressStyle.mTrackerIcon);
+ }
+
+ return !Objects.equals(mStartIcon, progressStyle.mStartIcon)
+ || !Objects.equals(mEndIcon, progressStyle.mEndIcon)
+ || nonIndeterminateCheckResult;
+ }
+
+ /**
+ * Gets the segments that define the background layer of the progress bar.
+ *
+ * If no segments are provided, the progress bar will be rendered with a single segment
+ * with length 100 and default color.
+ *
+ * @see #setProgressSegments
+ * @see #addProgressSegment
+ * @see Segment
+ */
+ public @NonNull List<Segment> getProgressSegments() {
+ return mProgressSegments;
+ }
+
+ /**
+ * Sets or replaces the segments of the progress bar.
+ *
+ * Segments allow for creating progress bars with multiple colors or sections
+ * to represent different stages or categories of progress.
+ * For example, Traffic conditions along a navigation journey.
+ * @see Segment
+ */
+ public @NonNull ProgressStyle setProgressSegments(@NonNull List<Segment> progressSegments) {
+ mProgressSegments = new ArrayList<>(progressSegments.size());
+ return this;
+ }
+
+ /**
+ * Appends a segment to the end of the progress bar.
+ *
+ * Segments allow for creating progress bars with multiple colors or sections
+ * to represent different stages or categories of progress.
+ * For example, Traffic conditions along a navigation journey.
+ * @see Segment
+ */
+ public @NonNull ProgressStyle addProgressSegment(@NonNull Segment segment) {
+ if (mProgressSegments == null) {
+ mProgressSegments = new ArrayList<>();
+ }
+ mProgressSegments.add(segment);
+
+ return this;
+ }
+
+ /**
+ * Gets the steps that are displayed on the progress bar.
+ *.
+ * @see #setProgressSteps
+ * @see #addProgressStep
+ * @see Step
+ */
+ public @NonNull List<Step> getProgressSteps() {
+ return mProgressSteps;
+ }
+
+ /**
+ * Replaces all the progress steps.
+ *
+ * Steps are designated points within a progressbar to visualize
+ * distinct stages or milestones.
+ * For example, you might use steps to mark stops in a multi-stop
+ * navigation journey, where each step represents a destination.
+ * @see Step
+ */
+ public @NonNull ProgressStyle setProgressSteps(@NonNull List<Step> steps) {
+ mProgressSteps = new ArrayList<>(steps);
+ return this;
+ }
+
+ /**
+ * Adds another step.
+ *
+ * Steps are designated points within a progressbar to visualize
+ * distinct stages or milestones.
+ * For example, you might use steps to mark stops in a multi-stop
+ * navigation journey, where each step represents a destination.
+ *
+ * Steps can be added in any order, as their
+ * position within the progress bar is determined by their individual
+ * {@link Step#getPosition()}.
+ * @see Step
+ */
+ public @NonNull ProgressStyle addProgressStep(@NonNull Step step) {
+ if (mProgressSteps == null) {
+ mProgressSteps = new ArrayList<>();
+ }
+ mProgressSteps.add(step);
+
+ return this;
+ }
+
+ /**
+ * Gets the progress value of the progress bar.
+ * @see #setProgress
+ */
+ public int getProgress() {
+ return mProgress;
+ }
+
+ /**
+ * Specifies the progress (in the same units as {@link Segment#getLength()})
+ * of the tracker along the length of the bar.
+ *
+ * The max progress value is the sum of all Segment lengths.
+ * The default value is 0.
+ */
+ public @NonNull ProgressStyle setProgress(int progress) {
+ mProgress = progress;
+ return this;
+ }
+
+ /**
+ * Gets the sum of the lengths of all Segments in the style, which
+ * defines the maximum progress. Defaults to 100 when segments are omitted.
+ */
+ public int getProgressMax() {
+ final List<Segment> progressSegment = mProgressSegments;
+ if (progressSegment == null || progressSegment.isEmpty()) {
+ return DEFAULT_PROGRESS_MAX;
+ } else {
+ int progressMax = 0;
+ int validSegmentCount = 0;
+ for (int i = 0; i < progressSegment.size()
+ && validSegmentCount < MAX_PROGRESS_SEGMENT_LIMIT; i++) {
+ int segmentLength = progressSegment.get(i).getLength();
+ if (segmentLength > 0) {
+ try {
+ progressMax = Math.addExact(progressMax, segmentLength);
+ validSegmentCount++;
+ } catch (ArithmeticException e) {
+ Log.e(TAG,
+ "Notification.ProgressStyle segment total overflowed.", e);
+ return DEFAULT_PROGRESS_MAX;
+ }
+ }
+ }
+
+ if (validSegmentCount == 0) {
+ return DEFAULT_PROGRESS_MAX;
+ }
+
+ return progressMax;
+ }
+
+ }
+
+ /**
+ * Get indeterminate value of the progress bar.
+ * @see #setProgressIndeterminate
+ */
+ public boolean isProgressIndeterminate() {
+ return mIndeterminate;
+ }
+
+ /**
+ * Used to indicate an initialization state without a known progress amount.
+ * When specified, the following fields are ignored:
+ * @see #setProgress
+ * @see #setProgressSegments
+ * @see #setProgressSteps
+ * @see #setProgressTrackerIcon
+ * @see #setStyledByProgress
+ *
+ * If the app provides exactly one Segment, that segment's color will be
+ * used to style the indeterminate bar.
+ */
+ public @NonNull ProgressStyle setProgressIndeterminate(boolean indeterminate) {
+ mIndeterminate = indeterminate;
+ return this;
+ }
+
+ /**
+ * Gets whether the progress bar's style is based on its progress.
+ * @see #setStyledByProgress
+ */
+ public boolean isStyledByProgress() {
+ return mIsStyledByProgress;
+ }
+
+ /**
+ * Indicates whether the segments and steps will be styled differently
+ * based on whether they are behind or ahead of the current progress.
+ * When true, segments appearing ahead of the current progress will be given a
+ * slightly different appearance to indicate that it is part of the progress bar
+ * that is not "filled".
+ * When false, all segments will be given the filled appearance, and it will be
+ * the app's responsibility to use #setProgressTrackerIcon or segment colors
+ * to make the current progress clear to the user.
+ * the default value is true.
+ */
+ public @NonNull ProgressStyle setStyledByProgress(boolean enabled) {
+ mIsStyledByProgress = enabled;
+ return this;
+ }
+
+
+ /**
+ * Gets the progress tracker icon for the progress bar.
+ * @see #setProgressTrackerIcon
+ */
+ public @Nullable Icon getProgressTrackerIcon() {
+ return mTrackerIcon;
+ }
+
+ /**
+ * An optional icon that can appear as an overlay on the bar at the point of
+ * current progress.
+ * Aspect ratio may be anywhere from 2:1 to 1:2; content outside that
+ * aspect ratio range will be cropped.
+ * This icon will be mirrored in RTL.
+ */
+ public @NonNull ProgressStyle setProgressTrackerIcon(@Nullable Icon trackerIcon) {
+ mTrackerIcon = trackerIcon;
+ return this;
+ }
+
+ /**
+ * Gets the progress bar start icon.
+ * @see #setProgressStartIcon
+ */
+ public @Nullable Icon getProgressStartIcon() {
+ return mStartIcon;
+ }
+
+ /**
+ * An optional square icon that appears at the start of the progress bar.
+ * This icon will be cropped to its central square.
+ * This icon will NOT be mirrored in RTL layouts.
+ */
+ public @NonNull ProgressStyle setProgressStartIcon(@Nullable Icon startIcon) {
+ mStartIcon = startIcon;
+ return this;
+ }
+
+ /**
+ * Gets the progress bar end icon.
+ * @see #setProgressEndIcon(Icon)
+ */
+ public @Nullable Icon getProgressEndIcon() {
+ return mEndIcon;
+ }
+
+ /**
+ * An optional square icon that appears at the end of the progress bar.
+ * This icon will be cropped to its central square.
+ * This icon will NOT be mirrored in RTL layouts.
+ */
+ public @NonNull ProgressStyle setProgressEndIcon(@Nullable Icon endIcon) {
+ mEndIcon = endIcon;
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void purgeResources() {
+ super.purgeResources();
+ if (mTrackerIcon != null) {
+ mTrackerIcon.convertToAshmem();
+ }
+ if (mStartIcon != null) {
+ mStartIcon.convertToAshmem();
+ }
+ if (mEndIcon != null) {
+ mEndIcon.convertToAshmem();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void reduceImageSizes(Context context) {
+ super.reduceImageSizes(context);
+
+ final Resources resources = context.getResources();
+
+ int progressIconSize =
+ resources.getDimensionPixelSize(R.dimen.notification_progress_icon_size);
+ if (mStartIcon != null) {
+ mStartIcon.scaleDownIfNecessary(progressIconSize, progressIconSize);
+ }
+ if (mEndIcon != null) {
+ mEndIcon.scaleDownIfNecessary(progressIconSize, progressIconSize);
+ }
+ if (mTrackerIcon != null) {
+ int progressTrackerWidth = resources.getDimensionPixelSize(
+ R.dimen.notification_progress_tracker_width);
+ int progressTrackerHeight = resources.getDimensionPixelSize(
+ R.dimen.notification_progress_tracker_height);
+ mTrackerIcon.scaleDownIfNecessary(progressTrackerWidth, progressTrackerHeight);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void addExtras(Bundle extras) {
+ super.addExtras(extras);
+ extras.putParcelableArrayList(EXTRA_PROGRESS_SEGMENTS,
+ getProgressSegmentsAsBundleList(mProgressSegments));
+ extras.putParcelableArrayList(EXTRA_PROGRESS_STEPS,
+ getProgressStepsAsBundleList(mProgressSteps));
+
+ extras.putInt(EXTRA_PROGRESS, mProgress);
+ extras.putBoolean(EXTRA_PROGRESS_INDETERMINATE, mIndeterminate);
+ extras.putInt(EXTRA_PROGRESS_MAX, getProgressMax());
+ extras.putBoolean(EXTRA_STYLED_BY_PROGRESS, mIsStyledByProgress);
+
+ if (mTrackerIcon != null) {
+ extras.putParcelable(EXTRA_PROGRESS_TRACKER_ICON, mTrackerIcon);
+ } else {
+ extras.remove(EXTRA_PROGRESS_TRACKER_ICON);
+ }
+
+ if (mStartIcon != null) {
+ extras.putParcelable(EXTRA_PROGRESS_START_ICON, mStartIcon);
+ } else {
+ extras.remove(EXTRA_PROGRESS_START_ICON);
+ }
+
+ if (mEndIcon != null) {
+ extras.putParcelable(EXTRA_PROGRESS_END_ICON, mEndIcon);
+ } else {
+ extras.remove(EXTRA_PROGRESS_END_ICON);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ protected void restoreFromExtras(Bundle extras) {
+ super.restoreFromExtras(extras);
+ mProgressSegments = getProgressSegmentsFromBundleList(
+ extras.getParcelableArrayList(EXTRA_PROGRESS_SEGMENTS, Bundle.class));
+ mProgress = extras.getInt(EXTRA_PROGRESS, 0);
+ mIndeterminate = extras.getBoolean(EXTRA_PROGRESS_INDETERMINATE, false);
+ mIsStyledByProgress = extras.getBoolean(EXTRA_STYLED_BY_PROGRESS, true);
+ mTrackerIcon = extras.getParcelable(EXTRA_PROGRESS_TRACKER_ICON, Icon.class);
+ mStartIcon = extras.getParcelable(EXTRA_PROGRESS_START_ICON, Icon.class);
+ mEndIcon = extras.getParcelable(EXTRA_PROGRESS_END_ICON, Icon.class);
+ mProgressSteps = getProgressStepsFromBundleList(
+ extras.getParcelableArrayList(EXTRA_PROGRESS_STEPS, Bundle.class));
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean displayCustomViewInline() {
+ // This is a lie; True is returned for progress notifications to make sure
+ // that the custom view is not used instead of the template, but it will not
+ // actually be included.
+ return true;
+ }
+
+ private static @NonNull ArrayList<Bundle> getProgressSegmentsAsBundleList(
+ @Nullable List<Segment> progressSegments) {
+ final ArrayList<Bundle> segments = new ArrayList<>();
+ if (progressSegments != null && !progressSegments.isEmpty()) {
+ for (int i = 0; i < progressSegments.size(); i++) {
+ final Segment segment = progressSegments.get(i);
+ if (segment.getLength() <= 0) {
+ continue;
+ }
+
+ final Bundle bundle = new Bundle();
+ bundle.putInt(KEY_SEGMENT_LENGTH, segment.getLength());
+ bundle.putInt(KEY_ELEMENT_STABLE_ID, segment.getStableId());
+ bundle.putInt(KEY_ELEMENT_COLOR, segment.getColor());
+
+ segments.add(bundle);
+ }
+ }
+
+ return segments;
+ }
+
+ private static @NonNull List<Segment> getProgressSegmentsFromBundleList(
+ @Nullable List<Bundle> segmentBundleList) {
+ final ArrayList<Segment> segments = new ArrayList<>();
+ if (segmentBundleList != null && !segmentBundleList.isEmpty()) {
+ for (int i = 0; i < segmentBundleList.size(); i++) {
+ final Bundle segmentBundle = segmentBundleList.get(i);
+ final int length = segmentBundle.getInt(KEY_SEGMENT_LENGTH);
+ if (length <= 0) {
+ continue;
+ }
+
+ final int stableId = segmentBundle.getInt(KEY_ELEMENT_STABLE_ID);
+ final int color = segmentBundle.getInt(KEY_ELEMENT_COLOR,
+ Notification.COLOR_DEFAULT);
+ final Segment segment = new Segment(length)
+ .setStableId(stableId).setColor(color);
+
+ segments.add(segment);
+ }
+ }
+
+ return segments;
+ }
+
+ private static @NonNull ArrayList<Bundle> getProgressStepsAsBundleList(
+ @Nullable List<Step> progressSteps) {
+ final ArrayList<Bundle> steps = new ArrayList<>();
+ if (progressSteps != null && !progressSteps.isEmpty()) {
+ for (int i = 0; i < progressSteps.size(); i++) {
+ final Step step = progressSteps.get(i);
+ if (step.getPosition() < 0) {
+ continue;
+ }
+
+ final Bundle bundle = new Bundle();
+ bundle.putInt(KEY_STEP_POSITION, step.getPosition());
+ bundle.putInt(KEY_ELEMENT_STABLE_ID, step.getStableId());
+ bundle.putInt(KEY_ELEMENT_COLOR, step.getColor());
+
+ steps.add(bundle);
+ }
+ }
+
+ return steps;
+ }
+
+ private static @NonNull List<Step> getProgressStepsFromBundleList(
+ @Nullable List<Bundle> stepBundleList) {
+ final ArrayList<Step> steps = new ArrayList<>();
+
+ if (stepBundleList != null && !stepBundleList.isEmpty()) {
+ for (int i = 0; i < stepBundleList.size(); i++) {
+ final Bundle segmentBundle = stepBundleList.get(i);
+ final int position = segmentBundle.getInt(KEY_STEP_POSITION);
+ if (position < 0) {
+ continue;
+ }
+ final int stableId = segmentBundle.getInt(KEY_ELEMENT_STABLE_ID);
+ final int color = segmentBundle.getInt(KEY_ELEMENT_COLOR,
+ Notification.COLOR_DEFAULT);
+ final Step step = new Step(position).setStableId(stableId).setColor(color);
+ steps.add(step);
+ }
+ }
+
+ return steps;
+ }
+
+ /**
+ * A segment of the progress bar, which defines its length and color.
+ * Segments allow for creating progress bars with multiple colors or sections
+ * to represent different stages or categories of progress.
+ * For example, Traffic conditions along a navigation journey.
+ */
+ public static final class Segment {
+ private int mLength;
+ private int mStableId = 0;
+ @ColorInt
+ private int mColor = Notification.COLOR_DEFAULT;
+
+ /**
+ * Create a segment with a non-zero length.
+ * @param length
+ * See {@link #getLength}
+ */
+ public Segment(int length) {
+ mLength = length;
+ }
+
+ /**
+ * The length of this Segment within the progress bar.
+ * This value has no units, it is just relative to the length of other segments,
+ * and the value provided to {@link ProgressStyle#setProgress}.
+ */
+ public int getLength() {
+ return mLength;
+ }
+
+ /**
+ * Gets the stable id of this Segment.
+ *
+ * @see #setStableId
+ */
+ public int getStableId() {
+ return mStableId;
+ }
+
+ /**
+ * Optional ID used to uniquely identify the element across updates.
+ */
+ public @NonNull Segment setStableId(int stableId) {
+ mStableId = stableId;
+ return this;
+ }
+
+ /**
+ * Returns the color of this Segment.
+ *
+ * @see #setColor
+ */
+ @ColorInt
+ public int getColor() {
+ return mColor;
+ }
+
+ /**
+ * Optional color of this Segment
+ */
+ public @NonNull Segment setColor(@ColorInt int color) {
+ mColor = color;
+ return this;
+ }
+
+ /**
+ * Needed for {@link Notification.Style#areNotificationsVisiblyDifferent}
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Segment segment = (Segment) o;
+ return mLength == segment.mLength && mStableId == segment.mStableId
+ && mColor == segment.mColor;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mLength, mStableId, mColor);
+ }
+ }
+
+ /**
+ * A step within the progress bar, defining its position and color.
+ * Steps are designated points within a progressbar to visualize
+ * distinct stages or milestones.
+ * For example, you might use steps to mark stops in a multi-stop
+ * navigation journey, where each step represents a destination.
+ */
+ public static final class Step {
+
+ private int mPosition;
+ private int mStableId;
+ @ColorInt
+ private int mColor = Notification.COLOR_DEFAULT;
+
+ /**
+ * Create a step element.
+ * The position of this step on the progress bar
+ * relative to {@link ProgressStyle#getProgressMax}
+ * @param position
+ * See {@link #getPosition}
+ */
+ public Step(int position) {
+ mPosition = position;
+ }
+
+ /**
+ * Gets the position of this Step.
+ * The position of this step on the progress bar
+ * relative to {@link ProgressStyle#getProgressMax}.
+ */
+ public int getPosition() {
+ return mPosition;
+ }
+
+
+ /**
+ * Optional ID used to uniqurely identify the element across updates.
+ */
+ public int getStableId() {
+ return mStableId;
+ }
+
+ /**
+ * Optional ID used to uniqurely identify the element across updates.
+ */
+ public @NonNull Step setStableId(int stableId) {
+ mStableId = stableId;
+ return this;
+ }
+
+ /**
+ * Returns the color of this Segment.
+ *
+ * @see #setColor
+ */
+ @ColorInt
+ public int getColor() {
+ return mColor;
+ }
+
+ /**
+ * Optional color of this Segment
+ */
+ public @NonNull Step setColor(@ColorInt int color) {
+ mColor = color;
+ return this;
+ }
+
+ /**
+ * Needed for {@link Notification.Style#areNotificationsVisiblyDifferent}
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Step step = (Step) o;
+ return mPosition == step.mPosition && mStableId == step.mStableId
+ && mColor == step.mColor;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mPosition, mStableId, mColor);
+ }
+ }
+ }
+
/**
* Notification style for custom views that are decorated by the system
*
diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java
index 4682f3d30e1e..216ba5d994ec 100644
--- a/core/java/android/app/appfunctions/AppFunctionManager.java
+++ b/core/java/android/app/appfunctions/AppFunctionManager.java
@@ -27,6 +27,8 @@ import android.annotation.RequiresPermission;
import android.annotation.SystemService;
import android.annotation.UserHandleAware;
import android.content.Context;
+import android.os.CancellationSignal;
+import android.os.ICancellationSignal;
import android.os.RemoteException;
import java.util.Objects;
@@ -73,7 +75,43 @@ public final class AppFunctionManager {
* android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code
* android.permission.EXECUTE_APP_FUNCTIONS}, the execution result will contain {@code
* ExecuteAppFunctionResponse.RESULT_DENIED}.
+ * @deprecated Use {@link #executeAppFunction(ExecuteAppFunctionRequest, Executor,
+ * CancellationSignal, Consumer)} instead. This method will be removed once usage references
+ * are updated.
*/
+ @RequiresPermission(
+ anyOf = {
+ Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED,
+ Manifest.permission.EXECUTE_APP_FUNCTIONS
+ },
+ conditional = true)
+ @UserHandleAware
+ @Deprecated
+ public void executeAppFunction(
+ @NonNull ExecuteAppFunctionRequest request,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
+ executeAppFunction(request, executor, new CancellationSignal(), callback);
+ }
+
+ /**
+ * Executes the app function.
+ *
+ * <p>Note: Applications can execute functions they define. To execute functions defined in
+ * another component, apps would need to have {@code
+ * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code
+ * android.permission.EXECUTE_APP_FUNCTIONS}.
+ *
+ * @param request the request to execute the app function
+ * @param executor the executor to run the callback
+ * @param cancellationSignal the cancellation signal to cancel the execution.
+ * @param callback the callback to receive the function execution result. if the calling app
+ * does not own the app function or does not have {@code
+ * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code
+ * android.permission.EXECUTE_APP_FUNCTIONS}, the execution result will contain {@code
+ * ExecuteAppFunctionResponse.RESULT_DENIED}.
+ */
+ // TODO(b/357551503): Document the behavior when the cancellation signal is issued.
// TODO(b/360864791): Document that apps can opt-out from being executed by callers with
// EXECUTE_APP_FUNCTIONS and how a caller knows whether a function is opted out.
// TODO(b/357551503): Update documentation when get / set APIs are implemented that this will
@@ -88,6 +126,7 @@ public final class AppFunctionManager {
public void executeAppFunction(
@NonNull ExecuteAppFunctionRequest request,
@NonNull @CallbackExecutor Executor executor,
+ @NonNull CancellationSignal cancellationSignal,
@NonNull Consumer<ExecuteAppFunctionResponse> callback) {
Objects.requireNonNull(request);
Objects.requireNonNull(executor);
@@ -96,25 +135,31 @@ public final class AppFunctionManager {
ExecuteAppFunctionAidlRequest aidlRequest =
new ExecuteAppFunctionAidlRequest(
request, mContext.getUser(), mContext.getPackageName());
+
try {
- mService.executeAppFunction(
- aidlRequest,
- new IExecuteAppFunctionCallback.Stub() {
- @Override
- public void onResult(ExecuteAppFunctionResponse result) {
- try {
- executor.execute(() -> callback.accept(result));
- } catch (RuntimeException e) {
- // Ideally shouldn't happen since errors are wrapped into the
- // response, but we catch it here for additional safety.
- callback.accept(
- ExecuteAppFunctionResponse.newFailure(
- getResultCode(e),
- e.getMessage(),
- /* extras= */ null));
- }
- }
- });
+ ICancellationSignal cancellationTransport =
+ mService.executeAppFunction(
+ aidlRequest,
+ new IExecuteAppFunctionCallback.Stub() {
+ @Override
+ public void onResult(ExecuteAppFunctionResponse result) {
+ try {
+ executor.execute(() -> callback.accept(result));
+ } catch (RuntimeException e) {
+ // Ideally shouldn't happen since errors are wrapped into
+ // the
+ // response, but we catch it here for additional safety.
+ callback.accept(
+ ExecuteAppFunctionResponse.newFailure(
+ getResultCode(e),
+ e.getMessage(),
+ /* extras= */ null));
+ }
+ }
+ });
+ if (cancellationTransport != null) {
+ cancellationSignal.setRemote(cancellationTransport);
+ }
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/app/appfunctions/AppFunctionService.java b/core/java/android/app/appfunctions/AppFunctionService.java
index 0d981ea5a679..8e417737515e 100644
--- a/core/java/android/app/appfunctions/AppFunctionService.java
+++ b/core/java/android/app/appfunctions/AppFunctionService.java
@@ -29,7 +29,12 @@ import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
+import android.os.Bundle;
import android.os.IBinder;
+import android.os.ICancellationSignal;
+import android.os.CancellationSignal;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
import java.util.function.Consumer;
@@ -74,6 +79,7 @@ public abstract class AppFunctionService extends Service {
*/
void perform(
@NonNull ExecuteAppFunctionRequest request,
+ @NonNull CancellationSignal cancellationSignal,
@NonNull Consumer<ExecuteAppFunctionResponse> callback);
}
@@ -85,6 +91,7 @@ public abstract class AppFunctionService extends Service {
@Override
public void executeAppFunction(
@NonNull ExecuteAppFunctionRequest request,
+ @NonNull ICancellationCallback cancellationCallback,
@NonNull IExecuteAppFunctionCallback callback) {
if (context.checkCallingPermission(BIND_APP_FUNCTION_SERVICE)
== PERMISSION_DENIED) {
@@ -93,7 +100,10 @@ public abstract class AppFunctionService extends Service {
SafeOneTimeExecuteAppFunctionCallback safeCallback =
new SafeOneTimeExecuteAppFunctionCallback(callback);
try {
- onExecuteFunction.perform(request, safeCallback::onResult);
+ onExecuteFunction.perform(
+ request,
+ buildCancellationSignal(cancellationCallback),
+ safeCallback::onResult);
} catch (Exception ex) {
// Apps should handle exceptions. But if they don't, report the error on
// behalf of them.
@@ -105,6 +115,21 @@ public abstract class AppFunctionService extends Service {
};
}
+ private static CancellationSignal buildCancellationSignal(
+ @NonNull ICancellationCallback cancellationCallback) {
+ final ICancellationSignal cancellationSignalTransport =
+ CancellationSignal.createTransport();
+ CancellationSignal cancellationSignal =
+ CancellationSignal.fromTransport(cancellationSignalTransport);
+ try {
+ cancellationCallback.sendCancellationTransport(cancellationSignalTransport);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ return cancellationSignal ;
+ }
+
private final Binder mBinder = createBinder(
AppFunctionService.this,
AppFunctionService.this::onExecuteFunction);
@@ -115,6 +140,7 @@ public abstract class AppFunctionService extends Service {
return mBinder;
}
+
/**
* Called by the system to execute a specific app function.
*
@@ -134,9 +160,45 @@ public abstract class AppFunctionService extends Service {
*
* @param request The function execution request.
* @param callback A callback to report back the result.
+ *
+ * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, CancellationSignal,
+ * Consumer)} instead. This method will be removed once usage references are updated.
*/
@MainThread
+ @Deprecated
public abstract void onExecuteFunction(
@NonNull ExecuteAppFunctionRequest request,
@NonNull Consumer<ExecuteAppFunctionResponse> callback);
+
+ /**
+ * Called by the system to execute a specific app function.
+ *
+ * <p>This method is triggered when the system requests your AppFunctionService to handle a
+ * particular function you have registered and made available.
+ *
+ * <p>To ensure proper routing of function requests, assign a unique identifier to each
+ * function. This identifier doesn't need to be globally unique, but it must be unique within
+ * your app. For example, a function to order food could be identified as "orderFood". In most
+ * cases this identifier should come from the ID automatically generated by the AppFunctions
+ * SDK. You can determine the specific function to invoke by calling {@link
+ * ExecuteAppFunctionRequest#getFunctionIdentifier()}.
+ *
+ * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker
+ * thread and dispatch the result with the given callback. You should always report back the
+ * result using the callback, no matter if the execution was successful or not.
+ *
+ * <p>This method also accepts a {@link CancellationSignal} that the app should listen to cancel
+ * the execution of function if requested by the system.
+ *
+ * @param request The function execution request.
+ * @param cancellationSignal A signal to cancel the execution.
+ * @param callback A callback to report back the result.
+ */
+ @MainThread
+ public void onExecuteFunction(
+ @NonNull ExecuteAppFunctionRequest request,
+ @NonNull CancellationSignal cancellationSignal,
+ @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
+ onExecuteFunction(request, callback);
+ }
}
diff --git a/core/java/android/app/appfunctions/IAppFunctionManager.aidl b/core/java/android/app/appfunctions/IAppFunctionManager.aidl
index 28827bb3052c..c63217ffe850 100644
--- a/core/java/android/app/appfunctions/IAppFunctionManager.aidl
+++ b/core/java/android/app/appfunctions/IAppFunctionManager.aidl
@@ -18,6 +18,7 @@ package android.app.appfunctions;
import android.app.appfunctions.ExecuteAppFunctionAidlRequest;
import android.app.appfunctions.IExecuteAppFunctionCallback;
+import android.os.ICancellationSignal;
/**
* Defines the interface for apps to interact with the app function execution service
@@ -32,8 +33,8 @@ interface IAppFunctionManager {
* @param callback the callback to report the result.
*/
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(anyOf = {android.Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED,android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional = true)")
- void executeAppFunction(
+ ICancellationSignal executeAppFunction(
in ExecuteAppFunctionAidlRequest request,
in IExecuteAppFunctionCallback callback
);
-} \ No newline at end of file
+}
diff --git a/core/java/android/app/appfunctions/IAppFunctionService.aidl b/core/java/android/app/appfunctions/IAppFunctionService.aidl
index cc5a20cfa194..291f33ccb1b8 100644
--- a/core/java/android/app/appfunctions/IAppFunctionService.aidl
+++ b/core/java/android/app/appfunctions/IAppFunctionService.aidl
@@ -16,7 +16,7 @@
package android.app.appfunctions;
-import android.os.Bundle;
+import android.app.appfunctions.ICancellationCallback;
import android.app.appfunctions.IExecuteAppFunctionCallback;
import android.app.appfunctions.ExecuteAppFunctionRequest;
@@ -34,10 +34,12 @@ oneway interface IAppFunctionService {
* Called by the system to execute a specific app function.
*
* @param request the function execution request.
+ * @param cancellationCallback a callback to send back the cancellation transport.
* @param callback a callback to report back the result.
*/
void executeAppFunction(
in ExecuteAppFunctionRequest request,
+ in ICancellationCallback cancellationCallback,
in IExecuteAppFunctionCallback callback
);
}
diff --git a/core/java/android/app/appfunctions/ICancellationCallback.aidl b/core/java/android/app/appfunctions/ICancellationCallback.aidl
new file mode 100644
index 000000000000..03235aca017a
--- /dev/null
+++ b/core/java/android/app/appfunctions/ICancellationCallback.aidl
@@ -0,0 +1,24 @@
+/*
+ * 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 android.app.appfunctions;
+
+import android.os.ICancellationSignal;
+
+/** {@hide} */
+oneway interface ICancellationCallback {
+ void sendCancellationTransport(in ICancellationSignal cancellationTransport);
+} \ No newline at end of file
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index 21627920f598..1b21bdf7ba45 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -181,7 +181,7 @@ public final class CameraManager {
* @hide
*/
@TestApi
- @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public static final int ROTATION_OVERRIDE_NONE = ICameraService.ROTATION_OVERRIDE_NONE;
/**
@@ -191,7 +191,7 @@ public final class CameraManager {
* @hide
*/
@TestApi
- @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public static final int ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT =
ICameraService.ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT;
@@ -201,7 +201,7 @@ public final class CameraManager {
* @hide
*/
@TestApi
- @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public static final int ROTATION_OVERRIDE_ROTATION_ONLY =
ICameraService.ROTATION_OVERRIDE_ROTATION_ONLY;
@@ -1562,7 +1562,7 @@ public final class CameraManager {
*/
public static int getRotationOverride(@Nullable Context context,
@Nullable PackageManager packageManager, @Nullable String packageName) {
- if (com.android.window.flags.Flags.cameraCompatForFreeform()) {
+ if (com.android.window.flags.Flags.enableCameraCompatForDesktopWindowing()) {
return getRotationOverrideInternal(context, packageManager, packageName);
} else {
return shouldOverrideToPortrait(packageManager, packageName)
@@ -1574,7 +1574,7 @@ public final class CameraManager {
/**
* @hide
*/
- @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@TestApi
public static int getRotationOverrideInternal(@Nullable Context context,
@Nullable PackageManager packageManager, @Nullable String packageName) {
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index b8a8be159d12..d82af55e2771 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -10750,6 +10750,16 @@ public final class Settings {
"lock_screen_show_only_unseen_notifications";
/**
+ * Indicates whether to minimalize the number of notifications to show on the lockscreen.
+ * <p>
+ * Type: int (0 for false, 1 for true)
+ *
+ * @hide
+ */
+ public static final String LOCK_SCREEN_NOTIFICATION_MINIMALISM =
+ "lock_screen_notification_minimalism";
+
+ /**
* Indicates whether snooze options should be shown on notifications
* <p>
* Type: int (0 for false, 1 for true)
diff --git a/core/java/android/security/responsible_apis_flags.aconfig b/core/java/android/security/responsible_apis_flags.aconfig
index 45e9def2c15f..5457bbee8ad3 100644
--- a/core/java/android/security/responsible_apis_flags.aconfig
+++ b/core/java/android/security/responsible_apis_flags.aconfig
@@ -24,6 +24,17 @@ flag {
}
flag {
+ name: "asm_reintroduce_grace_period"
+ namespace: "responsible_apis"
+ description: "Allow launches within the grace period for ASM apps"
+ bug: "367702727"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+
+flag {
name: "content_uri_permission_apis"
is_exported: true
namespace: "responsible_apis"
diff --git a/core/java/android/view/WindowLayout.java b/core/java/android/view/WindowLayout.java
index dda399357d8c..d5ccca992b4f 100644
--- a/core/java/android/view/WindowLayout.java
+++ b/core/java/android/view/WindowLayout.java
@@ -157,10 +157,10 @@ public class WindowLayout {
// which prevents overlap with the DisplayCutout.
if (!attachedInParent && !floatingInScreenWindow) {
mTempRect.set(outParentFrame);
- outParentFrame.intersectUnchecked(displayCutoutSafeExceptMaybeBars);
+ intersectOrClamp(outParentFrame, displayCutoutSafeExceptMaybeBars);
frames.isParentFrameClippedByDisplayCutout = !mTempRect.equals(outParentFrame);
}
- outDisplayFrame.intersectUnchecked(displayCutoutSafeExceptMaybeBars);
+ intersectOrClamp(outDisplayFrame, displayCutoutSafeExceptMaybeBars);
}
final boolean noLimits = (attrs.flags & FLAG_LAYOUT_NO_LIMITS) != 0;
@@ -283,6 +283,19 @@ public class WindowLayout {
+ " requestedInvisibleTypes=" + WindowInsets.Type.toString(~requestedVisibleTypes));
}
+ /**
+ * If both rectangles intersect, set inOutRect to that intersection. Otherwise, clamp inOutRect
+ * to the side (or the corner) that the other rectangle is away from.
+ * Unlike {@link Rect#intersectUnchecked(Rect)}, this method guarantees that the new rectangle
+ * is valid and contained in inOutRect if rectangles involved are valid.
+ */
+ private static void intersectOrClamp(Rect inOutRect, Rect other) {
+ inOutRect.left = Math.min(Math.max(inOutRect.left, other.left), inOutRect.right);
+ inOutRect.top = Math.min(Math.max(inOutRect.top, other.top), inOutRect.bottom);
+ inOutRect.right = Math.max(Math.min(inOutRect.right, other.right), inOutRect.left);
+ inOutRect.bottom = Math.max(Math.min(inOutRect.bottom, other.bottom), inOutRect.top);
+ }
+
public static void extendFrameByCutout(Rect displayCutoutSafe,
Rect displayFrame, Rect inOutFrame, Rect tempRect) {
if (displayCutoutSafe.contains(inOutFrame)) {
diff --git a/core/proto/android/server/vibrator/vibratormanagerservice.proto b/core/proto/android/server/vibrator/vibratormanagerservice.proto
index e7f0560612cc..258832e3e7ff 100644
--- a/core/proto/android/server/vibrator/vibratormanagerservice.proto
+++ b/core/proto/android/server/vibrator/vibratormanagerservice.proto
@@ -157,10 +157,8 @@ message VibratorManagerServiceDumpProto {
option (.android.msg_privacy).dest = DEST_AUTOMATIC;
repeated int32 vibrator_ids = 1;
optional VibrationProto current_vibration = 2;
- optional bool is_vibrating = 3;
optional int32 is_vibrator_controller_registered = 27;
optional VibrationProto current_external_vibration = 4;
- optional bool vibrator_under_external_control = 5;
optional bool low_power_mode = 6;
optional bool vibrate_on = 24;
reserved 25; // prev keyboard_vibration_on
@@ -183,4 +181,6 @@ message VibratorManagerServiceDumpProto {
repeated VibrationProto previous_vibrations = 16;
repeated VibrationParamProto previous_vibration_params = 28;
reserved 17; // prev previous_external_vibrations
+ reserved 3; // prev is_vibrating, check current_vibration instead
+ reserved 5; // prev vibrator_under_external_control, check current_external_vibration instead
} \ No newline at end of file
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 07efad89010a..92c390656da5 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4457,6 +4457,11 @@
<!-- Bytes that the PinnerService will pin for WebView -->
<integer name="config_pinnerWebviewPinBytes">0</integer>
+ <!-- Maximum memory that PinnerService will pin for apps expressed
+ as a percentage of total device memory [0,100].
+ Example: 10, means 10% of total memory will be the maximum pinned memory -->
+ <integer name="config_pinnerMaxPinnedMemoryPercentage">10</integer>
+
<!-- Number of days preloaded file cache should be preserved on a device before it can be
deleted -->
<integer name="config_keepPreloadsMinDays">7</integer>
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index f397ef2b151c..6683dc044c9a 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -808,6 +808,12 @@
This is bigger than displayed because listeners can use it for other displays
e.g. wearables. -->
<dimen name="notification_person_icon_max_size">144dp</dimen>
+ <!-- The size of the progress bar icon -->
+ <dimen name="notification_progress_icon_size">20dp</dimen>
+ <!-- The size of the progress tracker width -->
+ <dimen name="notification_progress_tracker_width">40dp</dimen>
+ <!-- The size of the progress tracker height -->
+ <dimen name="notification_progress_tracker_height">20dp</dimen>
<!-- The maximum size of the small notification icon on low memory devices. -->
<dimen name="notification_small_icon_size_low_ram">@dimen/notification_small_icon_size</dimen>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 06b36b8f74af..5f40a6c7eba4 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3467,6 +3467,7 @@
<java-symbol type="integer" name="config_pinnerHomePinBytes" />
<java-symbol type="bool" name="config_pinnerAssistantApp" />
<java-symbol type="integer" name="config_pinnerWebviewPinBytes" />
+ <java-symbol type="integer" name="config_pinnerMaxPinnedMemoryPercentage" />
<java-symbol type="string" name="config_doubleTouchGestureEnableFile" />
@@ -3854,6 +3855,9 @@
<java-symbol type="dimen" name="notification_custom_view_max_image_height"/>
<java-symbol type="dimen" name="notification_custom_view_max_image_width"/>
<java-symbol type="dimen" name="notification_person_icon_max_size" />
+ <java-symbol type="dimen" name="notification_progress_icon_size" />
+ <java-symbol type="dimen" name="notification_progress_tracker_width" />
+ <java-symbol type="dimen" name="notification_progress_tracker_height" />
<java-symbol type="dimen" name="notification_small_icon_size_low_ram"/>
<java-symbol type="dimen" name="notification_big_picture_max_height_low_ram"/>
diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java
index 0f73df92ca93..edcea241e620 100644
--- a/core/tests/coretests/src/android/app/NotificationTest.java
+++ b/core/tests/coretests/src/android/app/NotificationTest.java
@@ -97,7 +97,6 @@ import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.text.style.TextAppearanceSpan;
import android.util.Pair;
-import android.util.Slog;
import android.widget.RemoteViews;
import androidx.test.InstrumentationRegistry;
@@ -118,6 +117,7 @@ import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
+import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
@@ -2114,6 +2114,300 @@ public class NotificationTest {
assertThat(n.getWhen()).isEqualTo(9);
}
+ @Test
+ public void getNotificationStyleClass_forPlatformClassName_returnsPlatformClass() {
+ final List<Class<? extends Notification.Style>> platformStyleClasses = List.of(
+ Notification.BigTextStyle.class, Notification.BigPictureStyle.class,
+ Notification.MessagingStyle.class, Notification.CallStyle.class,
+ Notification.InboxStyle.class, Notification.MediaStyle.class,
+ Notification.DecoratedCustomViewStyle.class,
+ Notification.DecoratedMediaCustomViewStyle.class
+ );
+
+ for (Class<? extends Notification.Style> platformStyleClass : platformStyleClasses) {
+ assertThat(Notification.getNotificationStyleClass(platformStyleClass.getName()))
+ .isEqualTo(platformStyleClass);
+ }
+ }
+
+ @Test
+ public void getNotificationStyleClass_forNotPlatformClassName_returnsNull() {
+ assertThat(Notification.getNotificationStyleClass(NotAPlatformStyle.class.getName()))
+ .isNull();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void progressStyle_richOngoingEnabled_platformClass() {
+ assertThat(
+ Notification.getNotificationStyleClass(Notification.ProgressStyle.class.getName()))
+ .isEqualTo(Notification.ProgressStyle.class);
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void progressStyle_richOngoingDisabled_notPlatformClass() {
+ assertThat(
+ Notification.getNotificationStyleClass(Notification.ProgressStyle.class.getName()))
+ .isNull();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void progressStyle_onSegmentChange_visiblyDifferent() {
+ final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle()
+ .addProgressSegment(new Notification.ProgressStyle.Segment(100))
+ .addProgressSegment(new Notification.ProgressStyle.Segment(50)
+ .setColor(Color.RED)));
+
+ final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle()
+ .addProgressSegment(new Notification.ProgressStyle.Segment(100))
+ .addProgressSegment(new Notification.ProgressStyle.Segment(50)
+ .setColor(Color.BLUE)));
+
+ assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+ .isTrue();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void indeterminateProgressStyle_onSegmentChange_visiblyNotDifferent() {
+ final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true)
+ .addProgressSegment(new Notification.ProgressStyle.Segment(100))
+ .addProgressSegment(new Notification.ProgressStyle.Segment(50)
+ .setColor(Color.RED)));
+
+ final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true)
+ .addProgressSegment(new Notification.ProgressStyle.Segment(100))
+ .addProgressSegment(new Notification.ProgressStyle.Segment(50)
+ .setColor(Color.BLUE)));
+
+ assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+ .isFalse();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void progressStyle_onStartIconChange_visiblyDifferent() {
+ final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+ mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96));
+
+ final Icon icon2 = Icon.createWithBitmap(
+ Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+ final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle().setProgressStartIcon(icon1));
+ final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle().setProgressStartIcon(icon2));
+
+ assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+ .isTrue();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void progressStyle_onEndIconChange_visiblyDifferent() {
+ final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+ mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96));
+
+ final Icon icon2 = Icon.createWithBitmap(
+ Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+ final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle().setProgressEndIcon(icon1));
+ final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle().setProgressEndIcon(icon2));
+
+ assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+ .isTrue();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void progressStyle_onProgressChange_visiblyDifferent() {
+ final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle().setProgress(20));
+ final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle().setProgress(21));
+
+ assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+ .isTrue();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void indeterminateProgressStyle_onProgressChange_visiblyNotDifferent() {
+ final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle()
+ .setProgressIndeterminate(true).setProgress(20));
+ final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle()
+ .setProgressIndeterminate(true).setProgress(21));
+
+ assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+ .isFalse();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void progressStyle_onIsStyledByProgressChange_visiblyDifferent() {
+ final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle().setStyledByProgress(true));
+ final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle().setStyledByProgress(false));
+
+ assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+ .isTrue();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void indeterminateProgressStyle_onIsStyledByProgressChange_visiblyNotDifferent() {
+ final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle()
+ .setProgressIndeterminate(true).setStyledByProgress(true));
+ final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle()
+ .setProgressIndeterminate(true).setStyledByProgress(false));
+
+ assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+ .isFalse();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void progressStyle_onProgressStepChange_visiblyDifferent() {
+ final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle()
+ .addProgressStep(new Notification.ProgressStyle.Step(10)));
+ final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle()
+ .addProgressStep(new Notification.ProgressStyle.Step(12)));
+
+ assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+ .isTrue();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void indeterminateProgressStyle_onProgressStepChange_visiblyNotDifferent() {
+ final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true)
+ .addProgressStep(new Notification.ProgressStyle.Step(10)));
+ final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true)
+ .addProgressStep(new Notification.ProgressStyle.Step(12)));
+
+ assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+ .isFalse();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void progressStyle_onTrackerIconChange_visiblyDifferent() {
+ final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+ mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96));
+
+ final Icon icon2 = Icon.createWithBitmap(
+ Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+ final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle().setProgressTrackerIcon(icon1));
+ final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle().setProgressTrackerIcon(icon2));
+
+ assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+ .isTrue();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void indeterminateProgressStyle_onTrackerIconChange_visiblyNotDifferent() {
+ final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+ mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96));
+
+ final Icon icon2 = Icon.createWithBitmap(
+ Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+ final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true)
+ .setProgressTrackerIcon(icon1));
+ final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle()
+ .setProgressIndeterminate(true).setProgressTrackerIcon(icon2));
+
+ assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+ .isFalse();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void progressStyle_onIndeterminateChange_visiblyDifferent() {
+ final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true));
+ final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(false));
+
+ assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2))
+ .isTrue();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void progressStyle_getProgressMax_default100() {
+ final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle();
+ assertThat(progressStyle.getProgressMax()).isEqualTo(100);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void progressStyle_getProgressMax_nooSegments_returnsDefault() {
+ final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle();
+ progressStyle.setProgressSegments(Collections.emptyList());
+ assertThat(progressStyle.getProgressMax()).isEqualTo(100);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void progressStyle_getProgressMax_returnsSumOfSegmentLength() {
+ final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle();
+ progressStyle
+ .addProgressSegment(new Notification.ProgressStyle.Segment(10))
+ .addProgressSegment(new Notification.ProgressStyle.Segment(20));
+
+ assertThat(progressStyle.getProgressMax()).isEqualTo(30);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void progressStyle_getProgressMax_onSegmentOverflow_returnsDefault() {
+ final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle();
+ progressStyle
+ .addProgressSegment(new Notification.ProgressStyle.Segment(Integer.MAX_VALUE))
+ .addProgressSegment(new Notification.ProgressStyle.Segment(10));
+
+ assertThat(progressStyle.getProgressMax()).isEqualTo(100);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void progressStyle_indeterminate_defaultValueFalse() {
+ final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle();
+
+ assertThat(progressStyle1.isProgressIndeterminate()).isFalse();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void progressStyle_styledByProgress_defaultValueTrue() {
+ final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle();
+
+ assertThat(progressStyle1.isStyledByProgress()).isTrue();
+ }
private void assertValid(Notification.Colors c) {
// Assert that all colors are populated
assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID);
@@ -2214,4 +2508,11 @@ public class NotificationTest {
new Intent(action).setPackage(mContext.getPackageName()),
PendingIntent.FLAG_MUTABLE);
}
+
+ private static class NotAPlatformStyle extends Notification.Style {
+ @Override
+ public boolean areNotificationsVisiblyDifferent(Notification.Style other) {
+ return false;
+ }
+ }
}
diff --git a/core/tests/coretests/src/android/view/WindowLayoutTests.java b/core/tests/coretests/src/android/view/WindowLayoutTests.java
index 5cac98daee80..d4693e6e7130 100644
--- a/core/tests/coretests/src/android/view/WindowLayoutTests.java
+++ b/core/tests/coretests/src/android/view/WindowLayoutTests.java
@@ -413,4 +413,19 @@ public class WindowLayoutTests {
assertInsetByTopBottom(0, 0, mFrames.parentFrame);
assertInsetByTopBottom(0, 0, mFrames.frame);
}
+
+ @Test
+ public void windowBoundsOutsideDisplayCutoutSafe() {
+ addDisplayCutout();
+ mAttrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
+ mWindowBounds.set(0, -1000, DISPLAY_WIDTH, 0);
+ computeFrames();
+
+ assertRect(WATERFALL_INSETS.left, 0, DISPLAY_WIDTH - WATERFALL_INSETS.right, 0,
+ mFrames.displayFrame);
+ assertRect(WATERFALL_INSETS.left, 0, DISPLAY_WIDTH - WATERFALL_INSETS.right, 0,
+ mFrames.parentFrame);
+ assertRect(WATERFALL_INSETS.left, 0, DISPLAY_WIDTH - WATERFALL_INSETS.right, 0,
+ mFrames.frame);
+ }
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
index 37f0067de453..089613853555 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
@@ -16,12 +16,11 @@
package androidx.window.common;
-import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER;
+import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
import static androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_UNKNOWN;
import static androidx.window.common.layout.CommonFoldingFeature.parseListFromString;
-import android.annotation.NonNull;
import android.content.Context;
import android.hardware.devicestate.DeviceState;
import android.hardware.devicestate.DeviceStateManager;
@@ -31,16 +30,23 @@ import android.text.TextUtils;
import android.util.Log;
import android.util.SparseIntArray;
+import androidx.annotation.BinderThread;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
import androidx.window.common.layout.CommonFoldingFeature;
import androidx.window.common.layout.DisplayFoldFeatureCommon;
import com.android.internal.R;
+import com.android.window.flags.Flags;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
+import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
@@ -55,13 +61,6 @@ public final class DeviceStateManagerFoldingFeatureProducer
private static final boolean DEBUG = false;
/**
- * Emulated device state
- * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)} to
- * {@link CommonFoldingFeature.State} map.
- */
- private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray();
-
- /**
* Device state received via
* {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)}.
* The identifier returned through {@link DeviceState#getIdentifier()} may not correspond 1:1
@@ -71,23 +70,40 @@ public final class DeviceStateManagerFoldingFeatureProducer
* "rear display". Concurrent mode for example is activated via public API and can be active in
* both the "open" and "half folded" device states.
*/
- private DeviceState mCurrentDeviceState = new DeviceState(
- new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER,
- "INVALID").build());
+ // TODO: b/337820752 - Add @GuardedBy("mCurrentDeviceStateLock") after flag cleanup.
+ private DeviceState mCurrentDeviceState = INVALID_DEVICE_STATE;
- private List<DeviceState> mSupportedStates;
+ /**
+ * Lock to synchronize access to {@link #mCurrentDeviceState}.
+ *
+ * <p>This lock is used to ensure thread-safety when accessing and modifying the
+ * {@link #mCurrentDeviceState} field. It is acquired by both the binder thread (if
+ * {@link Flags#wlinfoOncreate()} is enabled) and the main thread (if
+ * {@link Flags#wlinfoOncreate()} is disabled) to prevent race conditions and
+ * ensure data consistency.
+ */
+ private final Object mCurrentDeviceStateLock = new Object();
@NonNull
private final RawFoldingFeatureProducer mRawFoldSupplier;
- private final boolean mIsHalfOpenedSupported;
-
- private final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() {
+ @NonNull
+ private final DeviceStateMapper mDeviceStateMapper;
+
+ @VisibleForTesting
+ final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() {
+ // The GuardedBy analysis is intra-procedural, meaning it doesn’t consider the getData()
+ // implementation. See https://errorprone.info/bugpattern/GuardedBy for limitations.
+ @SuppressWarnings("GuardedBy")
+ @BinderThread // When Flags.wlinfoOncreate() is enabled.
+ @MainThread // When Flags.wlinfoOncreate() is disabled.
@Override
public void onDeviceStateChanged(@NonNull DeviceState state) {
- mCurrentDeviceState = state;
- mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer
- .this::notifyFoldingFeatureChange);
+ synchronized (mCurrentDeviceStateLock) {
+ mCurrentDeviceState = state;
+ mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer.this
+ ::notifyFoldingFeatureChangeLocked);
+ }
}
};
@@ -95,41 +111,14 @@ public final class DeviceStateManagerFoldingFeatureProducer
@NonNull RawFoldingFeatureProducer rawFoldSupplier,
@NonNull DeviceStateManager deviceStateManager) {
mRawFoldSupplier = rawFoldSupplier;
- String[] deviceStatePosturePairs = context.getResources()
- .getStringArray(R.array.config_device_state_postures);
- mSupportedStates = deviceStateManager.getSupportedDeviceStates();
- boolean isHalfOpenedSupported = false;
- for (String deviceStatePosturePair : deviceStatePosturePairs) {
- String[] deviceStatePostureMapping = deviceStatePosturePair.split(":");
- if (deviceStatePostureMapping.length != 2) {
- if (DEBUG) {
- Log.e(TAG, "Malformed device state posture pair: "
- + deviceStatePosturePair);
- }
- continue;
- }
+ mDeviceStateMapper =
+ new DeviceStateMapper(context, deviceStateManager.getSupportedDeviceStates());
- int deviceState;
- int posture;
- try {
- deviceState = Integer.parseInt(deviceStatePostureMapping[0]);
- posture = Integer.parseInt(deviceStatePostureMapping[1]);
- } catch (NumberFormatException e) {
- if (DEBUG) {
- Log.e(TAG, "Failed to parse device state or posture: "
- + deviceStatePosturePair,
- e);
- }
- continue;
- }
- isHalfOpenedSupported = isHalfOpenedSupported
- || posture == CommonFoldingFeature.COMMON_STATE_HALF_OPENED;
- mDeviceStateToPostureMap.put(deviceState, posture);
- }
- mIsHalfOpenedSupported = isHalfOpenedSupported;
- if (mDeviceStateToPostureMap.size() > 0) {
+ if (!mDeviceStateMapper.isDeviceStateToPostureMapEmpty()) {
+ final Executor executor =
+ Flags.wlinfoOncreate() ? Runnable::run : context.getMainExecutor();
Objects.requireNonNull(deviceStateManager)
- .registerCallback(context.getMainExecutor(), mDeviceStateCallback);
+ .registerCallback(executor, mDeviceStateCallback);
}
}
@@ -137,50 +126,51 @@ public final class DeviceStateManagerFoldingFeatureProducer
* Add a callback to mCallbacks if there is no device state. This callback will be run
* once a device state is set. Otherwise,run the callback immediately.
*/
- private void runCallbackWhenValidState(@NonNull Consumer<List<CommonFoldingFeature>> callback,
- String displayFeaturesString) {
- if (isCurrentStateValid()) {
- callback.accept(calculateFoldingFeature(displayFeaturesString));
+ private void runCallbackWhenValidState(@NonNull DeviceState state,
+ @NonNull Consumer<List<CommonFoldingFeature>> callback,
+ @NonNull String displayFeaturesString) {
+ if (mDeviceStateMapper.isDeviceStateValid(state)) {
+ callback.accept(calculateFoldingFeature(state, displayFeaturesString));
} else {
// This callback will be added to mCallbacks and removed once it runs once.
- AcceptOnceConsumer<List<CommonFoldingFeature>> singleRunCallback =
+ final AcceptOnceConsumer<List<CommonFoldingFeature>> singleRunCallback =
new AcceptOnceConsumer<>(this, callback);
addDataChangedCallback(singleRunCallback);
}
}
- /**
- * Checks to find {@link DeviceStateManagerFoldingFeatureProducer#mCurrentDeviceState} in the
- * {@link DeviceStateManagerFoldingFeatureProducer#mDeviceStateToPostureMap} which was
- * initialized in the constructor of {@link DeviceStateManagerFoldingFeatureProducer}.
- * Returns a boolean value of whether the device state is valid.
- */
- private boolean isCurrentStateValid() {
- // If the device state is not found in the map, indexOfKey returns a negative number.
- return mDeviceStateToPostureMap.indexOfKey(mCurrentDeviceState.getIdentifier()) >= 0;
- }
-
+ // The GuardedBy analysis is intra-procedural, meaning it doesn’t consider the implementation of
+ // addDataChangedCallback(). See https://errorprone.info/bugpattern/GuardedBy for limitations.
+ @SuppressWarnings("GuardedBy")
@Override
protected void onListenersChanged() {
super.onListenersChanged();
- if (hasListeners()) {
- mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChange);
- } else {
- mCurrentDeviceState = new DeviceState(
- new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER,
- "INVALID").build());
- mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChange);
+ synchronized (mCurrentDeviceStateLock) {
+ if (hasListeners()) {
+ mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChangeLocked);
+ } else {
+ mCurrentDeviceState = INVALID_DEVICE_STATE;
+ mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChangeLocked);
+ }
+ }
+ }
+
+ @NonNull
+ private DeviceState getCurrentDeviceState() {
+ synchronized (mCurrentDeviceStateLock) {
+ return mCurrentDeviceState;
}
}
@NonNull
@Override
public Optional<List<CommonFoldingFeature>> getCurrentData() {
- Optional<String> displayFeaturesString = mRawFoldSupplier.getCurrentData();
- if (!isCurrentStateValid()) {
+ final Optional<String> displayFeaturesString = mRawFoldSupplier.getCurrentData();
+ final DeviceState state = getCurrentDeviceState();
+ if (!mDeviceStateMapper.isDeviceStateValid(state) || displayFeaturesString.isEmpty()) {
return Optional.empty();
} else {
- return displayFeaturesString.map(this::calculateFoldingFeature);
+ return Optional.of(calculateFoldingFeature(state, displayFeaturesString.get()));
}
}
@@ -191,7 +181,7 @@ public final class DeviceStateManagerFoldingFeatureProducer
*/
@NonNull
public List<CommonFoldingFeature> getFoldsWithUnknownState() {
- Optional<String> optionalFoldingFeatureString = mRawFoldSupplier.getCurrentData();
+ final Optional<String> optionalFoldingFeatureString = mRawFoldSupplier.getCurrentData();
if (optionalFoldingFeatureString.isPresent()) {
return CommonFoldingFeature.parseListFromString(
@@ -201,7 +191,6 @@ public final class DeviceStateManagerFoldingFeatureProducer
return Collections.emptyList();
}
-
/**
* Returns the list of supported {@link DisplayFoldFeatureCommon} calculated from the
* {@link DeviceStateManagerFoldingFeatureProducer}.
@@ -218,16 +207,16 @@ public final class DeviceStateManagerFoldingFeatureProducer
return foldFeatures;
}
-
/**
* Returns {@code true} if the device supports half-opened mode, {@code false} otherwise.
*/
public boolean isHalfOpenedSupported() {
- return mIsHalfOpenedSupported;
+ return mDeviceStateMapper.mIsHalfOpenedSupported;
}
/**
* Adds the data to the storeFeaturesConsumer when the data is ready.
+ *
* @param storeFeaturesConsumer a consumer to collect the data when it is first available.
*/
@Override
@@ -236,38 +225,123 @@ public final class DeviceStateManagerFoldingFeatureProducer
if (TextUtils.isEmpty(displayFeaturesString)) {
storeFeaturesConsumer.accept(new ArrayList<>());
} else {
- runCallbackWhenValidState(storeFeaturesConsumer, displayFeaturesString);
+ final DeviceState state = getCurrentDeviceState();
+ runCallbackWhenValidState(state, storeFeaturesConsumer, displayFeaturesString);
}
});
}
- private void notifyFoldingFeatureChange(String displayFeaturesString) {
- if (!isCurrentStateValid()) {
+ @GuardedBy("mCurrentDeviceStateLock")
+ private void notifyFoldingFeatureChangeLocked(String displayFeaturesString) {
+ final DeviceState state = mCurrentDeviceState;
+ if (!mDeviceStateMapper.isDeviceStateValid(state)) {
return;
}
if (TextUtils.isEmpty(displayFeaturesString)) {
notifyDataChanged(new ArrayList<>());
} else {
- notifyDataChanged(calculateFoldingFeature(displayFeaturesString));
+ notifyDataChanged(calculateFoldingFeature(state, displayFeaturesString));
}
}
- private List<CommonFoldingFeature> calculateFoldingFeature(String displayFeaturesString) {
- return parseListFromString(displayFeaturesString, currentHingeState());
+ @NonNull
+ private List<CommonFoldingFeature> calculateFoldingFeature(@NonNull DeviceState deviceState,
+ @NonNull String displayFeaturesString) {
+ @CommonFoldingFeature.State
+ final int hingeState = mDeviceStateMapper.getHingeState(deviceState);
+ return parseListFromString(displayFeaturesString, hingeState);
}
- @CommonFoldingFeature.State
- private int currentHingeState() {
- @CommonFoldingFeature.State
- int posture = mDeviceStateToPostureMap.get(mCurrentDeviceState.getIdentifier(),
- COMMON_STATE_UNKNOWN);
+ /**
+ * Internal class to map device states to corresponding postures.
+ *
+ * <p>This class encapsulates the logic for mapping device states to postures. The mapping is
+ * immutable after initialization to ensure thread safety.
+ */
+ private static class DeviceStateMapper {
+ /**
+ * Emulated device state
+ * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)} to
+ * {@link CommonFoldingFeature.State} map.
+ *
+ * <p>This map must be immutable after initialization to ensure thread safety, as it may be
+ * accessed from multiple threads. Modifications should only occur during object
+ * construction.
+ */
+ private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray();
+
+ /**
+ * The list of device states that are supported.
+ *
+ * <p>This list must be immutable after initialization to ensure thread safety.
+ */
+ @NonNull
+ private final List<DeviceState> mSupportedStates;
+
+ final boolean mIsHalfOpenedSupported;
+
+ DeviceStateMapper(@NonNull Context context, @NonNull List<DeviceState> supportedStates) {
+ mSupportedStates = supportedStates;
+
+ final String[] deviceStatePosturePairs = context.getResources()
+ .getStringArray(R.array.config_device_state_postures);
+ boolean isHalfOpenedSupported = false;
+ for (String deviceStatePosturePair : deviceStatePosturePairs) {
+ final String[] deviceStatePostureMapping = deviceStatePosturePair.split(":");
+ if (deviceStatePostureMapping.length != 2) {
+ if (DEBUG) {
+ Log.e(TAG, "Malformed device state posture pair: "
+ + deviceStatePosturePair);
+ }
+ continue;
+ }
- if (posture == CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) {
- posture = mDeviceStateToPostureMap.get(
- DeviceStateUtil.calculateBaseStateIdentifier(mCurrentDeviceState,
- mSupportedStates), COMMON_STATE_UNKNOWN);
+ final int deviceState;
+ final int posture;
+ try {
+ deviceState = Integer.parseInt(deviceStatePostureMapping[0]);
+ posture = Integer.parseInt(deviceStatePostureMapping[1]);
+ } catch (NumberFormatException e) {
+ if (DEBUG) {
+ Log.e(TAG, "Failed to parse device state or posture: "
+ + deviceStatePosturePair,
+ e);
+ }
+ continue;
+ }
+ isHalfOpenedSupported = isHalfOpenedSupported
+ || posture == CommonFoldingFeature.COMMON_STATE_HALF_OPENED;
+ mDeviceStateToPostureMap.put(deviceState, posture);
+ }
+ mIsHalfOpenedSupported = isHalfOpenedSupported;
+ }
+
+ boolean isDeviceStateToPostureMapEmpty() {
+ return mDeviceStateToPostureMap.size() == 0;
+ }
+
+ /**
+ * Validates if the provided deviceState exists in the {@link #mDeviceStateToPostureMap}
+ * which was initialized in the constructor of {@link DeviceStateMapper}.
+ * Returns a boolean value of whether the device state is valid.
+ */
+ boolean isDeviceStateValid(@NonNull DeviceState deviceState) {
+ // If the device state is not found in the map, indexOfKey returns a negative number.
+ return mDeviceStateToPostureMap.indexOfKey(deviceState.getIdentifier()) >= 0;
}
- return posture;
+ @CommonFoldingFeature.State
+ int getHingeState(@NonNull DeviceState deviceState) {
+ @CommonFoldingFeature.State
+ final int posture =
+ mDeviceStateToPostureMap.get(deviceState.getIdentifier(), COMMON_STATE_UNKNOWN);
+ if (posture != CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) {
+ return posture;
+ }
+
+ final int baseStateIdentifier =
+ DeviceStateUtil.calculateBaseStateIdentifier(deviceState, mSupportedStates);
+ return mDeviceStateToPostureMap.get(baseStateIdentifier, COMMON_STATE_UNKNOWN);
+ }
}
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index 74cce68f270b..dcc2d93060c9 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -377,8 +377,16 @@ class TaskContainer {
@Nullable
TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) {
- return getContainer(container -> container.hasAppearedActivity(activityToken)
- || container.hasPendingAppearedActivity(activityToken));
+ // When the new activity is launched to the topmost TF because the source activity
+ // was in that TF, and the source activity is finished before resolving the new activity,
+ // we will try to see if the new activity match a rule with the split activities below.
+ // If matched, it can be reparented.
+ final TaskFragmentContainer taskFragmentContainer
+ = getContainer(container -> container.hasPendingAppearedActivity(activityToken));
+ if (taskFragmentContainer != null) {
+ return taskFragmentContainer;
+ }
+ return getContainer(container -> container.hasAppearedActivity(activityToken));
}
@Nullable
diff --git a/libs/WindowManager/Jetpack/tests/unittest/Android.bp b/libs/WindowManager/Jetpack/tests/unittest/Android.bp
index bd430c0e610b..09185ee203b8 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/Android.bp
+++ b/libs/WindowManager/Jetpack/tests/unittest/Android.bp
@@ -29,6 +29,7 @@ android_test {
srcs: [
"**/*.java",
+ "**/*.kt",
],
static_libs: [
@@ -41,6 +42,7 @@ android_test {
"androidx.test.ext.junit",
"flag-junit",
"mockito-target-extended-minus-junit4",
+ "mockito-kotlin-nodeps",
"truth",
"testables",
"platform-test-annotations",
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt
new file mode 100644
index 000000000000..90887a747a6f
--- /dev/null
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt
@@ -0,0 +1,341 @@
+/*
+ * 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 androidx.window.common
+
+import android.content.Context
+import android.content.res.Resources
+import android.hardware.devicestate.DeviceState
+import android.hardware.devicestate.DeviceStateManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.window.common.layout.CommonFoldingFeature
+import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_FLAT
+import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_HALF_OPENED
+import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_NO_FOLDING_FEATURES
+import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_UNKNOWN
+import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE
+import androidx.window.common.layout.DisplayFoldFeatureCommon
+import androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED
+import androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN
+import com.android.internal.R
+import com.android.window.flags.Flags
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.stub
+import org.mockito.kotlin.verify
+
+/**
+ * Test class for [DeviceStateManagerFoldingFeatureProducer].
+ *
+ * Build/Install/Run:
+ * atest WMJetpackUnitTests:DeviceStateManagerFoldingFeatureProducerTest
+ */
+@RunWith(AndroidJUnit4::class)
+class DeviceStateManagerFoldingFeatureProducerTest {
+ @get:Rule
+ val setFlagsRule: SetFlagsRule = SetFlagsRule()
+
+ private val mMockDeviceStateManager = mock<DeviceStateManager>()
+ private val mMockResources = mock<Resources> {
+ on { getStringArray(R.array.config_device_state_postures) } doReturn DEVICE_STATE_POSTURES
+ }
+ private val mMockContext = mock<Context> {
+ on { resources } doReturn mMockResources
+ }
+ private val mRawFoldSupplier = mock<RawFoldingFeatureProducer> {
+ on { currentData } doReturn Optional.of(DISPLAY_FEATURES)
+ on { getData(any<Consumer<String>>()) } doAnswer { invocation ->
+ val callback = invocation.getArgument(0) as Consumer<String>
+ callback.accept(DISPLAY_FEATURES)
+ }
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_WLINFO_ONCREATE)
+ fun testRegisterCallback_whenWlinfoOncreateIsDisabled_usesMainExecutor() {
+ DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ verify(mMockDeviceStateManager).registerCallback(eq(mMockContext.mainExecutor), any())
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_WLINFO_ONCREATE)
+ fun testRegisterCallback_whenWlinfoOncreateIsEnabled_usesRunnableRun() {
+ val executorCaptor = ArgumentCaptor.forClass(Executor::class.java)
+ val runnable = mock<Runnable>()
+
+ DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ verify(mMockDeviceStateManager).registerCallback(executorCaptor.capture(), any())
+ executorCaptor.value.execute(runnable)
+ verify(runnable).run()
+ }
+
+ @Test
+ fun testGetCurrentData_validCurrentState_returnsFoldingFeatureWithState() {
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+ ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED)
+
+ val currentData = ffp.getCurrentData()
+
+ assertThat(currentData).isPresent()
+ assertThat(currentData.get()).containsExactlyElementsIn(HALF_OPENED_FOLDING_FEATURES)
+ }
+
+ @Test
+ fun testGetCurrentData_invalidCurrentState_returnsEmptyOptionalFoldingFeature() {
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ val currentData = ffp.getCurrentData()
+
+ assertThat(currentData).isEmpty()
+ }
+
+ @Test
+ fun testGetFoldsWithUnknownState_validFoldingFeature_returnsFoldingFeaturesWithUnknownState() {
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ val result = ffp.getFoldsWithUnknownState()
+
+ assertThat(result).containsExactlyElementsIn(UNKNOWN_STATE_FOLDING_FEATURES)
+ }
+
+ @Test
+ fun testGetFoldsWithUnknownState_emptyFoldingFeature_returnsEmptyList() {
+ mRawFoldSupplier.stub {
+ on { currentData } doReturn Optional.empty()
+ }
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ val result = ffp.getFoldsWithUnknownState()
+
+ assertThat(result).isEmpty()
+ }
+
+ @Test
+ fun testGetDisplayFeatures_validFoldingFeature_returnsDisplayFoldFeatures() {
+ mRawFoldSupplier.stub {
+ on { currentData } doReturn Optional.of(DISPLAY_FEATURES_HALF_OPENED_HINGE)
+ }
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ val result = ffp.displayFeatures
+
+ assertThat(result).containsExactly(
+ DisplayFoldFeatureCommon(
+ DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN,
+ setOf(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED),
+ ),
+ )
+ }
+
+ @Test
+ fun testIsHalfOpenedSupported_withHalfOpenedPostures_returnsTrue() {
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ assertThat(ffp.isHalfOpenedSupported).isTrue()
+ }
+
+ @Test
+ fun testIsHalfOpenedSupported_withEmptyPostures_returnsFalse() {
+ mMockResources.stub {
+ on { getStringArray(R.array.config_device_state_postures) } doReturn emptyArray()
+ }
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ assertThat(ffp.isHalfOpenedSupported).isFalse()
+ }
+
+ @Test
+ fun testGetData_emptyDisplayFeaturesString_callsConsumerWithEmptyList() {
+ mRawFoldSupplier.stub {
+ on { getData(any<Consumer<String>>()) } doAnswer { invocation ->
+ val callback = invocation.getArgument(0) as Consumer<String>
+ callback.accept("")
+ }
+ }
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+ val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>()
+
+ ffp.getData(storeFeaturesConsumer)
+
+ verify(storeFeaturesConsumer).accept(emptyList())
+ }
+
+ @Test
+ fun testGetData_validState_callsConsumerWithFoldingFeatures() {
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+ ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED)
+ val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>()
+
+ ffp.getData(storeFeaturesConsumer)
+
+ verify(storeFeaturesConsumer).accept(HALF_OPENED_FOLDING_FEATURES)
+ }
+
+ @Test
+ fun testGetData_invalidState_addsAcceptOnceConsumerToDataChangedCallback() {
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+ val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>()
+
+ ffp.getData(storeFeaturesConsumer)
+
+ verify(storeFeaturesConsumer, never()).accept(any())
+ ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED)
+ ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_OPENED)
+ verify(storeFeaturesConsumer).accept(HALF_OPENED_FOLDING_FEATURES)
+ }
+
+ @Test
+ fun testDeviceStateMapper_malformedDeviceStatePosturePair_skipsPair() {
+ val malformedDeviceStatePostures = arrayOf(
+ // Missing the posture.
+ "0",
+ // Empty string.
+ "",
+ // Too many elements.
+ "0:1:2",
+ )
+ mMockResources.stub {
+ on { getStringArray(R.array.config_device_state_postures) } doReturn
+ malformedDeviceStatePostures
+ }
+
+ DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ verify(mMockDeviceStateManager, never()).registerCallback(any(), any())
+ }
+
+ @Test
+ fun testDeviceStateMapper_invalidNumberFormat_skipsPair() {
+ val invalidNumberFormatDeviceStatePostures = arrayOf("a:1", "0:b", "a:b", ":1")
+ mMockResources.stub {
+ on { getStringArray(R.array.config_device_state_postures) } doReturn
+ invalidNumberFormatDeviceStatePostures
+ }
+
+ DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ verify(mMockDeviceStateManager, never()).registerCallback(any(), any())
+ }
+
+ companion object {
+ // Supported device states configuration.
+ private enum class SupportedDeviceStates {
+ CLOSED, HALF_OPENED, OPENED, REAR_DISPLAY, CONCURRENT;
+
+ override fun toString() = ordinal.toString()
+
+ fun toDeviceState(): DeviceState =
+ DeviceState(DeviceState.Configuration.Builder(ordinal, name).build())
+ }
+
+ // Map of supported device states supplied by DeviceStateManager to WM Jetpack posture.
+ private val DEVICE_STATE_POSTURES =
+ arrayOf(
+ "${SupportedDeviceStates.CLOSED}:$COMMON_STATE_NO_FOLDING_FEATURES",
+ "${SupportedDeviceStates.HALF_OPENED}:$COMMON_STATE_HALF_OPENED",
+ "${SupportedDeviceStates.OPENED}:$COMMON_STATE_FLAT",
+ "${SupportedDeviceStates.REAR_DISPLAY}:$COMMON_STATE_NO_FOLDING_FEATURES",
+ "${SupportedDeviceStates.CONCURRENT}:$COMMON_STATE_USE_BASE_STATE",
+ )
+ private val DEVICE_STATE_HALF_OPENED = SupportedDeviceStates.HALF_OPENED.toDeviceState()
+ private val DEVICE_STATE_OPENED = SupportedDeviceStates.OPENED.toDeviceState()
+
+ // WindowsManager Jetpack display features.
+ private val DISPLAY_FEATURES = "fold-[1104,0,1104,1848]"
+ private val DISPLAY_FEATURES_HALF_OPENED_HINGE = "$DISPLAY_FEATURES-half-opened"
+ private val HALF_OPENED_FOLDING_FEATURES = CommonFoldingFeature.parseListFromString(
+ DISPLAY_FEATURES,
+ COMMON_STATE_HALF_OPENED,
+ )
+ private val UNKNOWN_STATE_FOLDING_FEATURES = CommonFoldingFeature.parseListFromString(
+ DISPLAY_FEATURES,
+ COMMON_STATE_UNKNOWN,
+ )
+ }
+}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
index 7fab371cb790..bc4916a607a3 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
@@ -535,7 +535,8 @@ public class TaskFragmentContainerTest {
// container1.
container2.setInfo(mTransaction, mInfo);
- assertTrue(container2.hasActivity(mActivity.getActivityToken()));
+ assertTrue(container1.hasActivity(mActivity.getActivityToken()));
+ assertFalse(container2.hasActivity(mActivity.getActivityToken()));
// When the pending appeared record is removed from container1, we respect the appeared
// record in container2.
container1.removePendingAppearedActivity(mActivity.getActivityToken());
diff --git a/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml b/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml
index 045b975a854e..462a49ccb1eb 100644
--- a/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml
+++ b/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml
@@ -99,11 +99,11 @@
</LinearLayout>
- <FrameLayout
+
+ <LinearLayout
android:minHeight="@dimen/letterbox_restart_dialog_button_height"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- style="?android:attr/buttonBarButtonStyle"
android:layout_gravity="end">
<Button
@@ -133,7 +133,7 @@
android:text="@string/letterbox_restart_restart"
android:contentDescription="@string/letterbox_restart_restart"/>
- </FrameLayout>
+ </LinearLayout>
</LinearLayout>
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java
new file mode 100644
index 000000000000..26aae2d2aa78
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java
@@ -0,0 +1,30 @@
+/*
+ * 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.shared;
+
+import com.android.wm.shell.shared.annotations.ExternalThread;
+
+/**
+ * Listener to get focus-related transition callbacks.
+ */
+@ExternalThread
+public interface FocusTransitionListener {
+ /**
+ * Called when a transition changes the top, focused display.
+ */
+ void onFocusedDisplayChanged(int displayId);
+}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IFocusTransitionListener.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IFocusTransitionListener.aidl
new file mode 100644
index 000000000000..b91d5b6e2769
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IFocusTransitionListener.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.shared;
+
+/**
+ * Listener interface that to get focus-related transition callbacks.
+ */
+oneway interface IFocusTransitionListener {
+
+ /**
+ * Called when a transition changes the top, focused display.
+ */
+ void onFocusedDisplayChanged(int displayId);
+}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl
index 3256abf09116..02615a96a86c 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl
@@ -20,6 +20,7 @@ import android.view.SurfaceControl;
import android.window.RemoteTransition;
import android.window.TransitionFilter;
+import com.android.wm.shell.shared.IFocusTransitionListener;
import com.android.wm.shell.shared.IHomeTransitionListener;
/**
@@ -59,4 +60,9 @@ interface IShellTransitions {
*/
oneway void registerRemoteForTakeover(in TransitionFilter filter,
in RemoteTransition remoteTransition) = 6;
+
+ /**
+ * Set listener that will receive callbacks about transitions involving focus switch.
+ */
+ oneway void setFocusTransitionListener(in IFocusTransitionListener listener) = 7;
}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java
index 6d4ab4c1bd09..2db4311fb771 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java
@@ -22,6 +22,8 @@ import android.window.TransitionFilter;
import com.android.wm.shell.shared.annotations.ExternalThread;
+import java.util.concurrent.Executor;
+
/**
* Interface to manage remote transitions.
*/
@@ -44,4 +46,15 @@ public interface ShellTransitions {
* Unregisters a remote transition for all operations.
*/
default void unregisterRemote(@NonNull RemoteTransition remoteTransition) {}
+
+ /**
+ * Sets listener that will receive callbacks about transitions involving focus switch.
+ */
+ default void setFocusTransitionListener(@NonNull FocusTransitionListener listener,
+ Executor executor) {}
+
+ /**
+ * Unsets listener that will receive callbacks about transitions involving focus switch.
+ */
+ default void unsetFocusTransitionListener(@NonNull FocusTransitionListener listener) {}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index bec2ea58e106..4227a6e2903f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -123,6 +123,7 @@ import com.android.wm.shell.sysui.ShellInterface;
import com.android.wm.shell.taskview.TaskViewFactory;
import com.android.wm.shell.taskview.TaskViewFactoryController;
import com.android.wm.shell.taskview.TaskViewTransitions;
+import com.android.wm.shell.transition.FocusTransitionObserver;
import com.android.wm.shell.transition.HomeTransitionObserver;
import com.android.wm.shell.transition.MixedTransitionHandler;
import com.android.wm.shell.transition.Transitions;
@@ -742,14 +743,15 @@ public abstract class WMShellBaseModule {
@ShellMainThread Handler mainHandler,
@ShellAnimationThread ShellExecutor animExecutor,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
- HomeTransitionObserver homeTransitionObserver) {
+ HomeTransitionObserver homeTransitionObserver,
+ FocusTransitionObserver focusTransitionObserver) {
if (!context.getResources().getBoolean(R.bool.config_registerShellTransitionsOnInit)) {
// TODO(b/238217847): Force override shell init if registration is disabled
shellInit = new ShellInit(mainExecutor);
}
return new Transitions(context, shellInit, shellCommandHandler, shellController, organizer,
pool, displayController, mainExecutor, mainHandler, animExecutor,
- rootTaskDisplayAreaOrganizer, homeTransitionObserver);
+ rootTaskDisplayAreaOrganizer, homeTransitionObserver, focusTransitionObserver);
}
@WMSingleton
@@ -761,6 +763,12 @@ public abstract class WMShellBaseModule {
@WMSingleton
@Provides
+ static FocusTransitionObserver provideFocusTransitionObserver() {
+ return new FocusTransitionObserver();
+ }
+
+ @WMSingleton
+ @Provides
static TaskViewTransitions provideTaskViewTransitions(Transitions transitions) {
return new TaskViewTransitions(transitions);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 968f40c3df5d..afa27f9f1309 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -43,6 +43,7 @@ import android.view.Display.DEFAULT_DISPLAY
import android.view.DragEvent
import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_CLOSE
import android.view.WindowManager.TRANSIT_NONE
import android.view.WindowManager.TRANSIT_OPEN
import android.view.WindowManager.TRANSIT_TO_FRONT
@@ -1061,7 +1062,10 @@ class DesktopTasksController(
// Check if freeform task launch during recents should be handled
shouldHandleMidRecentsFreeformLaunch -> handleMidRecentsFreeformTaskLaunch(task)
// Check if the closing task needs to be handled
- TransitionUtil.isClosingType(request.type) -> handleTaskClosing(task)
+ TransitionUtil.isClosingType(request.type) -> handleTaskClosing(
+ task,
+ request.type
+ )
// Check if the top task shouldn't be allowed to enter desktop mode
isIncompatibleTask(task) -> handleIncompatibleTaskLaunch(task)
// Check if fullscreen task should be updated
@@ -1288,7 +1292,10 @@ class DesktopTasksController(
}
/** Handle task closing by removing wallpaper activity if it's the last active task */
- private fun handleTaskClosing(task: RunningTaskInfo): WindowContainerTransaction? {
+ private fun handleTaskClosing(
+ task: RunningTaskInfo,
+ transitionType: Int
+ ): WindowContainerTransaction? {
logV("handleTaskClosing")
if (!isDesktopModeShowing(task.displayId))
return null
@@ -1301,9 +1308,10 @@ class DesktopTasksController(
removeWallpaperActivity(wct)
}
taskRepository.addClosingTask(task.displayId, task.taskId)
- // If a CLOSE or TO_BACK is triggered on a desktop task, remove the task.
+ // If a CLOSE is triggered on a desktop task, remove the task.
if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue() &&
- taskRepository.isVisibleTask(task.taskId)
+ taskRepository.isVisibleTask(task.taskId) &&
+ transitionType == TRANSIT_CLOSE
) {
wct.removeTask(task.token)
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
index 0841628853a3..4796c4d0655a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
@@ -16,16 +16,19 @@
package com.android.wm.shell.desktopmode
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.content.Context
import android.os.IBinder
import android.view.SurfaceControl
import android.view.WindowManager
+import android.view.WindowManager.TRANSIT_TO_BACK
import android.window.TransitionInfo
import android.window.WindowContainerTransaction
+import android.window.flags.DesktopModeFlags
+import android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
-import android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.Transitions
@@ -64,6 +67,30 @@ class DesktopTasksTransitionObserver(
) {
// TODO: b/332682201 Update repository state
updateWallpaperToken(info)
+
+ if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
+ handleBackNavigation(info)
+ }
+ }
+
+ private fun handleBackNavigation(info: TransitionInfo) {
+ // When default back navigation happens, transition type is TO_BACK and the change is
+ // TO_BACK. Mark the task going to back as minimized.
+ if (info.type == TRANSIT_TO_BACK) {
+ for (change in info.changes) {
+ val taskInfo = change.taskInfo
+ if (taskInfo == null || taskInfo.taskId == -1) {
+ continue
+ }
+
+ if (desktopModeTaskRepository.getVisibleTaskCount(taskInfo.displayId) > 0 &&
+ change.mode == TRANSIT_TO_BACK &&
+ taskInfo.windowingMode == WINDOWING_MODE_FREEFORM
+ ) {
+ desktopModeTaskRepository.minimizeTask(taskInfo.displayId, taskInfo.taskId)
+ }
+ }
+ }
}
override fun onTransitionStarting(transition: IBinder) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java
new file mode 100644
index 000000000000..2f5059f3161c
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java
@@ -0,0 +1,142 @@
+/*
+ * 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.transition;
+
+import static android.view.Display.INVALID_DISPLAY;
+import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
+
+import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions;
+import static com.android.wm.shell.transition.Transitions.TransitionObserver;
+
+import android.annotation.NonNull;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Slog;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+
+import com.android.wm.shell.shared.FocusTransitionListener;
+import com.android.wm.shell.shared.IFocusTransitionListener;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * The {@link TransitionObserver} that observes for transitions involving focus switch.
+ * It reports transitions to callers outside of the process via {@link IFocusTransitionListener},
+ * and callers within the process via {@link FocusTransitionListener}.
+ */
+public class FocusTransitionObserver implements TransitionObserver {
+ private static final String TAG = FocusTransitionObserver.class.getSimpleName();
+
+ private IFocusTransitionListener mRemoteListener;
+ private final Map<FocusTransitionListener, Executor> mLocalListeners =
+ new HashMap<>();
+
+ private int mFocusedDisplayId = INVALID_DISPLAY;
+
+ public FocusTransitionObserver() {}
+
+ @Override
+ public void onTransitionReady(@NonNull IBinder transition,
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction) {
+ final List<TransitionInfo.Change> changes = info.getChanges();
+ for (int i = changes.size() - 1; i >= 0; i--) {
+ final TransitionInfo.Change change = changes.get(i);
+ final RunningTaskInfo task = change.getTaskInfo();
+ if (task != null && task.isFocused && change.hasFlags(FLAG_MOVED_TO_TOP)) {
+ if (mFocusedDisplayId != task.displayId) {
+ mFocusedDisplayId = task.displayId;
+ notifyFocusedDisplayChanged();
+ }
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onTransitionStarting(@NonNull IBinder transition) {}
+
+ @Override
+ public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {}
+
+ @Override
+ public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) {}
+
+ /**
+ * Sets the focus transition listener that receives any transitions resulting in focus switch.
+ * This is for calls from outside the Shell, within the host process.
+ *
+ */
+ public void setLocalFocusTransitionListener(FocusTransitionListener listener,
+ Executor executor) {
+ if (!enableDisplayFocusInShellTransitions()) {
+ return;
+ }
+ mLocalListeners.put(listener, executor);
+ executor.execute(() -> listener.onFocusedDisplayChanged(mFocusedDisplayId));
+ }
+
+ /**
+ * Sets the focus transition listener that receives any transitions resulting in focus switch.
+ * This is for calls from outside the Shell, within the host process.
+ *
+ */
+ public void unsetLocalFocusTransitionListener(FocusTransitionListener listener) {
+ if (!enableDisplayFocusInShellTransitions()) {
+ return;
+ }
+ mLocalListeners.remove(listener);
+ }
+
+ /**
+ * Sets the focus transition listener that receives any transitions resulting in focus switch.
+ * This is for calls from outside the host process.
+ */
+ public void setRemoteFocusTransitionListener(Transitions transitions,
+ IFocusTransitionListener listener) {
+ if (!enableDisplayFocusInShellTransitions()) {
+ return;
+ }
+ mRemoteListener = listener;
+ notifyFocusedDisplayChangedToRemote();
+ }
+
+ /**
+ * Notifies the listener that display focus has changed.
+ */
+ public void notifyFocusedDisplayChanged() {
+ notifyFocusedDisplayChangedToRemote();
+ mLocalListeners.forEach((listener, executor) ->
+ executor.execute(() -> listener.onFocusedDisplayChanged(mFocusedDisplayId)));
+ }
+
+ private void notifyFocusedDisplayChangedToRemote() {
+ if (mRemoteListener != null) {
+ try {
+ mRemoteListener.onFocusedDisplayChanged(mFocusedDisplayId);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed call notifyFocusedDisplayChangedToRemote", e);
+ }
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index d03832d3e85e..d280dcd252b4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -87,6 +87,8 @@ import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.FocusTransitionListener;
+import com.android.wm.shell.shared.IFocusTransitionListener;
import com.android.wm.shell.shared.IHomeTransitionListener;
import com.android.wm.shell.shared.IShellTransitions;
import com.android.wm.shell.shared.ShellTransitions;
@@ -103,6 +105,7 @@ import com.android.wm.shell.transition.tracing.TransitionTracer;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.concurrent.Executor;
/**
* Plays transition animations. Within this player, each transition has a lifecycle.
@@ -224,6 +227,7 @@ public class Transitions implements RemoteCallable<Transitions>,
private final ArrayList<TransitionObserver> mObservers = new ArrayList<>();
private HomeTransitionObserver mHomeTransitionObserver;
+ private FocusTransitionObserver mFocusTransitionObserver;
/** List of {@link Runnable} instances to run when the last active transition has finished. */
private final ArrayList<Runnable> mRunWhenIdleQueue = new ArrayList<>();
@@ -309,10 +313,12 @@ public class Transitions implements RemoteCallable<Transitions>,
@NonNull ShellExecutor mainExecutor,
@NonNull Handler mainHandler,
@NonNull ShellExecutor animExecutor,
- @NonNull HomeTransitionObserver observer) {
+ @NonNull HomeTransitionObserver homeTransitionObserver,
+ @NonNull FocusTransitionObserver focusTransitionObserver) {
this(context, shellInit, new ShellCommandHandler(), shellController, organizer, pool,
displayController, mainExecutor, mainHandler, animExecutor,
- new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit), observer);
+ new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit),
+ homeTransitionObserver, focusTransitionObserver);
}
public Transitions(@NonNull Context context,
@@ -326,7 +332,8 @@ public class Transitions implements RemoteCallable<Transitions>,
@NonNull Handler mainHandler,
@NonNull ShellExecutor animExecutor,
@NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer,
- @NonNull HomeTransitionObserver observer) {
+ @NonNull HomeTransitionObserver homeTransitionObserver,
+ @NonNull FocusTransitionObserver focusTransitionObserver) {
mOrganizer = organizer;
mContext = context;
mMainExecutor = mainExecutor;
@@ -345,7 +352,8 @@ public class Transitions implements RemoteCallable<Transitions>,
mHandlers.add(mRemoteTransitionHandler);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Remote");
shellInit.addInitCallback(this::onInit, this);
- mHomeTransitionObserver = observer;
+ mHomeTransitionObserver = homeTransitionObserver;
+ mFocusTransitionObserver = focusTransitionObserver;
if (android.tracing.Flags.perfettoTransitionTracing()) {
mTransitionTracer = new PerfettoTransitionTracer();
@@ -384,6 +392,8 @@ public class Transitions implements RemoteCallable<Transitions>,
mShellCommandHandler.addCommandCallback("transitions", this, this);
mShellCommandHandler.addDumpCallback(this::dump, this);
+
+ registerObserver(mFocusTransitionObserver);
}
public boolean isRegistered() {
@@ -1573,6 +1583,21 @@ public class Transitions implements RemoteCallable<Transitions>,
mMainExecutor.execute(
() -> mRemoteTransitionHandler.removeFiltered(remoteTransition));
}
+
+ @Override
+ public void setFocusTransitionListener(FocusTransitionListener listener,
+ Executor executor) {
+ mMainExecutor.execute(() ->
+ mFocusTransitionObserver.setLocalFocusTransitionListener(listener, executor));
+
+ }
+
+ @Override
+ public void unsetFocusTransitionListener(FocusTransitionListener listener) {
+ mMainExecutor.execute(() ->
+ mFocusTransitionObserver.unsetLocalFocusTransitionListener(listener));
+
+ }
}
/**
@@ -1634,6 +1659,15 @@ public class Transitions implements RemoteCallable<Transitions>,
}
@Override
+ public void setFocusTransitionListener(IFocusTransitionListener listener) {
+ executeRemoteCallWithTaskPermission(mTransitions, "setFocusTransitionListener",
+ (transitions) -> {
+ transitions.mFocusTransitionObserver.setRemoteFocusTransitionListener(
+ transitions, listener);
+ });
+ }
+
+ @Override
public SurfaceControl getHomeTaskOverlayContainer() {
SurfaceControl[] result = new SurfaceControl[1];
executeRemoteCallWithTaskPermission(mTransitions, "getHomeTaskOverlayContainer",
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index ee545209904f..29aea006c076 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -2086,16 +2086,13 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- @EnableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_withBackNav_removesTask() {
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
+ fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_removesTask() {
val task = setUpFreeformTask()
val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
- assertNotNull(result, "Should handle request").assertRemoveAt(0, task.token)
+ assertNull(result, "Should not handle request")
}
@Test
@@ -2137,26 +2134,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- @EnableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_backTransition_singleTask_withWallpaper_withBackNav_removesWallpaperAndTask() {
- val task = setUpFreeformTask()
- val wallpaperToken = MockToken().token()
-
- taskRepository.wallpaperActivityToken = wallpaperToken
- val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
-
- // Should create remove wallpaper transaction
- assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
- result.assertRemoveAt(index = 1, task.token)
- }
-
- @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
- fun handleRequest_backTransition_singleTaskWithToken_noBackNav_removesWallpaper() {
+ fun handleRequest_backTransition_singleTaskWithToken_removesWallpaper() {
val task = setUpFreeformTask()
val wallpaperToken = MockToken().token()
@@ -2183,23 +2162,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- @EnableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_backTransition_multipleTasks_withWallpaper_withBackNav_removesTask() {
- val task1 = setUpFreeformTask()
- setUpFreeformTask()
-
- taskRepository.wallpaperActivityToken = MockToken().token()
- val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
-
- assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, task1.token)
- }
-
- @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
fun handleRequest_backTransition_multipleTasks_noBackNav_doesNotHandle() {
val task1 = setUpFreeformTask()
setUpFreeformTask()
@@ -2226,29 +2189,11 @@ class DesktopTasksControllerTest : ShellTestCase() {
// Should create remove wallpaper transaction
assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
- result.assertRemoveAt(index = 1, task1.token)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
- fun handleRequest_backTransition_multipleTasksSingleNonClosing_noBackNav_removesWallpaper() {
- val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
- val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
- val wallpaperToken = MockToken().token()
-
- taskRepository.wallpaperActivityToken = wallpaperToken
- taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
- val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
-
- // Should create remove wallpaper transaction
- assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
}
@Test
@EnableFlags(
Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
)
fun handleRequest_backTransition_multipleTasksSingleNonMinimized_removesWallpaperAndTask() {
val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
@@ -2261,23 +2206,6 @@ class DesktopTasksControllerTest : ShellTestCase() {
// Should create remove wallpaper transaction
assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
- result.assertRemoveAt(index = 1, task1.token)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
- fun handleRequest_backTransition_multipleTasksSingleNonMinimized_noBackNav_removesWallpaper() {
- val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
- val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
- val wallpaperToken = MockToken().token()
-
- taskRepository.wallpaperActivityToken = wallpaperToken
- taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
- val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
-
- // Should create remove wallpaper transaction
- assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
new file mode 100644
index 000000000000..c989d1640f80
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
@@ -0,0 +1,138 @@
+/*
+ * 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.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.platform.test.annotations.EnableFlags
+import android.view.Display.DEFAULT_DISPLAY
+import android.view.WindowManager.TRANSIT_TO_BACK
+import android.window.IWindowContainerToken
+import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
+import android.window.WindowContainerToken
+import com.android.modules.utils.testing.ExtendedMockitoRule
+import com.android.window.flags.Flags
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.Transitions
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+class DesktopTasksTransitionObserverTest {
+
+ @JvmField
+ @Rule
+ val extendedMockitoRule =
+ ExtendedMockitoRule.Builder(this)
+ .mockStatic(DesktopModeStatus::class.java)
+ .build()!!
+
+ private val testExecutor = mock<ShellExecutor>()
+ private val mockShellInit = mock<ShellInit>()
+ private val transitions = mock<Transitions>()
+ private val context = mock<Context>()
+ private val shellTaskOrganizer = mock<ShellTaskOrganizer>()
+ private val taskRepository = mock<DesktopModeTaskRepository>()
+
+ private lateinit var transitionObserver: DesktopTasksTransitionObserver
+ private lateinit var shellInit: ShellInit
+
+ @Before
+ fun setup() {
+ whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true)
+ shellInit = spy(ShellInit(testExecutor))
+
+ transitionObserver =
+ DesktopTasksTransitionObserver(
+ context, taskRepository, transitions, shellTaskOrganizer, shellInit
+ )
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+ fun backNavigation_taskMinimized() {
+ val task = createTaskInfo(1)
+ whenever(taskRepository.getVisibleTaskCount(any())).thenReturn(1)
+
+ transitionObserver.onTransitionReady(
+ transition = mock(),
+ info =
+ createBackNavigationTransition(task),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ )
+
+ verify(taskRepository).minimizeTask(task.displayId, task.taskId)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+ fun backNavigation_nullTaskInfo_taskNotMinimized() {
+ val task = createTaskInfo(1)
+ whenever(taskRepository.getVisibleTaskCount(any())).thenReturn(1)
+
+ transitionObserver.onTransitionReady(
+ transition = mock(),
+ info =
+ createBackNavigationTransition(null),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ )
+
+ verify(taskRepository, never()).minimizeTask(task.displayId, task.taskId)
+ }
+
+ private fun createBackNavigationTransition(
+ task: RunningTaskInfo?
+ ): TransitionInfo {
+ return TransitionInfo(TRANSIT_TO_BACK, 0 /* flags */).apply {
+ addChange(
+ Change(mock(), mock()).apply {
+ mode = TRANSIT_TO_BACK
+ parent = null
+ taskInfo = task
+ flags = flags
+ }
+ )
+ }
+ }
+
+ private fun createTaskInfo(id: Int) =
+ RunningTaskInfo().apply {
+ taskId = id
+ displayId = DEFAULT_DISPLAY
+ configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+ token = WindowContainerToken(Mockito.mock(IWindowContainerToken::class.java))
+ baseIntent = Intent().apply {
+ component = ComponentName("package", "component.name")
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index a6c16c43c8cb..67eda8bfecd1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -74,6 +74,7 @@ import com.android.wm.shell.splitscreen.SplitScreen.SplitScreenListener;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.DefaultMixedHandler;
+import com.android.wm.shell.transition.FocusTransitionObserver;
import com.android.wm.shell.transition.HomeTransitionObserver;
import com.android.wm.shell.transition.Transitions;
@@ -429,7 +430,8 @@ public class StageCoordinatorTests extends ShellTestCase {
ShellInit shellInit = new ShellInit(mMainExecutor);
final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class),
mTaskOrganizer, mTransactionPool, mock(DisplayController.class), mMainExecutor,
- mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
+ mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
+ mock(FocusTransitionObserver.class));
shellInit.init();
return t;
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java
new file mode 100644
index 000000000000..d37b4cf4b4b3
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java
@@ -0,0 +1,155 @@
+/*
+ * 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.transition;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+import android.window.TransitionInfo.TransitionMode;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.window.flags.Flags;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.shared.IFocusTransitionListener;
+import com.android.wm.shell.shared.TransactionPool;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests for the focus transition observer.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@RequiresFlagsEnabled(Flags.FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS)
+public class FocusTransitionObserverTest extends ShellTestCase {
+
+ static final int SECONDARY_DISPLAY_ID = 1;
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+ private IFocusTransitionListener mListener;
+ private Transitions mTransition;
+ private FocusTransitionObserver mFocusTransitionObserver;
+
+ @Before
+ public void setUp() {
+ mListener = mock(IFocusTransitionListener.class);
+ when(mListener.asBinder()).thenReturn(mock(IBinder.class));
+
+ mFocusTransitionObserver = new FocusTransitionObserver();
+ mTransition =
+ new Transitions(InstrumentationRegistry.getInstrumentation().getTargetContext(),
+ mock(ShellInit.class), mock(ShellController.class),
+ mock(ShellTaskOrganizer.class), mock(TransactionPool.class),
+ mock(DisplayController.class), new TestShellExecutor(),
+ new Handler(Looper.getMainLooper()), new TestShellExecutor(),
+ mock(HomeTransitionObserver.class),
+ mFocusTransitionObserver);
+ mFocusTransitionObserver.setRemoteFocusTransitionListener(mTransition, mListener);
+ }
+
+ @Test
+ public void testTransitionWithMovedToFrontFlagChangesDisplayFocus() throws RemoteException {
+ final IBinder binder = mock(IBinder.class);
+ final SurfaceControl.Transaction tx = mock(SurfaceControl.Transaction.class);
+
+ // Open a task on the default display, which doesn't change display focus because the
+ // default display already has it.
+ TransitionInfo info = mock(TransitionInfo.class);
+ final List<TransitionInfo.Change> changes = new ArrayList<>();
+ setupChange(changes, 123 /* taskId */, TRANSIT_OPEN, DEFAULT_DISPLAY,
+ true /* focused */);
+ when(info.getChanges()).thenReturn(changes);
+ mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
+ verify(mListener, never()).onFocusedDisplayChanged(SECONDARY_DISPLAY_ID);
+ clearInvocations(mListener);
+
+ // Open a new task on the secondary display and verify display focus changes to the display.
+ changes.clear();
+ setupChange(changes, 456 /* taskId */, TRANSIT_OPEN, SECONDARY_DISPLAY_ID,
+ true /* focused */);
+ when(info.getChanges()).thenReturn(changes);
+ mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
+ verify(mListener, times(1)).onFocusedDisplayChanged(SECONDARY_DISPLAY_ID);
+ clearInvocations(mListener);
+
+ // Open the first task to front and verify display focus goes back to the default display.
+ changes.clear();
+ setupChange(changes, 123 /* taskId */, TRANSIT_TO_FRONT, DEFAULT_DISPLAY,
+ true /* focused */);
+ when(info.getChanges()).thenReturn(changes);
+ mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
+ verify(mListener, times(1)).onFocusedDisplayChanged(DEFAULT_DISPLAY);
+ clearInvocations(mListener);
+
+ // Open another task on the default display and verify no display focus switch as it's
+ // already on the default display.
+ changes.clear();
+ setupChange(changes, 789 /* taskId */, TRANSIT_OPEN, DEFAULT_DISPLAY,
+ true /* focused */);
+ when(info.getChanges()).thenReturn(changes);
+ mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
+ verify(mListener, never()).onFocusedDisplayChanged(DEFAULT_DISPLAY);
+ }
+
+ private void setupChange(List<TransitionInfo.Change> changes, int taskId,
+ @TransitionMode int mode, int displayId, boolean focused) {
+ TransitionInfo.Change change = mock(TransitionInfo.Change.class);
+ RunningTaskInfo taskInfo = mock(RunningTaskInfo.class);
+ taskInfo.taskId = taskId;
+ taskInfo.isFocused = focused;
+ when(change.hasFlags(FLAG_MOVED_TO_TOP)).thenReturn(focused);
+ taskInfo.displayId = displayId;
+ when(change.getTaskInfo()).thenReturn(taskInfo);
+ when(change.getMode()).thenReturn(mode);
+ changes.add(change);
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
index 8f49de0a98fb..8dfdfb4dcbcf 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
@@ -100,7 +100,8 @@ public class HomeTransitionObserverTest extends ShellTestCase {
mHomeTransitionObserver = new HomeTransitionObserver(mContext, mMainExecutor);
mTransition = new Transitions(mContext, mock(ShellInit.class), mock(ShellController.class),
mOrganizer, mTransactionPool, mDisplayController, mMainExecutor,
- mMainHandler, mAnimExecutor, mHomeTransitionObserver);
+ mMainHandler, mAnimExecutor, mHomeTransitionObserver,
+ mock(FocusTransitionObserver.class));
mHomeTransitionObserver.setHomeTransitionListener(mTransition, mListener);
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index aea14b900647..6cde0569796d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -158,7 +158,8 @@ public class ShellTransitionTests extends ShellTestCase {
ShellInit shellInit = mock(ShellInit.class);
final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class),
mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor,
- mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
+ mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
+ mock(FocusTransitionObserver.class));
// One from Transitions, one from RootTaskDisplayAreaOrganizer
verify(shellInit).addInitCallback(any(), eq(t));
verify(shellInit).addInitCallback(any(), isA(RootTaskDisplayAreaOrganizer.class));
@@ -170,7 +171,8 @@ public class ShellTransitionTests extends ShellTestCase {
ShellController shellController = mock(ShellController.class);
final Transitions t = new Transitions(mContext, shellInit, shellController,
mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor,
- mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
+ mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
+ mock(FocusTransitionObserver.class));
shellInit.init();
verify(shellController, times(1)).addExternalInterface(
eq(ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS), any(), any());
@@ -1238,7 +1240,8 @@ public class ShellTransitionTests extends ShellTestCase {
final Transitions transitions =
new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer,
mTransactionPool, createTestDisplayController(), mMainExecutor,
- mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
+ mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
+ mock(FocusTransitionObserver.class));
final RecentsTransitionHandler recentsHandler =
new RecentsTransitionHandler(shellInit, mock(ShellTaskOrganizer.class), transitions,
mock(RecentTasksController.class), mock(HomeTransitionObserver.class));
@@ -1780,7 +1783,8 @@ public class ShellTransitionTests extends ShellTestCase {
ShellInit shellInit = new ShellInit(mMainExecutor);
final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class),
mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor,
- mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
+ mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
+ mock(FocusTransitionObserver.class));
shellInit.init();
return t;
}
diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt
index 504e3290b0ae..3ed33db2222e 100644
--- a/libs/appfunctions/api/current.txt
+++ b/libs/appfunctions/api/current.txt
@@ -3,13 +3,15 @@ package com.google.android.appfunctions.sidecar {
public final class AppFunctionManager {
ctor public AppFunctionManager(android.content.Context);
- method public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+ method public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+ method @Deprecated public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
}
public abstract class AppFunctionService extends android.app.Service {
ctor public AppFunctionService();
method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
- method @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+ method @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+ method @Deprecated @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
field @NonNull public static final String BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE";
field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
}
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
index b1dd4676a35e..815fe05cc3ab 100644
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
@@ -19,12 +19,12 @@ package com.google.android.appfunctions.sidecar;
import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.content.Context;
+import android.os.CancellationSignal;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
-
/**
* Provides app functions related functionalities.
*
@@ -45,7 +45,7 @@ public final class AppFunctionManager {
*
* @param context A {@link Context}.
* @throws java.lang.IllegalStateException if the underlying {@link
- * android.app.appfunctions.AppFunctionManager} is not found.
+ * android.app.appfunctions.AppFunctionManager} is not found.
*/
public AppFunctionManager(Context context) {
mContext = Objects.requireNonNull(context);
@@ -66,6 +66,7 @@ public final class AppFunctionManager {
public void executeAppFunction(
@NonNull ExecuteAppFunctionRequest sidecarRequest,
@NonNull @CallbackExecutor Executor executor,
+ @NonNull CancellationSignal cancellationSignal,
@NonNull Consumer<ExecuteAppFunctionResponse> callback) {
Objects.requireNonNull(sidecarRequest);
Objects.requireNonNull(executor);
@@ -74,9 +75,40 @@ public final class AppFunctionManager {
android.app.appfunctions.ExecuteAppFunctionRequest platformRequest =
SidecarConverter.getPlatformExecuteAppFunctionRequest(sidecarRequest);
mManager.executeAppFunction(
- platformRequest, executor, (platformResponse) -> {
- callback.accept(SidecarConverter.getSidecarExecuteAppFunctionResponse(
- platformResponse));
+ platformRequest,
+ executor,
+ cancellationSignal,
+ (platformResponse) -> {
+ callback.accept(
+ SidecarConverter.getSidecarExecuteAppFunctionResponse(
+ platformResponse));
});
}
+
+ /**
+ * Executes the app function.
+ *
+ * <p>Proxies request and response to the underlying {@link
+ * android.app.appfunctions.AppFunctionManager#executeAppFunction}, converting the request and
+ * response in the appropriate type required by the function.
+ *
+ * @deprecated Use {@link #executeAppFunction(ExecuteAppFunctionRequest, Executor,
+ * CancellationSignal, Consumer)} instead. This method will be removed once usage references
+ * are updated.
+ */
+ @Deprecated
+ public void executeAppFunction(
+ @NonNull ExecuteAppFunctionRequest sidecarRequest,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
+ Objects.requireNonNull(sidecarRequest);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+
+ executeAppFunction(
+ sidecarRequest,
+ executor,
+ new CancellationSignal(),
+ callback);
+ }
}
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
index 65959dfdf561..6023c977bd76 100644
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
@@ -25,6 +25,7 @@ import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
+import android.os.CancellationSignal;
import java.util.function.Consumer;
@@ -69,10 +70,11 @@ public abstract class AppFunctionService extends Service {
private final Binder mBinder =
android.app.appfunctions.AppFunctionService.createBinder(
/* context= */ this,
- /* onExecuteFunction= */ (platformRequest, callback) -> {
+ /* onExecuteFunction= */ (platformRequest, cancellationSignal, callback) -> {
AppFunctionService.this.onExecuteFunction(
SidecarConverter.getSidecarExecuteAppFunctionRequest(
platformRequest),
+ cancellationSignal,
(sidecarResponse) -> {
callback.accept(
SidecarConverter.getPlatformExecuteAppFunctionResponse(
@@ -105,9 +107,42 @@ public abstract class AppFunctionService extends Service {
* result using the callback, no matter if the execution was successful or not.
*
* @param request The function execution request.
+ * @param cancellationSignal A {@link CancellationSignal} to cancel the request.
* @param callback A callback to report back the result.
*/
@MainThread
+ public void onExecuteFunction(
+ @NonNull ExecuteAppFunctionRequest request,
+ @NonNull CancellationSignal cancellationSignal,
+ @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
+ onExecuteFunction(request, callback);
+ }
+
+ /**
+ * Called by the system to execute a specific app function.
+ *
+ * <p>This method is triggered when the system requests your AppFunctionService to handle a
+ * particular function you have registered and made available.
+ *
+ * <p>To ensure proper routing of function requests, assign a unique identifier to each
+ * function. This identifier doesn't need to be globally unique, but it must be unique within
+ * your app. For example, a function to order food could be identified as "orderFood". In most
+ * cases this identifier should come from the ID automatically generated by the AppFunctions
+ * SDK. You can determine the specific function to invoke by calling {@link
+ * ExecuteAppFunctionRequest#getFunctionIdentifier()}.
+ *
+ * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker
+ * thread and dispatch the result with the given callback. You should always report back the
+ * result using the callback, no matter if the execution was successful or not.
+ *
+ * @param request The function execution request.
+ * @param callback A callback to report back the result.
+ *
+ * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, CancellationSignal,
+ * Consumer)} instead. This method will be removed once usage references are updated.
+ */
+ @MainThread
+ @Deprecated
public abstract void onExecuteFunction(
@NonNull ExecuteAppFunctionRequest request,
@NonNull Consumer<ExecuteAppFunctionResponse> callback);
diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index d4851e1ad698..af07686f064c 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -33,6 +33,7 @@ android_library {
"SettingsLibBarChartPreference",
"SettingsLibButtonPreference",
"SettingsLibBulletPreference",
+ "SettingsLibCardPreference",
"SettingsLibCollapsingToolbarBaseActivity",
"SettingsLibDeviceStateRotationLock",
"SettingsLibDisplayUtils",
diff --git a/packages/SettingsLib/CardPreference/Android.bp b/packages/SettingsLib/CardPreference/Android.bp
new file mode 100644
index 000000000000..1d871d168ee5
--- /dev/null
+++ b/packages/SettingsLib/CardPreference/Android.bp
@@ -0,0 +1,33 @@
+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"],
+}
+
+android_library {
+ name: "SettingsLibCardPreference",
+ use_resource_processor: true,
+ defaults: [
+ "SettingsLintDefaults",
+ ],
+
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+ resource_dirs: ["res"],
+
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.preference_preference",
+ "SettingsLibSettingsTheme",
+ ],
+ sdk_version: "system_current",
+ min_sdk_version: "21",
+ apex_available: [
+ "//apex_available:platform",
+ ],
+}
diff --git a/packages/SettingsLib/CardPreference/AndroidManifest.xml b/packages/SettingsLib/CardPreference/AndroidManifest.xml
new file mode 100644
index 000000000000..717f66e0296c
--- /dev/null
+++ b/packages/SettingsLib/CardPreference/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.settingslib.widget.preference.card">
+
+ <uses-sdk android:minSdkVersion="21" />
+
+</manifest>
diff --git a/packages/SettingsLib/CardPreference/res/layout/settingslib_expressive_preference_card.xml b/packages/SettingsLib/CardPreference/res/layout/settingslib_expressive_preference_card.xml
new file mode 100644
index 000000000000..716ed412eb5c
--- /dev/null
+++ b/packages/SettingsLib/CardPreference/res/layout/settingslib_expressive_preference_card.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<com.google.android.material.card.MaterialCardView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/SettingsLibCardStyle">
+
+ <LinearLayout
+ android:id="@+id/card_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:baselineAligned="false"
+ android:minHeight="@dimen/settingslib_expressive_space_large3"
+ android:paddingStart="@dimen/settingslib_expressive_space_small1"
+ android:paddingEnd="@dimen/settingslib_expressive_space_small1"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+
+ <LinearLayout
+ android:id="@+id/icon_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:minWidth="@dimen/settingslib_expressive_space_medium3"
+ android:minHeight="@dimen/settingslib_expressive_space_medium3"
+ android:gravity="center"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@android:id/icon"
+ android:src="@drawable/settingslib_arrow_drop_down"
+ android:layout_width="@dimen/settingslib_expressive_space_medium3"
+ android:layout_height="@dimen/settingslib_expressive_space_medium3"
+ android:scaleType="centerInside"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/text_container"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:paddingHorizontal="@dimen/settingslib_expressive_space_small1"
+ android:paddingVertical="@dimen/settingslib_expressive_space_small2"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hyphenationFrequency="normalFast"
+ android:lineBreakWordStyle="phrase"
+ android:textAppearance="@style/TextAppearance.CardTitle.SettingsLib"/>
+
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hyphenationFrequency="normalFast"
+ android:lineBreakWordStyle="phrase"
+ android:textAppearance="@style/TextAppearance.CardSummary.SettingsLib"/>
+
+ </LinearLayout>
+
+ <ImageView
+ android:id="@android:id/closeButton"
+ android:layout_width="@dimen/settingslib_expressive_space_medium4"
+ android:layout_height="@dimen/settingslib_expressive_space_medium4"
+ android:padding="@dimen/settingslib_expressive_space_extrasmall4"
+ android:layout_gravity="center"
+ android:src="@drawable/settingslib_expressive_icon_close"
+ android:background="?android:attr/selectableItemBackground" />
+
+ </LinearLayout>
+
+</com.google.android.material.card.MaterialCardView> \ No newline at end of file
diff --git a/packages/SettingsLib/CardPreference/res/values/styles_expressive.xml b/packages/SettingsLib/CardPreference/res/values/styles_expressive.xml
new file mode 100644
index 000000000000..4cbdea52d439
--- /dev/null
+++ b/packages/SettingsLib/CardPreference/res/values/styles_expressive.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+
+<resources>
+ <style name="TextAppearance.CardTitle.SettingsLib"
+ parent="@style/TextAppearance.PreferenceTitle.SettingsLib">
+ <item name="android:textColor">@color/settingslib_materialColorOnPrimary</item>
+ <item name="android:textSize">20sp</item>
+ </style>
+
+ <style name="TextAppearance.CardSummary.SettingsLib"
+ parent="@style/TextAppearance.PreferenceSummary.SettingsLib">
+ <item name="android:textColor">@color/settingslib_materialColorOnSecondary</item>
+ <item name="android:textSize">14sp</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/packages/SettingsLib/CardPreference/src/com/android/settingslib/widget/CardPreference.kt b/packages/SettingsLib/CardPreference/src/com/android/settingslib/widget/CardPreference.kt
new file mode 100644
index 000000000000..eb14746a0f22
--- /dev/null
+++ b/packages/SettingsLib/CardPreference/src/com/android/settingslib/widget/CardPreference.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settingslib.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
+import com.android.settingslib.widget.preference.card.R
+
+/**
+ * The CardPreference shows a card like suggestion in homepage, which also support dismiss.
+ */
+class CardPreference @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+ defStyleRes: Int = 0
+) : Preference(context, attrs, defStyleAttr, defStyleRes) {
+
+ init {
+ layoutResource = R.layout.settingslib_expressive_preference_card
+ }
+ private var dismissible = false
+ set(value) {
+ if (field != value) {
+ field = value
+ notifyChanged()
+ }
+ }
+
+ override fun onBindViewHolder(holder: PreferenceViewHolder) {
+ super.onBindViewHolder(holder)
+ holder.isDividerAllowedBelow = false
+ holder.isDividerAllowedAbove = false
+
+ holder.findViewById(android.R.id.closeButton)?.let { dismissButton ->
+ dismissButton.visibility = if (dismissible) View.VISIBLE else View.GONE
+ dismissButton.setOnClickListener {
+ isVisible = false
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml
index 816433c1a18b..dc2eb648f8c1 100644
--- a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml
@@ -193,4 +193,15 @@
<item name="android:layout_gravity">center</item>
<item name="trackTint">@color/settingslib_expressive_color_main_switch_track</item>
</style>
+
+ <style name="SettingsLibCardStyle" parent="">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_marginHorizontal">?android:attr/listPreferredItemPaddingStart</item>
+ <item name="android:layout_marginVertical">@dimen/settingslib_expressive_space_extrasmall4</item>
+ <item name="cardBackgroundColor">@color/settingslib_materialColorPrimary</item>
+ <item name="cardCornerRadius">@dimen/settingslib_expressive_radius_extralarge3</item>
+ <item name="cardElevation">0dp</item>
+ <item name="rippleColor">?android:attr/colorControlHighlight</item>
+ </style>
</resources> \ No newline at end of file
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt
index 460bf9993b41..965c97124329 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt
@@ -162,3 +162,6 @@ internal fun rememberSettingsTypography(): Typography {
/** Creates a new [TextStyle] which font weight set to medium. */
internal fun TextStyle.toMediumWeight() =
copy(fontWeight = FontWeight.Medium, letterSpacing = 0.01.em)
+
+internal fun TextStyle.toSemiBoldWeight() =
+ copy(fontWeight = FontWeight.SemiBold, letterSpacing = 0.01.em)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt
index 7e1df1694b10..5bb57b8ed1df 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt
@@ -110,7 +110,7 @@ private fun RowScope.ActionButton(actionButton: ActionButton) {
shape = RectangleShape,
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
- contentColor = MaterialTheme.colorScheme.onPrimary,
+ contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
disabledContainerColor = MaterialTheme.colorScheme.surface,
),
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp),
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt
index 94d2c210daab..f99d20669183 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt
@@ -41,9 +41,7 @@ import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled
internal fun NavigateBack() {
val navController = LocalNavController.current
val contentDescription = stringResource(R.string.abc_action_bar_up_description)
- BackAction(contentDescription) {
- navController.navigateBack()
- }
+ BackAction(contentDescription) { navController.navigateBack() }
}
/** Action that collapses the search bar. */
@@ -55,15 +53,35 @@ internal fun CollapseAction(onClick: () -> Unit) {
@Composable
private fun BackAction(contentDescription: String, onClick: () -> Unit) {
- IconButton(onClick) {
+ IconButton(
+ onClick = onClick,
+ modifier =
+ if (isSpaExpressiveEnabled)
+ Modifier
+ .padding(
+ start = SettingsDimension.paddingLarge,
+ end = SettingsDimension.paddingSmall,
+ top = SettingsDimension.paddingExtraSmall,
+ bottom = SettingsDimension.paddingExtraSmall,
+ )
+ .size(SettingsDimension.actionIconWidth, SettingsDimension.actionIconHeight)
+ .clip(SettingsShape.CornerExtraLarge)
+ else Modifier,
+ ) {
Icon(
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
contentDescription = contentDescription,
- modifier = if (isSpaExpressiveEnabled) Modifier
- .size(SettingsDimension.actionIconWidth, SettingsDimension.actionIconHeight)
- .clip(SettingsShape.CornerExtraLarge)
- .background(MaterialTheme.colorScheme.surfaceContainerHigh)
- .padding(SettingsDimension.actionIconPadding) else Modifier
+ modifier =
+ if (isSpaExpressiveEnabled)
+ Modifier
+ .size(
+ SettingsDimension.actionIconWidth,
+ SettingsDimension.actionIconHeight,
+ )
+ .clip(SettingsShape.CornerExtraLarge)
+ .background(MaterialTheme.colorScheme.surfaceContainerHighest)
+ .padding(SettingsDimension.actionIconPadding)
+ else Modifier,
)
}
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
index 2ae3b569bc70..2c55779c9a01 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
@@ -78,7 +78,9 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled
import com.android.settingslib.spa.framework.theme.settingsBackground
+import com.android.settingslib.spa.framework.theme.toSemiBoldWeight
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.roundToInt
@@ -116,8 +118,12 @@ internal fun CustomizedLargeTopAppBar(
) {
TwoRowsTopAppBar(
title = { Title(title = title, maxLines = 3) },
- titleTextStyle = MaterialTheme.typography.displaySmall,
- smallTitleTextStyle = MaterialTheme.typography.titleMedium,
+ titleTextStyle =
+ if (isSpaExpressiveEnabled) MaterialTheme.typography.displaySmall.toSemiBoldWeight()
+ else MaterialTheme.typography.displaySmall,
+ smallTitleTextStyle =
+ if (isSpaExpressiveEnabled) MaterialTheme.typography.titleLarge.toSemiBoldWeight()
+ else MaterialTheme.typography.titleLarge,
titleBottomPadding = LargeTitleBottomPadding,
smallTitle = { Title(title = title, maxLines = 1) },
modifier = modifier,
@@ -136,7 +142,9 @@ private fun Title(title: String, maxLines: Int = Int.MAX_VALUE) {
text = title,
modifier =
Modifier.padding(
- start = SettingsDimension.itemPaddingAround,
+ start =
+ if (isSpaExpressiveEnabled) SettingsDimension.paddingExtraSmall
+ else SettingsDimension.itemPaddingAround,
end = SettingsDimension.itemPaddingEnd,
)
.semantics { heading() },
@@ -194,7 +202,7 @@ private class TopAppBarColors(
return lerp(
containerColor,
scrolledContainerColor,
- FastOutLinearInEasing.transform(colorTransitionFraction)
+ FastOutLinearInEasing.transform(colorTransitionFraction),
)
}
@@ -241,7 +249,7 @@ private fun SingleRowTopAppBar(
Row(
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
- content = actions
+ content = actions,
)
}
@@ -296,7 +304,7 @@ private fun TwoRowsTopAppBar(
windowInsets: WindowInsets,
colors: TopAppBarColors,
pinnedHeight: Dp,
- scrollBehavior: TopAppBarScrollBehavior?
+ scrollBehavior: TopAppBarScrollBehavior?,
) {
if (MaxHeightWithoutTitle <= pinnedHeight) {
throw IllegalArgumentException(
@@ -333,7 +341,7 @@ private fun TwoRowsTopAppBar(
Row(
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
- content = actions
+ content = actions,
)
}
val topTitleAlpha = { TopTitleAlphaEasing.transform(colorTransitionFraction()) }
@@ -356,9 +364,9 @@ private fun TwoRowsTopAppBar(
scrollBehavior.state,
velocity,
scrollBehavior.flingAnimationSpec,
- scrollBehavior.snapAnimationSpec
+ scrollBehavior.snapAnimationSpec,
)
- }
+ },
)
} else {
Modifier
@@ -412,7 +420,8 @@ private fun TwoRowsTopAppBar(
val measuredMaxHeightPx =
density.run {
MaxHeightWithoutTitle.toPx() +
- coordinates.size.height.toFloat()
+ coordinates.size.height.toFloat() +
+ titleBaselineHeight.toPx()
}
// Allow larger max height for multi-line title, but do not reduce
// max height to prevent flaky.
@@ -430,7 +439,7 @@ private fun TwoRowsTopAppBar(
titleBottomPadding = titleBottomPaddingPx,
hideTitleSemantics = hideBottomRowSemantics,
navigationIcon = {},
- actions = {}
+ actions = {},
)
}
}
@@ -485,7 +494,7 @@ private fun TopAppBarLayout(
Box(Modifier.layoutId("navigationIcon").padding(start = TopAppBarHorizontalPadding)) {
CompositionLocalProvider(
LocalContentColor provides navigationIconContentColor,
- content = navigationIcon
+ content = navigationIcon,
)
}
Box(
@@ -504,18 +513,18 @@ private fun TopAppBarLayout(
fontScale = if (titleScaleDisabled) 1f else fontScale,
)
},
- content = title
+ content = title,
)
}
}
Box(Modifier.layoutId("actionIcons").padding(end = TopAppBarHorizontalPadding)) {
CompositionLocalProvider(
LocalContentColor provides actionIconContentColor,
- content = actions
+ content = actions,
)
}
},
- modifier = modifier
+ modifier = modifier,
) { measurables, constraints ->
val navigationIconPlaceable =
measurables
@@ -552,7 +561,7 @@ private fun TopAppBarLayout(
// Navigation icon
navigationIconPlaceable.placeRelative(
x = 0,
- y = (layoutHeight - navigationIconPlaceable.height) / 2
+ y = (layoutHeight - navigationIconPlaceable.height) / 2,
)
// Title
@@ -570,17 +579,17 @@ private fun TopAppBarLayout(
titlePlaceable.height -
max(
0,
- titleBottomPadding - titlePlaceable.height + titleBaseline
+ titleBottomPadding - titlePlaceable.height + titleBaseline,
)
// Arrangement.Top
else -> 0
- }
+ },
)
// Action icons
actionIconsPlaceable.placeRelative(
x = constraints.maxWidth - actionIconsPlaceable.width,
- y = (layoutHeight - actionIconsPlaceable.height) / 2
+ y = (layoutHeight - actionIconsPlaceable.height) / 2,
)
}
}
@@ -595,7 +604,7 @@ private suspend fun settleAppBar(
state: TopAppBarState,
velocity: Float,
flingAnimationSpec: DecayAnimationSpec<Float>?,
- snapAnimationSpec: AnimationSpec<Float>?
+ snapAnimationSpec: AnimationSpec<Float>?,
): Velocity {
// Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar,
// and just return Zero Velocity.
@@ -609,20 +618,18 @@ private suspend fun settleAppBar(
// continue the motion to expand or collapse the app bar.
if (flingAnimationSpec != null && abs(velocity) > 1f) {
var lastValue = 0f
- AnimationState(
- initialValue = 0f,
- initialVelocity = velocity,
- )
- .animateDecay(flingAnimationSpec) {
- val delta = value - lastValue
- val initialHeightOffset = state.heightOffset
- state.heightOffset = initialHeightOffset + delta
- val consumed = abs(initialHeightOffset - state.heightOffset)
- lastValue = value
- remainingVelocity = this.velocity
- // avoid rounding errors and stop if anything is unconsumed
- if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
- }
+ AnimationState(initialValue = 0f, initialVelocity = velocity).animateDecay(
+ flingAnimationSpec
+ ) {
+ val delta = value - lastValue
+ val initialHeightOffset = state.heightOffset
+ state.heightOffset = initialHeightOffset + delta
+ val consumed = abs(initialHeightOffset - state.heightOffset)
+ lastValue = value
+ remainingVelocity = this.velocity
+ // avoid rounding errors and stop if anything is unconsumed
+ if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
+ }
}
// Snap if animation specs were provided.
if (snapAnimationSpec != null) {
@@ -633,7 +640,7 @@ private suspend fun settleAppBar(
} else {
state.heightOffsetLimit
},
- animationSpec = snapAnimationSpec
+ animationSpec = snapAnimationSpec,
) {
state.heightOffset = value
}
@@ -647,9 +654,10 @@ private suspend fun settleAppBar(
// Medium or Large app bar.
private val TopTitleAlphaEasing = CubicBezierEasing(.8f, 0f, .8f, .15f)
-internal val MaxHeightWithoutTitle = 124.dp
+internal val MaxHeightWithoutTitle = if (isSpaExpressiveEnabled) 84.dp else 124.dp
internal val DefaultTitleHeight = 52.dp
internal val ContainerHeight = 56.dp
+private val titleBaselineHeight = if (isSpaExpressiveEnabled) 8.dp else 0.dp
private val LargeTitleBottomPadding = 28.dp
private val TopAppBarHorizontalPadding = 4.dp
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java
index 58dc8c7aad6c..e7c7476d4797 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java
@@ -45,6 +45,7 @@ import java.lang.annotation.RetentionPolicy;
DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS,
DeviceSettingId.DEVICE_SETTING_ID_DEVICE_DETAILS_FOOTER,
DeviceSettingId.DEVICE_SETTING_ID_ANC,
+ DeviceSettingId.DEVICE_SETTING_ID_GENERAL_BLUETOOTH_DEVICE_HEADER,
},
open = true)
public @interface DeviceSettingId {
@@ -114,6 +115,9 @@ public @interface DeviceSettingId {
/** Device setting ID for "More Settings" page. */
int DEVICE_SETTING_ID_MORE_SETTINGS = 21;
+ /** Device setting ID for general bluetooth device header. */
+ int DEVICE_SETTING_ID_GENERAL_BLUETOOTH_DEVICE_HEADER = 22;
+
/** Device setting ID for ANC. */
int DEVICE_SETTING_ID_ANC = 1001;
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt
index 38183d5a01fd..da01b3bcaafb 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt
@@ -32,9 +32,9 @@ import android.os.Parcelable
*/
data class DeviceSettingItem(
@DeviceSettingId val settingId: Int,
- val packageName: String,
- val className: String,
- val intentAction: String,
+ val packageName: String? = null,
+ val className: String? = null,
+ val intentAction: String? = null,
val preferenceKey: String? = null,
val highlighted: Boolean = false,
val extras: Bundle = Bundle.EMPTY,
@@ -62,11 +62,11 @@ data class DeviceSettingItem(
parcel.run {
DeviceSettingItem(
settingId = readInt(),
- packageName = readString() ?: "",
- className = readString() ?: "",
- intentAction = readString() ?: "",
+ packageName = readString(),
+ className = readString(),
+ intentAction = readString(),
highlighted = readBoolean(),
- preferenceKey = readString() ?: "",
+ preferenceKey = readString(),
extras = readBundle((Bundle::class.java.classLoader)) ?: Bundle.EMPTY,
)
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.aidl
new file mode 100644
index 000000000000..d8378067b115
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.bluetooth.devicesettings;
+
+parcelable DeviceSettingsConfigServiceStatus; \ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.kt
new file mode 100644
index 000000000000..ae867713c831
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.bluetooth.devicesettings
+
+import android.os.Bundle
+import android.os.Parcel
+import android.os.Parcelable
+
+/**
+ * A data class representing a device settings config service status.
+ *
+ * @property success Whether the status is succeed.
+ * @property extras Extra bundle
+ */
+data class DeviceSettingsConfigServiceStatus(
+ val success: Boolean,
+ val extras: Bundle = Bundle.EMPTY,
+) : Parcelable {
+
+ override fun describeContents(): Int = 0
+
+ override fun writeToParcel(parcel: Parcel, flags: Int) {
+ parcel.run {
+ writeBoolean(success)
+ writeBundle(extras)
+ }
+ }
+
+ companion object {
+ @JvmField
+ val CREATOR: Parcelable.Creator<DeviceSettingsConfigServiceStatus> =
+ object : Parcelable.Creator<DeviceSettingsConfigServiceStatus> {
+ override fun createFromParcel(parcel: Parcel) =
+ parcel.run {
+ DeviceSettingsConfigServiceStatus(
+ success = readBoolean(),
+ extras = readBundle((Bundle::class.java.classLoader)) ?: Bundle.EMPTY,
+ )
+ }
+
+ override fun newArray(size: Int): Array<DeviceSettingsConfigServiceStatus?> {
+ return arrayOfNulls(size)
+ }
+ }
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt
index 977849e75556..77d790e7f773 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt
@@ -21,7 +21,7 @@ import android.os.Parcel
import android.os.Parcelable
/**
- * A data class representing a device settings item in bluetooth device details config.
+ * A data class representing a device settings provider service status.
*
* @property enabled Whether the service is enabled.
* @property extras Extra bundle
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl
index 647611ed8ef4..9cf49070a62c 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl
@@ -17,8 +17,8 @@
package com.android.settingslib.bluetooth.devicesettings;
import com.android.settingslib.bluetooth.devicesettings.DeviceInfo;
-import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig;
+import com.android.settingslib.bluetooth.devicesettings.IGetDeviceSettingsConfigCallback;
interface IDeviceSettingsConfigProviderService {
- DeviceSettingsConfig getDeviceSettingsConfig(in DeviceInfo device);
+ oneway void getDeviceSettingsConfig(in DeviceInfo device, in IGetDeviceSettingsConfigCallback callback);
} \ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IGetDeviceSettingsConfigCallback.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IGetDeviceSettingsConfigCallback.aidl
new file mode 100644
index 000000000000..403cdd9e4d70
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IGetDeviceSettingsConfigCallback.aidl
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.bluetooth.devicesettings;
+
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig;
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfigServiceStatus;
+
+interface IGetDeviceSettingsConfigCallback {
+ oneway void onResult(in DeviceSettingsConfigServiceStatus status, in DeviceSettingsConfig config) = 0;
+} \ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
index 3d8ff86c9377..4af0504bd73a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
@@ -33,12 +33,15 @@ import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingPreferenceState
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingState
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfigServiceStatus
import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsConfigProviderService
import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener
import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsProviderService
+import com.android.settingslib.bluetooth.devicesettings.IGetDeviceSettingsConfigCallback
import com.android.settingslib.bluetooth.devicesettings.data.model.ServiceConnectionStatus
import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.resume
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -63,6 +66,7 @@ import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
+import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
@OptIn(ExperimentalCoroutinesApi::class)
@@ -74,22 +78,22 @@ class DeviceSettingServiceConnection(
private val backgroundCoroutineContext: CoroutineContext,
) {
data class EndPoint(
- private val packageName: String,
+ private val packageName: String?,
private val className: String?,
- private val intentAction: String,
+ private val intentAction: String?,
) {
- fun toIntent(): Intent =
- Intent().apply {
+ fun toIntent(): Intent? {
+ if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(intentAction)) {
+ return null
+ }
+ return Intent().apply {
if (className.isNullOrBlank()) {
setPackage(packageName)
} else {
- setClassName(packageName, className)
+ setClassName(packageName!!, className)
}
setAction(intentAction)
}
-
- fun isValid(): Boolean {
- return !TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(intentAction)
}
}
@@ -126,8 +130,9 @@ class DeviceSettingServiceConnection(
when (it) {
is ServiceConnectionStatus.Connected ->
flowOf(
- it.service.getDeviceSettingsConfig(
- deviceInfo { setBluetoothAddress(cachedDevice.address) }
+ getDeviceSettingsConfigFromService(
+ deviceInfo { setBluetoothAddress(cachedDevice.address) },
+ it.service,
)
)
ServiceConnectionStatus.Connecting -> flowOf()
@@ -137,6 +142,27 @@ class DeviceSettingServiceConnection(
.first()
}
+ private suspend fun getDeviceSettingsConfigFromService(
+ deviceInfo: DeviceInfo,
+ service: IDeviceSettingsConfigProviderService,
+ ): DeviceSettingsConfig? = suspendCancellableCoroutine { continuation ->
+ service.getDeviceSettingsConfig(
+ deviceInfo,
+ object : IGetDeviceSettingsConfigCallback.Stub() {
+ override fun onResult(
+ status: DeviceSettingsConfigServiceStatus,
+ config: DeviceSettingsConfig?,
+ ) {
+ if (!status.success) {
+ continuation.resume(null)
+ } else {
+ continuation.resume(config)
+ }
+ }
+ },
+ )
+ }
+
private val settingIdToItemMapping =
flow {
if (!isServiceEnabled.await()) {
@@ -160,6 +186,12 @@ class DeviceSettingServiceConnection(
}
.shareIn(scope = coroutineScope, started = SharingStarted.WhileSubscribed(), replay = 1)
+ private val services =
+ ConcurrentHashMap<
+ EndPoint,
+ StateFlow<ServiceConnectionStatus<IDeviceSettingsProviderService>>,
+ >()
+
/** Gets [DeviceSettingsConfig] for the device, return null when failed. */
suspend fun getDeviceSettingsConfig(): DeviceSettingsConfig? {
if (!isServiceEnabled.await()) {
@@ -222,24 +254,23 @@ class DeviceSettingServiceConnection(
)
}
}
- ?.filter { it.isValid() }
?.distinct()
- ?.associateBy(
- { it },
- { endpoint ->
- services.computeIfAbsent(endpoint) {
- getService(
- endpoint.toIntent(),
- IDeviceSettingsProviderService.Stub::asInterface,
- )
- .stateIn(
- coroutineScope.plus(backgroundCoroutineContext),
- SharingStarted.WhileSubscribed(),
- ServiceConnectionStatus.Connecting,
- )
- }
- },
- )
+ ?.mapNotNull { endpoint ->
+ endpoint.toIntent()?.let { intent ->
+ Pair(
+ endpoint,
+ services.computeIfAbsent(endpoint) {
+ getService(intent, IDeviceSettingsProviderService.Stub::asInterface)
+ .stateIn(
+ coroutineScope.plus(backgroundCoroutineContext),
+ SharingStarted.WhileSubscribed(),
+ ServiceConnectionStatus.Connecting,
+ )
+ },
+ )
+ }
+ }
+ ?.toMap()
private fun getDeviceSettingsFromService(
cachedDevice: CachedBluetoothDevice,
@@ -320,11 +351,5 @@ class DeviceSettingServiceConnection(
const val CONFIG_SERVICE_PACKAGE_NAME = "DEVICE_SETTINGS_CONFIG_PACKAGE_NAME"
const val CONFIG_SERVICE_CLASS_NAME = "DEVICE_SETTINGS_CONFIG_CLASS"
const val CONFIG_SERVICE_INTENT_ACTION = "DEVICE_SETTINGS_CONFIG_ACTION"
-
- val services =
- ConcurrentHashMap<
- EndPoint,
- StateFlow<ServiceConnectionStatus<IDeviceSettingsProviderService>>,
- >()
}
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatusTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatusTest.kt
new file mode 100644
index 000000000000..3149acf6fc3c
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatusTest.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.bluetooth.devicesettings
+
+import android.os.Bundle
+import android.os.Parcel
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class DeviceSettingsConfigServiceStatusTest {
+
+ @Test
+ fun parcelOperation() {
+ val item =
+ DeviceSettingsConfigServiceStatus(
+ success = true,
+ extras = Bundle().apply { putString("key1", "value1") },
+ )
+
+ val fromParcel = writeAndRead(item)
+
+ assertThat(fromParcel.success).isEqualTo(item.success)
+ assertThat(fromParcel.extras.getString("key1")).isEqualTo(item.extras.getString("key1"))
+ }
+
+ private fun writeAndRead(
+ item: DeviceSettingsConfigServiceStatus
+ ): DeviceSettingsConfigServiceStatus {
+ val parcel = Parcel.obtain()
+ item.writeToParcel(parcel, 0)
+ parcel.setDataPosition(0)
+ return DeviceSettingsConfigServiceStatus.CREATOR.createFromParcel(parcel)
+ }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
index 0cb6bc1b1261..4e62fd3b27c5 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
@@ -33,10 +33,12 @@ import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingItem
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingState
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfigServiceStatus
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsProviderServiceStatus
import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsConfigProviderService
import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener
import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsProviderService
+import com.android.settingslib.bluetooth.devicesettings.IGetDeviceSettingsConfigCallback
import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreference
import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreferenceState
import com.android.settingslib.bluetooth.devicesettings.ToggleInfo
@@ -53,7 +55,6 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
-import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -140,15 +141,10 @@ class DeviceSettingRepositoryTest {
)
}
- @After
- fun clean() {
- DeviceSettingServiceConnection.services.clear()
- }
-
@Test
fun getDeviceSettingsConfig_withMetadata_success() {
testScope.runTest {
- `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+ setUpConfigService(true, DEVICE_SETTING_CONFIG)
`when`(settingProviderService1.serviceStatus)
.thenReturn(DeviceSettingsProviderServiceStatus(true))
`when`(settingProviderService2.serviceStatus)
@@ -179,7 +175,7 @@ class DeviceSettingRepositoryTest {
)
)
.thenReturn("".toByteArray())
- `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+ setUpConfigService(true, DEVICE_SETTING_CONFIG)
`when`(settingProviderService1.serviceStatus)
.thenReturn(DeviceSettingsProviderServiceStatus(true))
`when`(settingProviderService2.serviceStatus)
@@ -194,7 +190,7 @@ class DeviceSettingRepositoryTest {
@Test
fun getDeviceSettingsConfig_providerServiceNotEnabled_returnNull() {
testScope.runTest {
- `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+ setUpConfigService(true, DEVICE_SETTING_CONFIG)
`when`(settingProviderService1.serviceStatus)
.thenReturn(DeviceSettingsProviderServiceStatus(false))
`when`(settingProviderService2.serviceStatus)
@@ -209,7 +205,7 @@ class DeviceSettingRepositoryTest {
@Test
fun getDeviceSettingsConfig_bindingServiceFail_returnNull() {
testScope.runTest {
- `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+ setUpConfigService(true, DEVICE_SETTING_CONFIG)
doReturn(false).`when`(context).bindService(any(), anyInt(), any(), any())
val config = underTest.getDeviceSettingsConfig(cachedDevice)
@@ -221,7 +217,7 @@ class DeviceSettingRepositoryTest {
@Test
fun getDeviceSetting_actionSwitchPreference_success() {
testScope.runTest {
- `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+ setUpConfigService(true, DEVICE_SETTING_CONFIG)
`when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then {
input ->
input
@@ -247,7 +243,7 @@ class DeviceSettingRepositoryTest {
@Test
fun getDeviceSetting_multiTogglePreference_success() {
testScope.runTest {
- `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+ setUpConfigService(true, DEVICE_SETTING_CONFIG)
`when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then {
input ->
input
@@ -273,7 +269,7 @@ class DeviceSettingRepositoryTest {
@Test
fun getDeviceSetting_helpPreference_success() {
testScope.runTest {
- `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+ setUpConfigService(true, DEVICE_SETTING_CONFIG)
`when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then {
input ->
input
@@ -299,6 +295,7 @@ class DeviceSettingRepositoryTest {
@Test
fun getDeviceSetting_noConfig_returnNull() {
testScope.runTest {
+ setUpConfigService(false, null)
`when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then {
input ->
input
@@ -320,7 +317,7 @@ class DeviceSettingRepositoryTest {
@Test
fun updateDeviceSettingState_switchState_success() {
testScope.runTest {
- `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+ setUpConfigService(true, DEVICE_SETTING_CONFIG)
`when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then {
input ->
input
@@ -358,7 +355,7 @@ class DeviceSettingRepositoryTest {
@Test
fun updateDeviceSettingState_multiToggleState_success() {
testScope.runTest {
- `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
+ setUpConfigService(true, DEVICE_SETTING_CONFIG)
`when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then {
input ->
input
@@ -459,6 +456,17 @@ class DeviceSettingRepositoryTest {
assertThat(actual.settingId).isEqualTo(serviceResponse.settingId)
}
+ private fun setUpConfigService(success: Boolean, config: DeviceSettingsConfig?) {
+ `when`(configService.getDeviceSettingsConfig(any(), any())).then { input ->
+ input
+ .getArgument<IGetDeviceSettingsConfigCallback>(1)
+ .onResult(
+ DeviceSettingsConfigServiceStatus(success = success),
+ config
+ )
+ }
+ }
+
private companion object {
const val BLUETOOTH_ADDRESS = "12:34:56:78"
const val CONFIG_SERVICE_PACKAGE_NAME = "com.android.fake.configservice"
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index d7109398b956..5e31da411e49 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -164,6 +164,7 @@ public class SecureSettings {
Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS,
Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+ Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM,
Settings.Secure.SHOW_NOTIFICATION_SNOOZE,
Settings.Secure.NOTIFICATION_HISTORY_ENABLED,
Settings.Secure.ZEN_DURATION,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index fa16a44f4592..b3f73749f393 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -243,6 +243,7 @@ public class SecureSettingsValidators {
VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.SHOW_NOTIFICATION_SNOOZE, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.NOTIFICATION_HISTORY_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.ZEN_DURATION, ANY_INTEGER_VALIDATOR);
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index f8383d94b1ab..88cc152b3819 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -40,16 +40,6 @@ flag {
}
flag {
- name: "notification_minimalism_prototype"
- namespace: "systemui"
- description: "Prototype of notification minimalism; the new 'Intermediate' lockscreen customization proposal."
- bug: "330387368"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
name: "notification_view_flipper_pausing_v2"
namespace: "systemui"
description: "Pause ViewFlippers inside Notification custom layouts when the shade is closed."
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt b/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
index a5f8057b524f..20efea513b3a 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
@@ -28,11 +28,11 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonColors
import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
-import com.android.compose.theme.LocalAndroidColorScheme
@Composable
fun PlatformButton(
@@ -100,12 +100,7 @@ fun PlatformIconButton(
@DrawableRes iconResource: Int,
contentDescription: String?,
) {
- IconButton(
- modifier = modifier,
- onClick = onClick,
- enabled = enabled,
- colors = colors,
- ) {
+ IconButton(modifier = modifier, onClick = onClick, enabled = enabled, colors = colors) {
Icon(
painter = painterResource(id = iconResource),
contentDescription = contentDescription,
@@ -118,7 +113,7 @@ private val ButtonPaddings = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
@Composable
private fun filledButtonColors(): ButtonColors {
- val colors = LocalAndroidColorScheme.current
+ val colors = MaterialTheme.colorScheme
return ButtonDefaults.buttonColors(
containerColor = colors.primary,
contentColor = colors.onPrimary,
@@ -127,27 +122,22 @@ private fun filledButtonColors(): ButtonColors {
@Composable
private fun outlineButtonColors(): ButtonColors {
- return ButtonDefaults.outlinedButtonColors(
- contentColor = LocalAndroidColorScheme.current.onSurface,
- )
+ return ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.onSurface)
}
@Composable
private fun iconButtonColors(): IconButtonColors {
return IconButtonDefaults.filledIconButtonColors(
- contentColor = LocalAndroidColorScheme.current.onSurface,
+ contentColor = MaterialTheme.colorScheme.onSurface
)
}
@Composable
private fun outlineButtonBorder(): BorderStroke {
- return BorderStroke(
- width = 1.dp,
- color = LocalAndroidColorScheme.current.primary,
- )
+ return BorderStroke(width = 1.dp, color = MaterialTheme.colorScheme.primary)
}
@Composable
private fun textButtonColors(): ButtonColors {
- return ButtonDefaults.textButtonColors(contentColor = LocalAndroidColorScheme.current.primary)
+ return ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.primary)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt
index 0b9669410b8e..69ca0a5f476c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt
@@ -38,7 +38,6 @@ import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
-import com.android.compose.theme.LocalAndroidColorScheme
import kotlin.math.roundToInt
/**
@@ -69,7 +68,7 @@ fun AlertDialogContent(
Modifier.defaultMinSize(minWidth = defaultSize, minHeight = defaultSize),
propagateMinConstraints = true,
) {
- val iconColor = LocalAndroidColorScheme.current.primary
+ val iconColor = MaterialTheme.colorScheme.primary
CompositionLocalProvider(LocalContentColor provides iconColor) { icon() }
}
@@ -77,7 +76,7 @@ fun AlertDialogContent(
}
// Title.
- val titleColor = LocalAndroidColorScheme.current.onSurface
+ val titleColor = MaterialTheme.colorScheme.onSurface
CompositionLocalProvider(LocalContentColor provides titleColor) {
ProvideTextStyle(
MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center)
@@ -88,7 +87,7 @@ fun AlertDialogContent(
Spacer(Modifier.height(16.dp))
// Content.
- val contentColor = LocalAndroidColorScheme.current.onSurfaceVariant
+ val contentColor = MaterialTheme.colorScheme.onSurfaceVariant
Box {
CompositionLocalProvider(LocalContentColor provides contentColor) {
ProvideTextStyle(
@@ -169,7 +168,7 @@ private fun AlertDialogButtons(
negative.width -
positive.width -
horizontalSpacing.roundToInt(),
- 0
+ 0,
)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
index ba689179c33d..29035ce2aa0a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
@@ -38,7 +38,6 @@ 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
-import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.testKosmos
@@ -76,7 +75,6 @@ class KeyguardDismissActionInteractorTest : SysuiTestCase() {
transitionInteractor = kosmos.keyguardTransitionInteractor,
dismissInteractor = dismissInteractor,
applicationScope = testScope.backgroundScope,
- sceneInteractor = { kosmos.sceneInteractor },
deviceUnlockedInteractor = { kosmos.deviceUnlockedInteractor },
powerInteractor = kosmos.powerInteractor,
alternateBouncerInteractor = kosmos.alternateBouncerInteractor,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
index 7b87aeb60c13..d772e3effbeb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
@@ -42,7 +42,8 @@ import com.android.systemui.statusbar.notification.collection.modifyEntry
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.domain.interactor.lockScreenNotificationMinimalismSetting
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
import com.android.systemui.testKosmos
import com.android.systemui.util.settings.FakeSettings
@@ -66,7 +67,7 @@ import org.mockito.kotlin.whenever
@SmallTest
@RunWith(AndroidJUnit4::class)
-@EnableFlags(NotificationMinimalismPrototype.FLAG_NAME)
+@EnableFlags(NotificationMinimalism.FLAG_NAME)
class LockScreenMinimalismCoordinatorTest : SysuiTestCase() {
private val kosmos =
@@ -76,7 +77,7 @@ class LockScreenMinimalismCoordinatorTest : SysuiTestCase() {
mock<SysuiStatusBarStateController>().also { mock ->
doAnswer { statusBarState.ordinal }.whenever(mock).state
}
- fakeSettings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1)
+ lockScreenNotificationMinimalismSetting = true
}
private val notifPipeline: NotifPipeline = mock()
private var statusBarState: StatusBarState = StatusBarState.KEYGUARD
@@ -193,7 +194,7 @@ class LockScreenMinimalismCoordinatorTest : SysuiTestCase() {
kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key
assertThat(promoter.shouldPromoteToTopLevel(child1)).isFalse()
assertThat(promoter.shouldPromoteToTopLevel(child2))
- .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen)
+ .isEqualTo(NotificationMinimalism.ungroupTopUnseen)
assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
@@ -201,7 +202,7 @@ class LockScreenMinimalismCoordinatorTest : SysuiTestCase() {
kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key
assertThat(promoter.shouldPromoteToTopLevel(child1)).isTrue()
assertThat(promoter.shouldPromoteToTopLevel(child2))
- .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen)
+ .isEqualTo(NotificationMinimalism.ungroupTopUnseen)
assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt
index 2159b864d2a2..ea2e25e8eb1c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt
@@ -21,7 +21,7 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
@@ -57,7 +57,7 @@ class SeenNotificationsInteractorTest : SysuiTestCase() {
}
@Test
- @EnableFlags(NotificationMinimalismPrototype.FLAG_NAME)
+ @EnableFlags(NotificationMinimalism.FLAG_NAME)
fun topOngoingAndUnseenNotification() = runTest {
val entry1 = NotificationEntryBuilder().setTag("entry1").build()
val entry2 = NotificationEntryBuilder().setTag("entry2").build()
@@ -91,4 +91,17 @@ class SeenNotificationsInteractorTest : SysuiTestCase() {
testScheduler.runCurrent()
assertThat(settingEnabled).isTrue()
}
+
+ fun testLockScreenNotificationMinimalismSetting() = runTest {
+ val settingEnabled by
+ collectLastValue(underTest.isLockScreenNotificationMinimalismEnabled())
+
+ kosmos.lockScreenNotificationMinimalismSetting = false
+ testScheduler.runCurrent()
+ assertThat(settingEnabled).isFalse()
+
+ kosmos.lockScreenNotificationMinimalismSetting = true
+ testScheduler.runCurrent()
+ assertThat(settingEnabled).isTrue()
+ }
}
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index 8f55961af4e9..0f1da509468a 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -70,6 +70,7 @@ android_library {
"jsr330",
"//frameworks/libs/systemui:com_android_systemui_shared_flags_lib",
"//frameworks/libs/systemui:msdl",
+ "//frameworks/libs/systemui:view_capture",
],
resource_dirs: [
"res",
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
index f358ba2d3ccd..4db6ab6ea579 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
@@ -16,6 +16,9 @@
package com.android.systemui.shared.rotation;
+import static com.android.app.viewcapture.ViewCaptureFactory.getViewCaptureAwareWindowManagerInstance;
+import static com.android.systemui.Flags.enableViewCaptureTracing;
+
import android.annotation.DimenRes;
import android.annotation.IdRes;
import android.annotation.LayoutRes;
@@ -30,7 +33,6 @@ import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.FrameLayout;
@@ -38,6 +40,7 @@ import android.widget.FrameLayout;
import androidx.annotation.BoolRes;
import androidx.core.view.OneShotPreDrawListener;
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
import com.android.systemui.shared.rotation.FloatingRotationButtonPositionCalculator.Position;
/**
@@ -47,7 +50,7 @@ public class FloatingRotationButton implements RotationButton {
private static final int MARGIN_ANIMATION_DURATION_MILLIS = 300;
- private final WindowManager mWindowManager;
+ private final ViewCaptureAwareWindowManager mWindowManager;
private final ViewGroup mKeyButtonContainer;
private final FloatingRotationButtonView mKeyButtonView;
@@ -88,7 +91,8 @@ public class FloatingRotationButton implements RotationButton {
@DimenRes int taskbarBottomMargin, @DimenRes int buttonDiameter,
@DimenRes int rippleMaxWidth, @BoolRes int floatingRotationBtnPositionLeftResource) {
mContext = context;
- mWindowManager = mContext.getSystemService(WindowManager.class);
+ mWindowManager = getViewCaptureAwareWindowManagerInstance(mContext,
+ enableViewCaptureTracing());
mKeyButtonContainer = (ViewGroup) LayoutInflater.from(mContext).inflate(layout, null);
mKeyButtonView = mKeyButtonContainer.findViewById(keyButtonId);
mKeyButtonView.setVisibility(View.VISIBLE);
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 820c102e81d8..47f0ecfb237a 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -34,12 +34,14 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.shared.flag.DualShade
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.android.systemui.statusbar.core.StatusBarSimpleFragment
import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag
import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor
import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
import javax.inject.Inject
@@ -57,7 +59,7 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha
// Internal notification frontend dependencies
NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token
PriorityPeopleSection.token dependsOn SortBySectionTimeFlag.token
- NotificationMinimalismPrototype.token dependsOn NotificationThrottleHun.token
+ NotificationMinimalism.token dependsOn NotificationThrottleHun.token
ModesEmptyShadeFix.token dependsOn FooterViewRefactor.token
ModesEmptyShadeFix.token dependsOn modesUi
@@ -73,6 +75,8 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha
// Status bar chip dependencies
statusBarCallChipNotificationIconToken dependsOn statusBarUseReposForCallChipToken
statusBarCallChipNotificationIconToken dependsOn statusBarScreenSharingChipsToken
+
+ StatusBarConnectedDisplays.token dependsOn StatusBarSimpleFragment.token
}
private inline val politeNotifications
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 ea80911335fa..18b14951ee70 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
@@ -28,7 +28,6 @@ import com.android.systemui.keyguard.shared.model.KeyguardDone
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.shared.flag.SceneContainerFlag
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.domain.interactor.ShadeInteractor
@@ -60,7 +59,6 @@ constructor(
transitionInteractor: KeyguardTransitionInteractor,
val dismissInteractor: KeyguardDismissInteractor,
@Application private val applicationScope: CoroutineScope,
- sceneInteractor: Lazy<SceneInteractor>,
deviceUnlockedInteractor: Lazy<DeviceUnlockedInteractor>,
powerInteractor: PowerInteractor,
alternateBouncerInteractor: AlternateBouncerInteractor,
@@ -102,20 +100,20 @@ constructor(
private val isOnShadeWhileUnlocked: Flow<Boolean> =
if (SceneContainerFlag.isEnabled) {
combine(
- sceneInteractor.get().currentScene,
+ shadeInteractor.get().isAnyExpanded,
deviceUnlockedInteractor.get().deviceUnlockStatus,
- ) { scene, unlockStatus ->
- unlockStatus.isUnlocked &&
- (scene == Scenes.QuickSettings || scene == Scenes.Shade)
+ ) { isAnyExpanded, unlockStatus ->
+ isAnyExpanded && unlockStatus.isUnlocked
}
.distinctUntilChanged()
} else if (ComposeBouncerFlags.isOnlyComposeBouncerEnabled()) {
combine(
- shadeInteractor.get().isAnyExpanded,
- keyguardInteractor.get().isKeyguardDismissible,
- ) { isAnyExpanded, keyguardDismissible ->
- isAnyExpanded && keyguardDismissible
- }
+ shadeInteractor.get().isAnyExpanded,
+ keyguardInteractor.get().isKeyguardDismissible,
+ ) { isAnyExpanded, keyguardDismissible ->
+ isAnyExpanded && keyguardDismissible
+ }
+ .distinctUntilChanged()
} else {
flow {
error(
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
index 275f1eecd4db..39cedc36dbec 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
@@ -128,7 +128,7 @@ constructor(
data: MediaData,
immediately: Boolean,
receivedSmartspaceCardLatency: Int,
- isSsReactivated: Boolean
+ isSsReactivated: Boolean,
) {
var reusedListener: PlaybackStateListener? = null
@@ -183,7 +183,7 @@ constructor(
override fun onSmartspaceMediaDataLoaded(
key: String,
data: SmartspaceMediaData,
- shouldPrioritize: Boolean
+ shouldPrioritize: Boolean,
) {
if (!mediaFlags.isPersistentSsCardEnabled()) return
@@ -259,7 +259,9 @@ constructor(
}
override fun onPlaybackStateChanged(state: PlaybackState?) {
- processState(state, dispatchEvents = true, currentResumption = resumption)
+ bgExecutor.execute {
+ processState(state, dispatchEvents = true, currentResumption = resumption)
+ }
}
override fun onSessionDestroyed() {
@@ -276,6 +278,7 @@ constructor(
}
}
+ @WorkerThread
private fun processState(
state: PlaybackState?,
dispatchEvents: Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt
index 228b57603bed..d413474fde90 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt
@@ -54,6 +54,7 @@ import com.android.systemui.mediaprojection.MediaProjectionServiceHelper
import com.android.systemui.mediaprojection.appselector.data.RecentTask
import com.android.systemui.mediaprojection.appselector.view.MediaProjectionRecentsViewController
import com.android.systemui.res.R
+import com.android.systemui.shared.system.ActivityManagerWrapper
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.AsyncActivityLauncher
import java.lang.IllegalArgumentException
@@ -62,9 +63,10 @@ import javax.inject.Inject
class MediaProjectionAppSelectorActivity(
private val componentFactory: MediaProjectionAppSelectorComponent.Factory,
private val activityLauncher: AsyncActivityLauncher,
+ private val activityManager: ActivityManagerWrapper,
/** This is used to override the dependency in a screenshot test */
@VisibleForTesting
- private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)?
+ private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)?,
) :
ChooserActivity(),
MediaProjectionAppSelectorView,
@@ -74,8 +76,9 @@ class MediaProjectionAppSelectorActivity(
@Inject
constructor(
componentFactory: MediaProjectionAppSelectorComponent.Factory,
- activityLauncher: AsyncActivityLauncher
- ) : this(componentFactory, activityLauncher, listControllerFactory = null)
+ activityLauncher: AsyncActivityLauncher,
+ activityManager: ActivityManagerWrapper,
+ ) : this(componentFactory, activityLauncher, activityManager, listControllerFactory = null)
private val lifecycleRegistry = LifecycleRegistry(this)
override val lifecycle = lifecycleRegistry
@@ -100,7 +103,7 @@ class MediaProjectionAppSelectorActivity(
callingPackage = callingPackage,
view = this,
resultHandler = this,
- isFirstStart = savedInstanceState == null
+ isFirstStart = savedInstanceState == null,
)
component.lifecycleObservers.forEach { lifecycle.addObserver(it) }
@@ -113,7 +116,7 @@ class MediaProjectionAppSelectorActivity(
intent.configureChooserIntent(
resources,
component.hostUserHandle,
- component.personalProfileUserHandle
+ component.personalProfileUserHandle,
)
reviewGrantedConsentRequired =
@@ -180,7 +183,13 @@ class MediaProjectionAppSelectorActivity(
// is created and ready to be captured.
val activityStarted =
activityLauncher.startActivityAsUser(intent, userHandle, activityOptions.toBundle()) {
- returnSelectedApp(launchCookie, taskId = -1)
+ if (targetInfo.resolvedComponentName == callingActivity) {
+ // If attempting to launch the app used to launch the MediaProjection, then
+ // provide the task id since the launch cookie won't match the existing task
+ returnSelectedApp(launchCookie, taskId = activityManager.runningTask.taskId)
+ } else {
+ returnSelectedApp(launchCookie, taskId = -1)
+ }
}
// Rely on the ActivityManager to pop up a dialog regarding app suspension
@@ -213,7 +222,7 @@ class MediaProjectionAppSelectorActivity(
MediaProjectionServiceHelper.setReviewedConsentIfNeeded(
RECORD_CANCEL,
reviewGrantedConsentRequired,
- /* projection= */ null
+ /* projection= */ null,
)
if (isFinishing) {
// Only log dismissed when actually finishing, and not when changing configuration.
@@ -246,7 +255,7 @@ class MediaProjectionAppSelectorActivity(
val resultReceiver =
intent.getParcelableExtra(
EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
- ResultReceiver::class.java
+ ResultReceiver::class.java,
) as ResultReceiver
val captureRegion = MediaProjectionCaptureTarget(launchCookie, taskId)
val data = Bundle().apply { putParcelable(KEY_CAPTURE_TARGET, captureRegion) }
@@ -260,8 +269,8 @@ class MediaProjectionAppSelectorActivity(
val mediaProjectionBinder = intent.getIBinderExtra(EXTRA_MEDIA_PROJECTION)
val projection = IMediaProjection.Stub.asInterface(mediaProjectionBinder)
- projection.setLaunchCookie(launchCookie)
- projection.setTaskId(taskId)
+ projection.launchCookie = launchCookie
+ projection.taskId = taskId
val intent = Intent()
intent.putExtra(EXTRA_MEDIA_PROJECTION, projection.asBinder())
@@ -270,7 +279,7 @@ class MediaProjectionAppSelectorActivity(
MediaProjectionServiceHelper.setReviewedConsentIfNeeded(
RECORD_CONTENT_TASK,
reviewGrantedConsentRequired,
- projection
+ projection,
)
}
@@ -457,7 +466,7 @@ class MediaProjectionAppSelectorActivity(
*/
private class RecyclerViewExpandingAccessibilityDelegate(
rdl: ResolverDrawerLayout,
- view: RecyclerView
+ view: RecyclerView,
) : RecyclerViewAccessibilityDelegate(view) {
private val delegate = AppListAccessibilityDelegate(rdl)
@@ -465,7 +474,7 @@ class MediaProjectionAppSelectorActivity(
override fun onRequestSendAccessibilityEvent(
host: ViewGroup,
child: View,
- event: AccessibilityEvent
+ event: AccessibilityEvent,
): Boolean {
super.onRequestSendAccessibilityEvent(host, child, event)
return delegate.onRequestSendAccessibilityEvent(host, child, event)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
index 5ea9e6ae0a70..301ab2bcdd65 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
@@ -311,10 +311,7 @@ public class InternetDialogController implements AccessPointController.AccessPoi
mConfig = MobileMappings.Config.readConfig(mContext);
mTelephonyManager = mTelephonyManager.createForSubscriptionId(mDefaultDataSubId);
mSubIdTelephonyManagerMap.put(mDefaultDataSubId, mTelephonyManager);
- InternetTelephonyCallback telephonyCallback =
- new InternetTelephonyCallback(mDefaultDataSubId);
- mSubIdTelephonyCallbackMap.put(mDefaultDataSubId, telephonyCallback);
- mTelephonyManager.registerTelephonyCallback(mExecutor, telephonyCallback);
+ registerInternetTelephonyCallback(mTelephonyManager, mDefaultDataSubId);
// Listen the connectivity changes
mConnectivityManager.registerDefaultNetworkCallback(mConnectivityManagerNetworkCallback);
mCanConfigWifi = canConfigWifi;
@@ -346,6 +343,23 @@ public class InternetDialogController implements AccessPointController.AccessPoi
mCallback = null;
}
+ /**
+ * This is to generate and register the new callback to Telephony for uncached subscription id,
+ * then cache it. Telephony also cached this callback into
+ * {@link com.android.server.TelephonyRegistry}, so if subscription id and callback were cached
+ * already, it shall do nothing to avoid registering redundant callback to Telephony.
+ */
+ private void registerInternetTelephonyCallback(
+ TelephonyManager telephonyManager, int subId) {
+ if (mSubIdTelephonyCallbackMap.containsKey(subId)) {
+ // Avoid to generate and register unnecessary callback to Telephony.
+ return;
+ }
+ InternetTelephonyCallback telephonyCallback = new InternetTelephonyCallback(subId);
+ mSubIdTelephonyCallbackMap.put(subId, telephonyCallback);
+ telephonyManager.registerTelephonyCallback(mExecutor, telephonyCallback);
+ }
+
boolean isAirplaneModeEnabled() {
return mGlobalSettings.getInt(Settings.Global.AIRPLANE_MODE_ON, 0) != 0;
}
@@ -673,9 +687,7 @@ public class InternetDialogController implements AccessPointController.AccessPoi
int subId = subInfo.getSubscriptionId();
if (mSubIdTelephonyManagerMap.get(subId) == null) {
TelephonyManager secondaryTm = mTelephonyManager.createForSubscriptionId(subId);
- InternetTelephonyCallback telephonyCallback = new InternetTelephonyCallback(subId);
- secondaryTm.registerTelephonyCallback(mExecutor, telephonyCallback);
- mSubIdTelephonyCallbackMap.put(subId, telephonyCallback);
+ registerInternetTelephonyCallback(secondaryTm, subId);
mSubIdTelephonyManagerMap.put(subId, secondaryTm);
}
return subId;
@@ -1351,6 +1363,7 @@ public class InternetDialogController implements AccessPointController.AccessPoi
if (DEBUG) {
Log.d(TAG, "DDS: defaultDataSubId:" + defaultDataSubId);
}
+
if (SubscriptionManager.isUsableSubscriptionId(defaultDataSubId)) {
// clean up old defaultDataSubId
TelephonyCallback oldCallback = mSubIdTelephonyCallbackMap.get(mDefaultDataSubId);
@@ -1366,9 +1379,7 @@ public class InternetDialogController implements AccessPointController.AccessPoi
// create for new defaultDataSubId
mTelephonyManager = mTelephonyManager.createForSubscriptionId(defaultDataSubId);
mSubIdTelephonyManagerMap.put(defaultDataSubId, mTelephonyManager);
- InternetTelephonyCallback newCallback = new InternetTelephonyCallback(defaultDataSubId);
- mSubIdTelephonyCallbackMap.put(defaultDataSubId, newCallback);
- mTelephonyManager.registerTelephonyCallback(mHandler::post, newCallback);
+ registerInternetTelephonyCallback(mTelephonyManager, defaultDataSubId);
mCallback.onSubscriptionsChanged(defaultDataSubId);
}
mDefaultDataSubId = defaultDataSubId;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarConntectedDisplays.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarConntectedDisplays.kt
new file mode 100644
index 000000000000..54a18f764406
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarConntectedDisplays.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.statusbar.core
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the status bar connected displays flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object StatusBarConnectedDisplays {
+ /** The aconfig flag name */
+ const val FLAG_NAME = Flags.FLAG_STATUS_BAR_CONNECTED_DISPLAYS
+
+ /** A token used for dependency declaration */
+ val token: FlagToken
+ get() = FlagToken(FLAG_NAME, isEnabled)
+
+ /** Is the refactor enabled */
+ @JvmStatic
+ inline val isEnabled
+ get() = Flags.statusBarConnectedDisplays()
+
+ /**
+ * Called to ensure code is only run when the flag is enabled. This protects users from the
+ * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+ * build to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun isUnexpectedlyInLegacyMode() =
+ RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+ /**
+ * Called to ensure code is only run when the flag is enabled. This will throw an exception if
+ * the flag is not enabled to ensure that the refactor author catches issues in testing.
+ * Caution!! Using this check incorrectly will cause crashes in nextfood builds!
+ */
+ @JvmStatic
+ inline fun assertInNewMode() = RefactorFlagUtils.assertInNewMode(isEnabled, FLAG_NAME)
+
+ /**
+ * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+ * the flag is enabled to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
index b342722ebb09..b67092ca9348 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
@@ -22,7 +22,7 @@ import com.android.internal.annotations.VisibleForTesting
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NOTIFICATIONS_USE_PEOPLE_FILTERING
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.notification.collection.NotificationClassificationFlag
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING
import com.android.systemui.statusbar.notification.stack.BUCKET_FOREGROUND_SERVICE
@@ -54,7 +54,7 @@ constructor(val proxy: DeviceConfigProxy, val context: Context) {
fun getNotificationBuckets(): IntArray {
if (
PriorityPeopleSection.isEnabled ||
- NotificationMinimalismPrototype.isEnabled ||
+ NotificationMinimalism.isEnabled ||
NotificationClassificationFlag.isEnabled
) {
// We don't need this list to be adaptive, it can be the superset of all features.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
index a621b2a02c5d..4e63b920a73d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
@@ -35,7 +35,7 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_ONGOING
import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_UNSEEN
import com.android.systemui.util.asIndenting
@@ -77,7 +77,7 @@ constructor(
private var unseenFilterEnabled = false
override fun attach(pipeline: NotifPipeline) {
- if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) {
+ if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) {
return
}
pipeline.addPromoter(unseenNotifPromoter)
@@ -129,26 +129,25 @@ constructor(
}
}
- private fun unseenFeatureEnabled(): Flow<Boolean> {
- // TODO(b/330387368): create LOCK_SCREEN_NOTIFICATION_MINIMALISM setting to use here?
- // Or should we actually just repurpose using the existing setting?
- if (NotificationMinimalismPrototype.isEnabled) {
- return flowOf(true)
+ private fun minimalismFeatureSettingEnabled(): Flow<Boolean> {
+ if (!NotificationMinimalism.isEnabled) {
+ return flowOf(false)
}
- return seenNotificationsInteractor.isLockScreenShowOnlyUnseenNotificationsEnabled()
+ return seenNotificationsInteractor.isLockScreenNotificationMinimalismEnabled()
}
private suspend fun trackUnseenFilterSettingChanges() {
- unseenFeatureEnabled().collectLatest { isSettingEnabled ->
+ // Only filter the seen notifs when the lock screen minimalism feature settings is on.
+ minimalismFeatureSettingEnabled().collectLatest { isMinimalismSettingEnabled ->
// update local field and invalidate if necessary
- if (isSettingEnabled != unseenFilterEnabled) {
- unseenFilterEnabled = isSettingEnabled
+ if (isMinimalismSettingEnabled != unseenFilterEnabled) {
+ unseenFilterEnabled = isMinimalismSettingEnabled
unseenNotifications.clear()
unseenNotifPromoter.invalidateList("unseen setting changed")
}
// if the setting is enabled, then start tracking and filtering unseen notifications
- logger.logTrackingUnseen(isSettingEnabled)
- if (isSettingEnabled) {
+ logger.logTrackingUnseen(isMinimalismSettingEnabled)
+ if (isMinimalismSettingEnabled) {
trackSeenNotifications()
}
}
@@ -178,7 +177,7 @@ constructor(
}
private fun pickOutTopUnseenNotifs(list: List<ListEntry>) {
- if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
+ if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return
if (!unseenFilterEnabled) return
// Only ever elevate a top unseen notification on keyguard, not even locked shade
if (statusBarStateController.state != StatusBarState.KEYGUARD) {
@@ -215,9 +214,9 @@ constructor(
object : NotifPromoter(TAG) {
override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean =
when {
- NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode() -> false
+ NotificationMinimalism.isUnexpectedlyInLegacyMode() -> false
seenNotificationsInteractor.isTopOngoingNotification(child) -> true
- !NotificationMinimalismPrototype.ungroupTopUnseen -> false
+ !NotificationMinimalism.ungroupTopUnseen -> false
else -> seenNotificationsInteractor.isTopUnseenNotification(child)
}
}
@@ -225,7 +224,7 @@ constructor(
val topOngoingSectioner =
object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) {
override fun isInSection(entry: ListEntry): Boolean {
- if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false
+ if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return false
return entry.anyEntry { notificationEntry ->
seenNotificationsInteractor.isTopOngoingNotification(notificationEntry)
}
@@ -235,7 +234,7 @@ constructor(
val topUnseenSectioner =
object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) {
override fun isInSection(entry: ListEntry): Boolean {
- if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false
+ if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return false
return entry.anyEntry { notificationEntry ->
seenNotificationsInteractor.isTopUnseenNotification(notificationEntry)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt
index e44a77c30999..a4fa72942380 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt
@@ -34,7 +34,10 @@ constructor(
TAG,
LogLevel.DEBUG,
messageInitializer = { bool1 = trackingUnseen },
- messagePrinter = { "${if (bool1) "Start" else "Stop"} tracking unseen notifications." },
+ messagePrinter = {
+ "${if (bool1) "Start" else "Stop"} " +
+ "tracking unseen notifications because of settings change."
+ },
)
fun logShadeVisible(numUnseen: Int) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
index 73ce48b2324a..96c260bb0852 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
@@ -25,7 +25,7 @@ import com.android.systemui.statusbar.notification.collection.SortBySectionTimeF
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
import javax.inject.Inject
@@ -88,11 +88,10 @@ constructor(
mCoordinators.add(hideLocallyDismissedNotifsCoordinator)
mCoordinators.add(hideNotifsForOtherUsersCoordinator)
mCoordinators.add(keyguardCoordinator)
- if (NotificationMinimalismPrototype.isEnabled) {
+ if (NotificationMinimalism.isEnabled) {
mCoordinators.add(lockScreenMinimalismCoordinator)
- } else {
- mCoordinators.add(unseenKeyguardCoordinator)
}
+ mCoordinators.add(unseenKeyguardCoordinator)
mCoordinators.add(rankingCoordinator)
mCoordinators.add(colorizedFgsCoordinator)
mCoordinators.add(deviceProvisionedCoordinator)
@@ -125,11 +124,11 @@ constructor(
}
// Manually add Ordered Sections
- if (NotificationMinimalismPrototype.isEnabled) {
+ if (NotificationMinimalism.isEnabled) {
mOrderedSections.add(lockScreenMinimalismCoordinator.topOngoingSectioner) // Top Ongoing
}
mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp
- if (NotificationMinimalismPrototype.isEnabled) {
+ if (NotificationMinimalism.isEnabled) {
mOrderedSections.add(lockScreenMinimalismCoordinator.topUnseenSectioner) // Top Unseen
}
mOrderedSections.add(colorizedFgsCoordinator.sectioner) // ForegroundService
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
index bfea2ba6b839..cf1329c6b564 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
@@ -35,7 +35,6 @@ import com.android.systemui.statusbar.notification.collection.coordinator.dagger
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
import com.android.systemui.statusbar.policy.HeadsUpManager
import com.android.systemui.statusbar.policy.headsUpEvents
import com.android.systemui.util.asIndenting
@@ -51,7 +50,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@@ -87,7 +85,6 @@ constructor(
private var unseenFilterEnabled = false
override fun attach(pipeline: NotifPipeline) {
- NotificationMinimalismPrototype.assertInLegacyMode()
pipeline.addFinalizeFilter(unseenNotifFilter)
pipeline.addCollectionListener(collectionListener)
scope.launch { trackUnseenFilterSettingChanges() }
@@ -253,10 +250,6 @@ constructor(
}
private fun unseenFeatureEnabled(): Flow<Boolean> {
- if (NotificationMinimalismPrototype.isEnabled) {
- // TODO(b/330387368): should this really just be turned off? If so, hide the setting.
- return flowOf(false)
- }
return seenNotificationsInteractor.isLockScreenShowOnlyUnseenNotificationsEnabled()
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
index 6d0148a24cf8..41419f31eb7a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
@@ -41,7 +41,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager;
import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor;
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype;
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.util.concurrency.DelayableExecutor;
import com.android.systemui.util.kotlin.BooleanFlowOperators;
@@ -170,7 +170,7 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable {
if (entry == null) {
return false;
}
- boolean isTopUnseen = NotificationMinimalismPrototype.isEnabled()
+ boolean isTopUnseen = NotificationMinimalism.isEnabled()
&& (mSeenNotificationsInteractor.isTopUnseenNotification(entry)
|| mSeenNotificationsInteractor.isTopOngoingNotification(entry));
if (isTopUnseen || mHeadsUpManager.isHeadsUpEntry(entry.getKey())) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
index 29564326481f..1babe47559e5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
@@ -23,7 +23,7 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.util.printSection
import com.android.systemui.util.settings.SecureSettings
import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
@@ -57,25 +57,25 @@ constructor(
/** Set the entry that is identified as the top ongoing notification. */
fun setTopOngoingNotification(entry: NotificationEntry?) {
- if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
+ if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return
notificationListRepository.topOngoingNotificationKey.value = entry?.key
}
/** Determine if the given notification is the top ongoing notification. */
fun isTopOngoingNotification(entry: NotificationEntry?): Boolean =
- if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false
+ if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) false
else
entry != null && notificationListRepository.topOngoingNotificationKey.value == entry.key
/** Set the entry that is identified as the top unseen notification. */
fun setTopUnseenNotification(entry: NotificationEntry?) {
- if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
+ if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return
notificationListRepository.topUnseenNotificationKey.value = entry?.key
}
/** Determine if the given notification is the top unseen notification. */
fun isTopUnseenNotification(entry: NotificationEntry?): Boolean =
- if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false
+ if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) false
else entry != null && notificationListRepository.topUnseenNotificationKey.value == entry.key
fun dump(pw: IndentingPrintWriter) =
@@ -120,4 +120,29 @@ constructor(
// only track the most recent emission, if events are happening faster than they can be
// consumed
.conflate()
+
+ fun isLockScreenNotificationMinimalismEnabled(): Flow<Boolean> =
+ secureSettings
+ // emit whenever the setting has changed
+ .observerFlow(
+ UserHandle.USER_ALL,
+ Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM,
+ )
+ // perform a query immediately
+ .onStart { emit(Unit) }
+ // for each change, lookup the new value
+ .map {
+ secureSettings.getIntForUser(
+ name = Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM,
+ default = 1,
+ userHandle = UserHandle.USER_CURRENT,
+ ) == 1
+ }
+ // don't emit anything if nothing has changed
+ .distinctUntilChanged()
+ // perform lookups on the bg thread pool
+ .flowOn(bgDispatcher)
+ // only track the most recent emission, if events are happening faster than they can be
+ // consumed
+ .conflate()
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalism.kt
index 06f3db504aaf..70bb2722c678 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalism.kt
@@ -17,23 +17,23 @@
package com.android.systemui.statusbar.notification.shared
import android.os.SystemProperties
-import com.android.systemui.Flags
+import com.android.server.notification.Flags
import com.android.systemui.flags.FlagToken
import com.android.systemui.flags.RefactorFlagUtils
/** Helper for reading or using the minimalism prototype flag state. */
@Suppress("NOTHING_TO_INLINE")
-object NotificationMinimalismPrototype {
- const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
+object NotificationMinimalism {
+ const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM
/** A token used for dependency declaration */
val token: FlagToken
get() = FlagToken(FLAG_NAME, isEnabled)
- /** Is the heads-up cycling animation enabled */
+ /** Is the notification minimalism enabled */
@JvmStatic
inline val isEnabled
- get() = Flags.notificationMinimalismPrototype()
+ get() = Flags.notificationMinimalism()
/**
* The prototype will (by default) use a promoter to ensure that the top unseen notification is
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
index 06222fdb2761..3bc549543ef2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
@@ -29,7 +29,7 @@ import com.android.systemui.statusbar.StatusBarState.KEYGUARD
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.ExpandableView
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.statusbar.policy.SplitShadeStateController
import com.android.systemui.util.Compile
import com.android.systemui.util.children
@@ -74,7 +74,7 @@ constructor(
/** Whether we allow keyguard to show less important notifications above the shelf. */
private val limitLockScreenToOneImportant
- get() = NotificationMinimalismPrototype.isEnabled
+ get() = NotificationMinimalism.isEnabled
/** Minimum space between two notifications, see [calculateGapAndDividerHeight]. */
private var dividerHeight by notNull<Float>()
@@ -406,7 +406,7 @@ constructor(
fun updateResources() {
maxKeyguardNotifications =
infiniteIfNegative(resources.getInteger(R.integer.keyguard_max_notification_count))
- maxNotificationsExcludesMedia = NotificationMinimalismPrototype.isEnabled
+ maxNotificationsExcludesMedia = NotificationMinimalism.isEnabled
dividerHeight =
max(1f, resources.getDimensionPixelSize(R.dimen.notification_divider_height).toFloat())
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 e1dcc524c486..78edd3916a88 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
@@ -19,8 +19,10 @@ package com.android.systemui.statusbar.policy.ui.dialog
import android.content.Intent
import android.provider.Settings
import android.util.Log
+import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
@@ -30,6 +32,7 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.android.compose.PlatformButton
import com.android.compose.PlatformOutlinedButton
+import com.android.compose.theme.PlatformTheme
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.animation.DialogCuj
@@ -74,7 +77,7 @@ constructor(
currentDialog?.dismiss()
}
- currentDialog = sysuiDialogFactory.create() { ModesDialogContent(it) }
+ currentDialog = sysuiDialogFactory.create { ModesDialogContent(it) }
currentDialog
?.lifecycle
?.addObserver(
@@ -91,28 +94,34 @@ constructor(
@Composable
private fun ModesDialogContent(dialog: SystemUIDialog) {
- AlertDialogContent(
- 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) }) {
- Text(stringResource(R.string.zen_modes_dialog_settings))
- }
- },
- positiveButton = {
- PlatformButton(onClick = { dialog.dismiss() }) {
- Text(stringResource(R.string.zen_modes_dialog_done))
- }
- },
- )
+ // TODO(b/369376884): The composable does correctly update when the theme changes
+ // while the dialog is open, but the background (which we don't control here)
+ // doesn't, which causes us to show things like white text on a white background.
+ // as a workaround, we remember the original theme and keep it on recomposition.
+ val isCurrentlyInDarkTheme = isSystemInDarkTheme()
+ val cachedDarkTheme = remember { isCurrentlyInDarkTheme }
+ PlatformTheme(isDarkTheme = cachedDarkTheme) {
+ AlertDialogContent(
+ 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) }) {
+ Text(stringResource(R.string.zen_modes_dialog_settings))
+ }
+ },
+ positiveButton = {
+ PlatformButton(onClick = { dialog.dismiss() }) {
+ Text(stringResource(R.string.zen_modes_dialog_done))
+ }
+ },
+ )
+ }
}
@VisibleForTesting
@@ -129,7 +138,7 @@ constructor(
activityStarter.startActivity(
ZEN_MODE_SETTINGS_INTENT,
true /* dismissShade */,
- animationController
+ animationController,
)
}
@@ -163,7 +172,7 @@ constructor(
Log.w(
TAG,
"Cannot launch from dialog, the dialog is not present. " +
- "Will launch activity without animating."
+ "Will launch activity without animating.",
)
}
@@ -172,11 +181,7 @@ constructor(
if (animationController == null) {
currentDialog?.dismiss()
}
- activityStarter.startActivity(
- intent,
- true, /* dismissShade */
- animationController,
- )
+ activityStarter.startActivity(intent, true, /* dismissShade */ animationController)
}
companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
index 680df1584f89..dcf32a5f574d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
@@ -137,7 +137,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
MediaTestUtils.emptyMediaData.copy(
app = PACKAGE,
packageName = PACKAGE,
- token = session.sessionToken
+ token = session.sessionToken,
)
resumeData = mediaData.copy(token = null, active = false, resumption = true)
@@ -237,7 +237,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
// Assuming we're registered
testOnMediaDataLoaded_registersPlaybackListener()
- mediaCallbackCaptor.value.onPlaybackStateChanged(
+ onPlaybackStateChanged(
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
)
assertThat(mainExecutor.numPending()).isEqualTo(1)
@@ -249,7 +249,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
// Assuming we have a pending timeout
testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
- mediaCallbackCaptor.value.onPlaybackStateChanged(
+ onPlaybackStateChanged(
PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 0f).build()
)
assertThat(mainExecutor.numPending()).isEqualTo(0)
@@ -261,7 +261,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
// Assuming we have a pending timeout
testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
- mediaCallbackCaptor.value.onPlaybackStateChanged(
+ onPlaybackStateChanged(
PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build()
)
assertThat(mainExecutor.numPending()).isEqualTo(1)
@@ -435,7 +435,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
// When the playback state changes, and has different actions
val playingState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build()
- mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
+ onPlaybackStateChanged(playingState)
assertThat(uiExecutor.runAllReady()).isEqualTo(1)
// Then the callback is invoked
@@ -448,7 +448,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
PlaybackState.CustomAction.Builder(
"ACTION_1",
"custom action 1",
- android.R.drawable.ic_media_ff
+ android.R.drawable.ic_media_ff,
)
.build()
val pausedState =
@@ -463,7 +463,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
PlaybackState.CustomAction.Builder(
"ACTION_2",
"custom action 2",
- android.R.drawable.ic_media_rew
+ android.R.drawable.ic_media_rew,
)
.build()
val pausedStateTwoActions =
@@ -472,7 +472,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
.addCustomAction(customOne)
.addCustomAction(customTwo)
.build()
- mediaCallbackCaptor.value.onPlaybackStateChanged(pausedStateTwoActions)
+ onPlaybackStateChanged(pausedStateTwoActions)
assertThat(uiExecutor.runAllReady()).isEqualTo(1)
// Then the callback is invoked
@@ -485,7 +485,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
loadMediaDataWithPlaybackState(stateWithActions)
// When the playback state updates with the same actions
- mediaCallbackCaptor.value.onPlaybackStateChanged(stateWithActions)
+ onPlaybackStateChanged(stateWithActions)
// Then the callback is not invoked again
verify(stateCallback, never()).invoke(eq(KEY), any())
@@ -512,7 +512,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
.setActions(PlaybackState.ACTION_PAUSE)
.addCustomAction(customTwo)
.build()
- mediaCallbackCaptor.value.onPlaybackStateChanged(stateTwo)
+ onPlaybackStateChanged(stateTwo)
// Then the callback is not invoked
verify(stateCallback, never()).invoke(eq(KEY), any())
@@ -544,7 +544,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
// When the playback state changes to playing
val playingState =
PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
- mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
+ onPlaybackStateChanged(playingState)
uiExecutor.runAllReady()
// Then the callback is invoked
@@ -561,7 +561,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
// When the playback state is updated, but still not playing
val playingState =
PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build()
- mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
+ onPlaybackStateChanged(playingState)
// Then the callback is not invoked
verify(stateCallback, never()).invoke(eq(KEY), eq(playingState!!))
@@ -571,7 +571,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
fun testTimeoutCallback_dozedPastTimeout_invokedOnWakeup() {
// When paused media is loaded
testOnMediaDataLoaded_registersPlaybackListener()
- mediaCallbackCaptor.value.onPlaybackStateChanged(
+ onPlaybackStateChanged(
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
)
verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
@@ -597,7 +597,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
val time = clock.currentTimeMillis()
clock.setElapsedRealtime(time)
testOnMediaDataLoaded_registersPlaybackListener()
- mediaCallbackCaptor.value.onPlaybackStateChanged(
+ onPlaybackStateChanged(
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
)
verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
@@ -706,4 +706,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
bgExecutor.runAllReady()
uiExecutor.runAllReady()
}
+
+ private fun onPlaybackStateChanged(state: PlaybackState) {
+ mediaCallbackCaptor.value.onPlaybackStateChanged(state)
+ bgExecutor.runAllReady()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
index eea02eec7099..2f8f45cb0197 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
@@ -29,6 +29,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -887,6 +888,34 @@ public class InternetDialogDelegateControllerTest extends SysuiTestCase {
}
@Test
+ public void getActiveAutoSwitchNonDdsSubId_registerCallbackForExistedSubId_notRegister() {
+ mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true);
+
+ // Adds non DDS subId
+ SubscriptionInfo info = mock(SubscriptionInfo.class);
+ doReturn(SUB_ID2).when(info).getSubscriptionId();
+ doReturn(false).when(info).isOpportunistic();
+ when(mSubscriptionManager.getActiveSubscriptionInfo(anyInt())).thenReturn(info);
+
+ mInternetDialogController.getActiveAutoSwitchNonDdsSubId();
+
+ // 1st time is onStart(), 2nd time is getActiveAutoSwitchNonDdsSubId()
+ verify(mTelephonyManager, times(2)).registerTelephonyCallback(any(), any());
+ assertThat(mInternetDialogController.mSubIdTelephonyCallbackMap.size() == 2);
+
+ // Adds non DDS subId again
+ doReturn(SUB_ID2).when(info).getSubscriptionId();
+ doReturn(false).when(info).isOpportunistic();
+ when(mSubscriptionManager.getActiveSubscriptionInfo(anyInt())).thenReturn(info);
+
+ mInternetDialogController.getActiveAutoSwitchNonDdsSubId();
+
+ // Does not add due to cached subInfo in mSubIdTelephonyCallbackMap.
+ verify(mTelephonyManager, times(2)).registerTelephonyCallback(any(), any());
+ assertThat(mInternetDialogController.mSubIdTelephonyCallbackMap.size() == 2);
+ }
+
+ @Test
public void getMobileNetworkSummary() {
mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true);
Resources res1 = mock(Resources.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
index 0407fc14d35a..ac7388281a15 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
@@ -24,7 +24,7 @@ import androidx.test.filters.SmallTest
import com.android.dx.mockito.inline.extended.ExtendedMockito
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NOTIFICATIONS_USE_PEOPLE_FILTERING
import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
import com.android.systemui.util.DeviceConfigProxyFake
import com.android.systemui.util.Utils
@@ -42,7 +42,7 @@ import org.mockito.quality.Strictness
@RunWith(AndroidJUnit4::class)
@SmallTest
// this class has no testable logic with either of these flags enabled
-@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalismPrototype.FLAG_NAME)
+@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalism.FLAG_NAME)
class NotificationSectionsFeatureManagerTest : SysuiTestCase() {
lateinit var manager: NotificationSectionsFeatureManager
private val proxyFake = DeviceConfigProxyFake()
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 e2b283b06562..38bc758232a2 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
@@ -22,7 +22,6 @@ 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.shade.domain.interactor.shadeInteractor
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -34,7 +33,6 @@ val Kosmos.keyguardDismissActionInteractor by
transitionInteractor = keyguardTransitionInteractor,
dismissInteractor = keyguardDismissInteractor,
applicationScope = testScope.backgroundScope,
- sceneInteractor = { sceneInteractor },
deviceUnlockedInteractor = { deviceUnlockedInteractor },
powerInteractor = powerInteractor,
alternateBouncerInteractor = alternateBouncerInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt
index b19e221d099c..3d2bd6cf49d4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt
@@ -45,3 +45,17 @@ var Kosmos.lockScreenShowOnlyUnseenNotificationsSetting: Boolean
UserHandle.USER_CURRENT,
)
}
+
+var Kosmos.lockScreenNotificationMinimalismSetting: Boolean
+ get() =
+ fakeSettings.getIntForUser(
+ Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM,
+ UserHandle.USER_CURRENT,
+ ) == 1
+ set(value) {
+ fakeSettings.putIntForUser(
+ Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM,
+ if (value) 1 else 0,
+ UserHandle.USER_CURRENT,
+ )
+ }
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index c8f8c2a6b223..082459b8c863 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -21,11 +21,13 @@ import static com.android.server.appfunctions.AppFunctionExecutors.THREAD_POOL_E
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.WorkerThread;
+import android.app.appfunctions.AppFunctionService;
import android.app.appfunctions.AppFunctionStaticMetadataHelper;
import android.app.appfunctions.ExecuteAppFunctionAidlRequest;
import android.app.appfunctions.ExecuteAppFunctionResponse;
import android.app.appfunctions.IAppFunctionManager;
import android.app.appfunctions.IAppFunctionService;
+import android.app.appfunctions.ICancellationCallback;
import android.app.appfunctions.IExecuteAppFunctionCallback;
import android.app.appfunctions.SafeOneTimeExecuteAppFunctionCallback;
import android.app.appsearch.AppSearchManager;
@@ -37,11 +39,15 @@ import android.app.appsearch.observer.SchemaChangeInfo;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
+import android.os.CancellationSignal;
+import android.os.IBinder;
+import android.os.ICancellationSignal;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.infra.AndroidFuture;
import com.android.server.SystemService.TargetUser;
import com.android.server.appfunctions.RemoteServiceCaller.RunServiceCallCallback;
import com.android.server.appfunctions.RemoteServiceCaller.ServiceUsageCompleteListener;
@@ -99,7 +105,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
}
@Override
- public void executeAppFunction(
+ public ICancellationSignal executeAppFunction(
@NonNull ExecuteAppFunctionAidlRequest requestInternal,
@NonNull IExecuteAppFunctionCallback executeAppFunctionCallback) {
Objects.requireNonNull(requestInternal);
@@ -120,11 +126,14 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
ExecuteAppFunctionResponse.RESULT_DENIED,
exception.getMessage(),
/* extras= */ null));
- return;
+ return null;
}
int callingUid = Binder.getCallingUid();
- int callingPid = Binder.getCallingUid();
+ int callingPid = Binder.getCallingPid();
+
+ ICancellationSignal localCancelTransport = CancellationSignal.createTransport();
+
THREAD_POOL_EXECUTOR.execute(
() -> {
try {
@@ -132,12 +141,14 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
requestInternal,
callingUid,
callingPid,
+ localCancelTransport,
safeExecuteAppFunctionCallback);
} catch (Exception e) {
safeExecuteAppFunctionCallback.onResult(
mapExceptionToExecuteAppFunctionResponse(e));
}
});
+ return localCancelTransport;
}
@WorkerThread
@@ -145,6 +156,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
ExecuteAppFunctionAidlRequest requestInternal,
int callingUid,
int callingPid,
+ ICancellationSignal localCancelTransport,
SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback) {
UserHandle targetUser = requestInternal.getUserHandle();
// TODO(b/354956319): Add and honor the new enterprise policies.
@@ -203,6 +215,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
requestInternal,
serviceIntent,
targetUser,
+ localCancelTransport,
safeExecuteAppFunctionCallback,
/* bindFlags= */ Context.BIND_AUTO_CREATE
| Context.BIND_FOREGROUND_SERVICE);
@@ -219,8 +232,19 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
@NonNull ExecuteAppFunctionAidlRequest requestInternal,
@NonNull Intent serviceIntent,
@NonNull UserHandle targetUser,
+ @NonNull ICancellationSignal cancellationSignalTransport,
@NonNull SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback,
int bindFlags) {
+ CancellationSignal cancellationSignal =
+ CancellationSignal.fromTransport(cancellationSignalTransport);
+ ICancellationCallback cancellationCallback =
+ new ICancellationCallback.Stub() {
+ @Override
+ public void sendCancellationTransport(
+ @NonNull ICancellationSignal cancellationTransport) {
+ cancellationSignal.setRemote(cancellationTransport);
+ }
+ };
boolean bindServiceResult =
mRemoteServiceCaller.runServiceCall(
serviceIntent,
@@ -236,6 +260,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
try {
service.executeAppFunction(
requestInternal.getClientRequest(),
+ cancellationCallback,
new IExecuteAppFunctionCallback.Stub() {
@Override
public void onResult(
diff --git a/services/core/java/android/os/BatteryStatsInternal.java b/services/core/java/android/os/BatteryStatsInternal.java
index 0713999d4354..60b826b50045 100644
--- a/services/core/java/android/os/BatteryStatsInternal.java
+++ b/services/core/java/android/os/BatteryStatsInternal.java
@@ -41,6 +41,7 @@ public abstract class BatteryStatsInternal {
public static final int CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER = 3;
public static final int CPU_WAKEUP_SUBSYSTEM_SENSOR = 4;
public static final int CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA = 5;
+ public static final int CPU_WAKEUP_SUBSYSTEM_BLUETOOTH = 6;
/** @hide */
@IntDef(prefix = {"CPU_WAKEUP_SUBSYSTEM_"}, value = {
@@ -50,6 +51,7 @@ public abstract class BatteryStatsInternal {
CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER,
CPU_WAKEUP_SUBSYSTEM_SENSOR,
CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA,
+ CPU_WAKEUP_SUBSYSTEM_BLUETOOTH,
})
@Retention(RetentionPolicy.SOURCE)
public @interface CpuWakeupSubsystem {
@@ -99,6 +101,14 @@ public abstract class BatteryStatsInternal {
public abstract void noteCpuWakingNetworkPacket(Network network, long elapsedMillis, int uid);
/**
+ * Informs battery stats of a sysproxy packet that woke up the CPU
+ *
+ * @param uid The uid that received the packet.
+ * @param elapsedMillis The time of the packet's arrival in elapsed timebase.
+ */
+ public abstract void noteCpuWakingBluetoothProxyPacket(int uid, long elapsedMillis);
+
+ /**
* Informs battery stats of binder stats for the given work source UID.
*/
public abstract void noteBinderCallStats(int workSourceUid, long incrementalBinderCallCount,
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index d86bae19f174..e64a4803b14f 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -219,7 +219,7 @@ class StorageManagerService extends IStorageManager.Stub
public static final int FAILED_MOUNT_RESET_TIMEOUT_SECONDS = 10;
/** Extended timeout for the system server watchdog. */
- private static final int SLOW_OPERATION_WATCHDOG_TIMEOUT_MS = 20 * 1000;
+ private static final int SLOW_OPERATION_WATCHDOG_TIMEOUT_MS = 30 * 1000;
/** Extended timeout for the system server watchdog for vold#partition operation. */
private static final int PARTITION_OPERATION_WATCHDOG_TIMEOUT_MS = 3 * 60 * 1000;
@@ -3251,7 +3251,7 @@ class StorageManagerService extends IStorageManager.Stub
if (Binder.getCallingUid() != android.os.Process.SYSTEM_UID) {
throw new SecurityException("no permission to commit checkpoint changes");
}
-
+ extendWatchdogTimeout("vold#commitChanges might be slow");
mVold.commitChanges();
}
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 15277cebac6e..ef82c7477558 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -664,6 +664,8 @@ public final class BatteryStatsService extends IBatteryStats.Stub
} else if (nc.hasTransport(TRANSPORT_CELLULAR)) {
return CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA;
}
+ // For TRANSPORT_BLUETOOTH, we have a separate channel to catch Bluetooth wakeups.
+ // See noteCpuWakingSysproxyPacket method.
return CPU_WAKEUP_SUBSYSTEM_UNKNOWN;
}
@@ -686,6 +688,15 @@ public final class BatteryStatsService extends IBatteryStats.Stub
}
@Override
+ public void noteCpuWakingBluetoothProxyPacket(int uid, long elapsedMillis) {
+ if (uid < 0) {
+ Slog.e(TAG, "Invalid uid for waking bluetooth proxy packet: " + uid);
+ return;
+ }
+ noteCpuWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, elapsedMillis, uid);
+ }
+
+ @Override
public void noteBinderCallStats(int workSourceUid, long incrementatCallCount,
Collection<BinderCallsStats.CallStat> callStats) {
synchronized (BatteryStatsService.this.mLock) {
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index a6389f7f5311..e0cf96fbccd0 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -1031,6 +1031,9 @@ public class AppOpsService extends IAppOpsService.Stub {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
+ if (action == null) {
+ return;
+ }
String pkgName = intent.getData().getEncodedSchemeSpecificPart().intern();
int uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID);
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 88907e35854f..1f9eb082aaf4 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -3377,10 +3377,18 @@ public final class DisplayManagerService extends SystemService {
private void dumpInternal(PrintWriter pw) {
pw.println("DISPLAY MANAGER (dumpsys display)");
BrightnessTracker brightnessTrackerLocal;
+ SparseArray<DisplayPowerController> displayPowerControllersLocal = new SparseArray<>();
+ int displayPowerControllerCount;
synchronized (mSyncRoot) {
brightnessTrackerLocal = mBrightnessTracker;
+ displayPowerControllerCount = mDisplayPowerControllers.size();
+ for (int i = 0; i < displayPowerControllerCount; i++) {
+ displayPowerControllersLocal.put(
+ mDisplayPowerControllers.keyAt(i), mDisplayPowerControllers.valueAt(i));
+ }
+
pw.println(" mSafeMode=" + mSafeMode);
pw.println(" mPendingTraversal=" + mPendingTraversal);
pw.println(" mViewports=" + mViewports);
@@ -3451,13 +3459,6 @@ public final class DisplayManagerService extends SystemService {
+ ", mWifiDisplayScanRequested=" + callback.mWifiDisplayScanRequested);
}
- final int displayPowerControllerCount = mDisplayPowerControllers.size();
- pw.println();
- pw.println("Display Power Controllers: size=" + displayPowerControllerCount);
- for (int i = 0; i < displayPowerControllerCount; i++) {
- mDisplayPowerControllers.valueAt(i).dump(pw);
- }
-
pw.println();
mPersistentDataStore.dump(pw);
@@ -3470,6 +3471,12 @@ public final class DisplayManagerService extends SystemService {
mDisplayWindowPolicyControllers.valueAt(i).second.dump(" ", pw);
}
}
+ pw.println();
+ pw.println("Display Power Controllers: size=" + displayPowerControllerCount);
+ for (int i = 0; i < displayPowerControllerCount; i++) {
+ displayPowerControllersLocal.valueAt(i).dump(pw);
+ }
+
if (brightnessTrackerLocal != null) {
pw.println();
brightnessTrackerLocal.dump(pw);
diff --git a/services/core/java/com/android/server/flags/pinner.aconfig b/services/core/java/com/android/server/flags/pinner.aconfig
index 2f817dbb9a7f..345366882d5a 100644
--- a/services/core/java/com/android/server/flags/pinner.aconfig
+++ b/services/core/java/com/android/server/flags/pinner.aconfig
@@ -9,8 +9,11 @@ flag {
}
flag {
- name: "skip_home_art_pins"
- namespace: "system_performance"
- description: "Ablation study flag that controls if home app odex/vdex files should be pinned in memory."
- bug: "340935152"
-} \ No newline at end of file
+ name: "pin_global_quota"
+ namespace: "system_performance"
+ description: "This flag controls whether pinner will use a global quota or not"
+ bug: "340935152"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index 48d24f2e14dd..47f579db604f 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -195,10 +195,9 @@ public final class MediaProjectionManagerService extends SystemService
== PackageManager.PERMISSION_GRANTED) {
return true;
}
- boolean operationActive = mAppOps.isOperationActive(AppOpsManager.OP_PROJECT_MEDIA,
- mProjectionGrant.uid,
- mProjectionGrant.packageName);
- if (operationActive) {
+ if (AppOpsManager.MODE_ALLOWED == mAppOps.noteOpNoThrow(AppOpsManager.OP_PROJECT_MEDIA,
+ mProjectionGrant.uid, mProjectionGrant.packageName, /* attributionTag= */ null,
+ "recording lockscreen")) {
// Some tools use media projection by granting the OP_PROJECT_MEDIA app
// op via a shell command. Those tools can be granted keyguard capture
return true;
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index c3a714b0eef0..79633f19715b 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -2579,6 +2579,7 @@ public class NotificationManagerService extends SystemService {
mNotificationChannelLogger,
mAppOps,
mUserProfiles,
+ mUgmInternal,
mShowReviewPermissionsNotification,
Clock.systemUTC());
mRankingHelper = new RankingHelper(getContext(), mRankingHandler, mPreferencesHelper,
@@ -6672,13 +6673,7 @@ public class NotificationManagerService extends SystemService {
final Uri originalSoundUri =
(originalChannel != null) ? originalChannel.getSound() : null;
if (soundUri != null && !Objects.equals(originalSoundUri, soundUri)) {
- Binder.withCleanCallingIdentity(() -> {
- mUgmInternal.checkGrantUriPermission(sourceUid, null,
- ContentProvider.getUriWithoutUserId(soundUri),
- Intent.FLAG_GRANT_READ_URI_PERMISSION,
- ContentProvider.getUserIdFromUri(soundUri,
- UserHandle.getUserId(sourceUid)));
- });
+ PermissionHelper.grantUriPermission(mUgmInternal, soundUri, sourceUid);
}
}
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index b9f0968b5864..3ba93845a290 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -1493,14 +1493,23 @@ public final class NotificationRecord {
final Notification notification = getNotification();
notification.visitUris((uri) -> {
- visitGrantableUri(uri, false, false);
+ if (com.android.server.notification.Flags.notificationVerifyChannelSoundUri()) {
+ visitGrantableUri(uri, false, false);
+ } else {
+ oldVisitGrantableUri(uri, false, false);
+ }
});
if (notification.getChannelId() != null) {
NotificationChannel channel = getChannel();
if (channel != null) {
- visitGrantableUri(channel.getSound(), (channel.getUserLockedFields()
- & NotificationChannel.USER_LOCKED_SOUND) != 0, true);
+ if (com.android.server.notification.Flags.notificationVerifyChannelSoundUri()) {
+ visitGrantableUri(channel.getSound(), (channel.getUserLockedFields()
+ & NotificationChannel.USER_LOCKED_SOUND) != 0, true);
+ } else {
+ oldVisitGrantableUri(channel.getSound(), (channel.getUserLockedFields()
+ & NotificationChannel.USER_LOCKED_SOUND) != 0, true);
+ }
}
}
} finally {
@@ -1516,7 +1525,7 @@ public final class NotificationRecord {
* {@link #mGrantableUris}. Otherwise, this will either log or throw
* {@link SecurityException} depending on target SDK of enqueuing app.
*/
- private void visitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) {
+ private void oldVisitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) {
if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return;
if (mGrantableUris != null && mGrantableUris.contains(uri)) {
@@ -1555,6 +1564,45 @@ public final class NotificationRecord {
}
}
+ /**
+ * Note the presence of a {@link Uri} that should have permission granted to
+ * whoever will be rendering it.
+ * <p>
+ * If the enqueuing app has the ability to grant access, it will be added to
+ * {@link #mGrantableUris}. Otherwise, this will either log or throw
+ * {@link SecurityException} depending on target SDK of enqueuing app.
+ */
+ private void visitGrantableUri(Uri uri, boolean userOverriddenUri,
+ boolean isSound) {
+ if (mGrantableUris != null && mGrantableUris.contains(uri)) {
+ return; // already verified this URI
+ }
+
+ final int sourceUid = getSbn().getUid();
+ try {
+ PermissionHelper.grantUriPermission(mUgmInternal, uri, sourceUid);
+
+ if (mGrantableUris == null) {
+ mGrantableUris = new ArraySet<>();
+ }
+ mGrantableUris.add(uri);
+ } catch (SecurityException e) {
+ if (!userOverriddenUri) {
+ if (isSound) {
+ mSound = Settings.System.DEFAULT_NOTIFICATION_URI;
+ Log.w(TAG, "Replacing " + uri + " from " + sourceUid + ": " + e.getMessage());
+ } else {
+ if (mTargetSdkVersion >= Build.VERSION_CODES.P) {
+ throw e;
+ } else {
+ Log.w(TAG,
+ "Ignoring " + uri + " from " + sourceUid + ": " + e.getMessage());
+ }
+ }
+ }
+ }
+ }
+
public LogMaker getLogMaker(long now) {
LogMaker lm = getSbn().getLogMaker()
.addTaggedData(MetricsEvent.FIELD_NOTIFICATION_CHANNEL_IMPORTANCE, mImportance)
diff --git a/services/core/java/com/android/server/notification/PermissionHelper.java b/services/core/java/com/android/server/notification/PermissionHelper.java
index b6f48890c528..1464d481311a 100644
--- a/services/core/java/com/android/server/notification/PermissionHelper.java
+++ b/services/core/java/com/android/server/notification/PermissionHelper.java
@@ -25,19 +25,25 @@ import android.Manifest;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.companion.virtual.VirtualDeviceManager;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
+import android.net.Uri;
import android.os.Binder;
import android.os.RemoteException;
+import android.os.UserHandle;
import android.permission.IPermissionManager;
import android.util.ArrayMap;
import android.util.Pair;
import android.util.Slog;
import com.android.internal.util.ArrayUtils;
+import com.android.server.uri.UriGrantsManagerInternal;
import java.util.Collections;
import java.util.HashSet;
@@ -58,7 +64,7 @@ public final class PermissionHelper {
private final IPermissionManager mPermManager;
public PermissionHelper(Context context, IPackageManager packageManager,
- IPermissionManager permManager) {
+ IPermissionManager permManager) {
mContext = context;
mPackageManager = packageManager;
mPermManager = permManager;
@@ -298,6 +304,19 @@ public final class PermissionHelper {
return false;
}
+ static void grantUriPermission(final UriGrantsManagerInternal ugmInternal, Uri uri,
+ int sourceUid) {
+ if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return;
+
+ Binder.withCleanCallingIdentity(() -> {
+ // This will throw a SecurityException if the caller can't grant.
+ ugmInternal.checkGrantUriPermission(sourceUid, null,
+ ContentProvider.getUriWithoutUserId(uri),
+ Intent.FLAG_GRANT_READ_URI_PERMISSION,
+ ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(sourceUid)));
+ });
+ }
+
public static class PackagePermission {
public final String packageName;
public final @UserIdInt int userId;
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 85c395781d0a..9e70f815dff9 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -94,6 +94,7 @@ import com.android.internal.util.XmlUtils;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.notification.PermissionHelper.PackagePermission;
+import com.android.server.uri.UriGrantsManagerInternal;
import org.json.JSONArray;
import org.json.JSONException;
@@ -219,6 +220,7 @@ public class PreferencesHelper implements RankingConfig {
private final NotificationChannelLogger mNotificationChannelLogger;
private final AppOpsManager mAppOps;
private final ManagedServices.UserProfiles mUserProfiles;
+ private final UriGrantsManagerInternal mUgmInternal;
private SparseBooleanArray mBadgingEnabled;
private SparseBooleanArray mBubblesEnabled;
@@ -239,6 +241,7 @@ public class PreferencesHelper implements RankingConfig {
ZenModeHelper zenHelper, PermissionHelper permHelper, PermissionManager permManager,
NotificationChannelLogger notificationChannelLogger,
AppOpsManager appOpsManager, ManagedServices.UserProfiles userProfiles,
+ UriGrantsManagerInternal ugmInternal,
boolean showReviewPermissionsNotification, Clock clock) {
mContext = context;
mZenModeHelper = zenHelper;
@@ -249,6 +252,7 @@ public class PreferencesHelper implements RankingConfig {
mNotificationChannelLogger = notificationChannelLogger;
mAppOps = appOpsManager;
mUserProfiles = userProfiles;
+ mUgmInternal = ugmInternal;
mShowReviewPermissionsNotification = showReviewPermissionsNotification;
mIsMediaNotificationFilteringEnabled = context.getResources()
.getBoolean(R.bool.config_quickSettingsShowMediaPlayer);
@@ -1169,6 +1173,13 @@ public class PreferencesHelper implements RankingConfig {
}
clearLockedFieldsLocked(channel);
+ // Verify that the app has permission to read the sound Uri
+ // Only check for new channels, as regular apps can only set sound
+ // before creating. See: {@link NotificationChannel#setSound}
+ if (Flags.notificationVerifyChannelSoundUri()) {
+ PermissionHelper.grantUriPermission(mUgmInternal, channel.getSound(), uid);
+ }
+
channel.setImportanceLockedByCriticalDeviceFunction(
r.defaultAppLockedImportance || r.fixedImportance);
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index be3adc142fa4..0b34177d7413 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -144,6 +144,13 @@ flag {
}
flag {
+ name: "notification_minimalism"
+ namespace: "systemui"
+ description: "Minimize the notifications to show on the lockscreen."
+ bug: "330387368"
+}
+
+flag {
name: "notification_force_group_singletons"
namespace: "systemui"
description: "This flag enables forced auto-grouping singleton groups"
@@ -163,3 +170,13 @@ flag {
description: "This flag enables sound uri with vibration source"
bug: "358524009"
}
+
+flag {
+ name: "notification_verify_channel_sound_uri"
+ namespace: "systemui"
+ description: "Verify Uri permission for sound when creating a notification channel"
+ bug: "337775777"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/services/core/java/com/android/server/pinner/PinRangeSource.java b/services/core/java/com/android/server/pinner/PinRangeSource.java
new file mode 100644
index 000000000000..5f9641122294
--- /dev/null
+++ b/services/core/java/com/android/server/pinner/PinRangeSource.java
@@ -0,0 +1,27 @@
+/*
+ * 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.pinner;
+
+/* package */ abstract class PinRangeSource {
+ /**
+ * Retrieve a range to pin.
+ *
+ * @param outPinRange Receives the pin region
+ * @return True if we filled in outPinRange or false if we're out of pin entries
+ */
+ abstract boolean read(PinnerService.PinRange outPinRange);
+} \ No newline at end of file
diff --git a/services/core/java/com/android/server/pinner/PinRangeSourceStatic.java b/services/core/java/com/android/server/pinner/PinRangeSourceStatic.java
new file mode 100644
index 000000000000..d6fc48790883
--- /dev/null
+++ b/services/core/java/com/android/server/pinner/PinRangeSourceStatic.java
@@ -0,0 +1,37 @@
+/*
+ * 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.pinner;
+
+/* package */ class PinRangeSourceStatic extends PinRangeSource {
+ private final int mPinStart;
+ private final int mPinLength;
+ private boolean mDone = false;
+
+ PinRangeSourceStatic(int pinStart, int pinLength) {
+ mPinStart = pinStart;
+ mPinLength = pinLength;
+ }
+
+ @Override
+ boolean read(PinnerService.PinRange outPinRange) {
+ outPinRange.start = mPinStart;
+ outPinRange.length = mPinLength;
+ boolean done = mDone;
+ mDone = true;
+ return !done;
+ }
+} \ No newline at end of file
diff --git a/services/core/java/com/android/server/pinner/PinRangeSourceStream.java b/services/core/java/com/android/server/pinner/PinRangeSourceStream.java
new file mode 100644
index 000000000000..79900b9de463
--- /dev/null
+++ b/services/core/java/com/android/server/pinner/PinRangeSourceStream.java
@@ -0,0 +1,43 @@
+/*
+ * 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.pinner;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/* package */ final class PinRangeSourceStream extends PinRangeSource {
+ private final DataInputStream mStream;
+ private boolean mDone = false;
+
+ PinRangeSourceStream(InputStream stream) {
+ mStream = new DataInputStream(stream);
+ }
+
+ @Override
+ boolean read(PinnerService.PinRange outPinRange) {
+ if (!mDone) {
+ try {
+ outPinRange.start = mStream.readInt();
+ outPinRange.length = mStream.readInt();
+ } catch (IOException ex) {
+ mDone = true;
+ }
+ }
+ return !mDone;
+ }
+} \ No newline at end of file
diff --git a/services/core/java/com/android/server/pinner/PinnedFile.java b/services/core/java/com/android/server/pinner/PinnedFile.java
new file mode 100644
index 000000000000..a8de344d10af
--- /dev/null
+++ b/services/core/java/com/android/server/pinner/PinnedFile.java
@@ -0,0 +1,61 @@
+/*
+ * 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.pinner;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+
+@VisibleForTesting
+public final class PinnedFile implements AutoCloseable {
+ private long mAddress;
+ final long mapSize;
+ final String fileName;
+ public final long bytesPinned;
+
+ // Whether this file was pinned using a pinlist
+ boolean used_pinlist;
+
+ // User defined group name for pinner accounting
+ String groupName = "";
+ ArrayList<PinnedFile> pinnedDeps = new ArrayList<>();
+
+ public PinnedFile(long address, long mapSize, String fileName, long bytesPinned) {
+ mAddress = address;
+ this.mapSize = mapSize;
+ this.fileName = fileName;
+ this.bytesPinned = bytesPinned;
+ }
+
+ @Override
+ public void close() {
+ if (mAddress >= 0) {
+ PinnerUtils.safeMunmap(mAddress, mapSize);
+ mAddress = -1;
+ }
+ for (PinnedFile dep : pinnedDeps) {
+ if (dep != null) {
+ dep.close();
+ }
+ }
+ }
+
+ @Override
+ public void finalize() {
+ close();
+ }
+} \ No newline at end of file
diff --git a/services/core/java/com/android/server/PinnerService.java b/services/core/java/com/android/server/pinner/PinnerService.java
index ef03888d6620..d7ac5203ff53 100644
--- a/services/core/java/com/android/server/PinnerService.java
+++ b/services/core/java/com/android/server/pinner/PinnerService.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
@@ -14,14 +14,14 @@
* limitations under the License.
*/
-package com.android.server;
+package com.android.server.pinner;
import static android.app.ActivityManager.UID_OBSERVER_ACTIVE;
import static android.app.ActivityManager.UID_OBSERVER_GONE;
import static android.os.Process.SYSTEM_UID;
+import static com.android.server.flags.Flags.pinGlobalQuota;
import static com.android.server.flags.Flags.pinWebview;
-import static com.android.server.flags.Flags.skipHomeArtPins;
import android.annotation.EnforcePermission;
import android.annotation.IntDef;
@@ -49,6 +49,7 @@ import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.Looper;
import android.os.Message;
+import android.os.Process;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
@@ -72,13 +73,13 @@ import com.android.internal.app.ResolverActivity;
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.server.LocalServices;
+import com.android.server.SystemService;
import com.android.server.wm.ActivityTaskManagerInternal;
import dalvik.system.DexFile;
import dalvik.system.VMRuntime;
-import java.io.Closeable;
-import java.io.DataInputStream;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -110,8 +111,7 @@ public final class PinnerService extends SystemService {
private static final String PIN_META_FILENAME = "pinlist.meta";
private static final int PAGE_SIZE = (int) Os.sysconf(OsConstants._SC_PAGESIZE);
private static final int MATCH_FLAGS = PackageManager.MATCH_DEFAULT_ONLY
- | PackageManager.MATCH_DIRECT_BOOT_AWARE
- | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+ | PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
private static final int KEY_CAMERA = 0;
private static final int KEY_HOME = 1;
@@ -126,6 +126,8 @@ public final class PinnerService extends SystemService {
public static final String ANON_REGION_STAT_NAME = "[anon]";
+ private static final String SYSTEM_GROUP_NAME = "system";
+
@IntDef({KEY_CAMERA, KEY_HOME, KEY_ASSISTANT})
@Retention(RetentionPolicy.SOURCE)
public @interface AppKey {}
@@ -139,7 +141,8 @@ public final class PinnerService extends SystemService {
private final UserManager mUserManager;
/** The list of the statically pinned files. */
- @GuardedBy("this") private final ArrayMap<String, PinnedFile> mPinnedFiles = new ArrayMap<>();
+ @GuardedBy("this")
+ private final ArrayMap<String, PinnedFile> mPinnedFiles = new ArrayMap<>();
/** The list of the pinned apps. This is a map from {@link AppKey} to a pinned app. */
@GuardedBy("this")
@@ -159,8 +162,8 @@ public final class PinnerService extends SystemService {
/**
* A set of {@link AppKey} that are configured to be pinned.
*/
- @GuardedBy("this")
- private ArraySet<Integer> mPinKeys;
+ @GuardedBy("this") private
+ ArraySet<Integer> mPinKeys;
// Note that we don't use the `_BOOT` namespace for anonymous pinnings, as we want
// them to be responsive to dynamic flag changes for experimentation.
@@ -180,14 +183,23 @@ public final class PinnerService extends SystemService {
private final boolean mConfiguredToPinAssistant;
private final int mConfiguredWebviewPinBytes;
+ // This is the percentage of total device memory that will be used to set the global quota.
+ private final int mConfiguredMaxPinnedMemoryPercentage;
+
+ // This is the global pinner quota that can be pinned.
+ private long mConfiguredMaxPinnedMemory;
+
+ // This is the currently pinned memory.
+ private long mCurrentPinnedMemory = 0;
+
private BinderService mBinderService;
private PinnerHandler mPinnerHandler = null;
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- // If an app has updated, update pinned files accordingly.
- if (Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction())) {
+ // If an app has updated, update pinned files accordingly.
+ if (Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction())) {
Uri packageUri = intent.getData();
String packageName = packageUri.getSchemeSpecificPart();
ArraySet<String> updatedPackages = new ArraySet<>();
@@ -210,7 +222,7 @@ public final class PinnerService extends SystemService {
/** Utility class for testing. */
@VisibleForTesting
- static class Injector {
+ public static class Injector {
protected DeviceConfigInterface getDeviceConfigInterface() {
return DeviceConfigInterface.REAL;
}
@@ -219,9 +231,9 @@ public final class PinnerService extends SystemService {
service.publishBinderService("pinner", binderService);
}
- protected PinnedFile pinFileInternal(String fileToPin,
- int maxBytesToPin, boolean attemptPinIntrospection) {
- return PinnerService.pinFileInternal(fileToPin, maxBytesToPin, attemptPinIntrospection);
+ protected PinnedFile pinFileInternal(PinnerService service, String fileToPin,
+ long maxBytesToPin, boolean attemptPinIntrospection) {
+ return service.pinFileInternal(fileToPin, maxBytesToPin, attemptPinIntrospection);
}
}
@@ -230,7 +242,7 @@ public final class PinnerService extends SystemService {
}
@VisibleForTesting
- PinnerService(Context context, Injector injector) {
+ public PinnerService(Context context, Injector injector) {
super(context);
mContext = context;
@@ -244,6 +256,9 @@ public final class PinnerService extends SystemService {
com.android.internal.R.bool.config_pinnerAssistantApp);
mConfiguredWebviewPinBytes = context.getResources().getInteger(
com.android.internal.R.integer.config_pinnerWebviewPinBytes);
+ mConfiguredMaxPinnedMemoryPercentage = context.getResources().getInteger(
+ com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage);
+
mPinKeys = createPinKeys();
mPinnerHandler = new PinnerHandler(BackgroundThread.get().getLooper());
@@ -261,10 +276,8 @@ public final class PinnerService extends SystemService {
registerUidListener();
registerUserSetupCompleteListener();
- mDeviceConfigInterface.addOnPropertiesChangedListener(
- DEVICE_CONFIG_NAMESPACE_ANON_SIZE,
- new HandlerExecutor(mPinnerHandler),
- mDeviceConfigAnonSizeListener);
+ mDeviceConfigInterface.addOnPropertiesChangedListener(DEVICE_CONFIG_NAMESPACE_ANON_SIZE,
+ new HandlerExecutor(mPinnerHandler), mDeviceConfigAnonSizeListener);
}
@Override
@@ -272,6 +285,10 @@ public final class PinnerService extends SystemService {
if (DEBUG) {
Slog.i(TAG, "Starting PinnerService");
}
+ mConfiguredMaxPinnedMemory =
+ (Process.getTotalMemory()
+ * Math.clamp(mConfiguredMaxPinnedMemoryPercentage, 0, 100))
+ / 100;
mBinderService = new BinderService();
mInjector.publishBinderService(this, mBinderService);
publishLocalService(PinnerService.class, this);
@@ -348,7 +365,7 @@ public final class PinnerService extends SystemService {
protected PinnedFileStats(int uid, PinnedFile file) {
this.uid = uid;
this.filename = file.fileName.substring(file.fileName.lastIndexOf('/') + 1);
- this.sizeKb = file.bytesPinned / 1024;
+ this.sizeKb = (int) file.bytesPinned / 1024;
}
}
@@ -358,20 +375,11 @@ public final class PinnerService extends SystemService {
private void handlePinOnStart() {
// Files to pin come from the overlay and can be specified per-device config
String[] filesToPin = mContext.getResources().getStringArray(
- com.android.internal.R.array.config_defaultPinnerServiceFiles);
+ com.android.internal.R.array.config_defaultPinnerServiceFiles);
// Continue trying to pin each file even if we fail to pin some of them
for (String fileToPin : filesToPin) {
- PinnedFile pf = mInjector.pinFileInternal(fileToPin, Integer.MAX_VALUE,
- /*attemptPinIntrospection=*/false);
- if (pf == null) {
- Slog.e(TAG, "Failed to pin file = " + fileToPin);
- continue;
- }
- synchronized (this) {
- mPinnedFiles.put(pf.fileName, pf);
- }
- pf.groupName = "system";
- pinOptimizedDexDependencies(pf, Integer.MAX_VALUE, null);
+ pinFile(fileToPin, Integer.MAX_VALUE, /*appInfo=*/null, /*groupName=*/SYSTEM_GROUP_NAME,
+ true);
}
refreshPinAnonConfig();
@@ -383,10 +391,9 @@ public final class PinnerService extends SystemService {
* regular home app.
*/
private void registerUserSetupCompleteListener() {
- Uri userSetupCompleteUri = Settings.Secure.getUriFor(
- Settings.Secure.USER_SETUP_COMPLETE);
- mContext.getContentResolver().registerContentObserver(userSetupCompleteUri,
- false, new ContentObserver(null) {
+ Uri userSetupCompleteUri = Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE);
+ mContext.getContentResolver().registerContentObserver(
+ userSetupCompleteUri, false, new ContentObserver(null) {
@Override
public void onChange(boolean selfChange, Uri uri) {
if (userSetupCompleteUri.equals(uri)) {
@@ -409,7 +416,7 @@ public final class PinnerService extends SystemService {
}
@Override
- public void onUidActive(int uid) {
+ public void onUidActive(int uid) {
mPinnerHandler.sendMessage(PooledLambda.obtainMessage(
PinnerService::handleUidActive, PinnerService.this, uid));
}
@@ -423,7 +430,6 @@ public final class PinnerService extends SystemService {
updateActiveState(uid, false /* active */);
int key;
synchronized (this) {
-
// In case we have a pending repin, repin now. See mPendingRepin for more information.
key = mPendingRepin.getOrDefault(uid, -1);
if (key == -1) {
@@ -491,8 +497,8 @@ public final class PinnerService extends SystemService {
private ApplicationInfo getCameraInfo(int userHandle) {
Intent cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA);
- ApplicationInfo info = getApplicationInfoForIntent(cameraIntent, userHandle,
- false /* defaultToSystemApp */);
+ ApplicationInfo info = getApplicationInfoForIntent(
+ cameraIntent, userHandle, false /* defaultToSystemApp */);
// If the STILL_IMAGE_CAMERA intent doesn't resolve, try the _SECURE intent.
// We don't use _SECURE first because it will never get set on a device
@@ -501,16 +507,16 @@ public final class PinnerService extends SystemService {
// preference using this intent.
if (info == null) {
cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE);
- info = getApplicationInfoForIntent(cameraIntent, userHandle,
- false /* defaultToSystemApp */);
+ info = getApplicationInfoForIntent(
+ cameraIntent, userHandle, false /* defaultToSystemApp */);
}
// If the _SECURE intent doesn't resolve, try the original intent but request
// the system app for camera if there was more than one result.
if (info == null) {
cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA);
- info = getApplicationInfoForIntent(cameraIntent, userHandle,
- true /* defaultToSystemApp */);
+ info = getApplicationInfoForIntent(
+ cameraIntent, userHandle, true /* defaultToSystemApp */);
}
return info;
}
@@ -525,14 +531,14 @@ public final class PinnerService extends SystemService {
return getApplicationInfoForIntent(intent, userHandle, true);
}
- private ApplicationInfo getApplicationInfoForIntent(Intent intent, int userHandle,
- boolean defaultToSystemApp) {
+ private ApplicationInfo getApplicationInfoForIntent(
+ Intent intent, int userHandle, boolean defaultToSystemApp) {
if (intent == null) {
return null;
}
- ResolveInfo resolveInfo = mContext.getPackageManager().resolveActivityAsUser(intent,
- MATCH_FLAGS, userHandle);
+ ResolveInfo resolveInfo =
+ mContext.getPackageManager().resolveActivityAsUser(intent, MATCH_FLAGS, userHandle);
// If this intent can resolve to only one app, choose that one.
// Otherwise, if we've requested to default to the system app, return it;
@@ -547,12 +553,11 @@ public final class PinnerService extends SystemService {
}
if (defaultToSystemApp) {
- List<ResolveInfo> infoList = mContext.getPackageManager()
- .queryIntentActivitiesAsUser(intent, MATCH_FLAGS, userHandle);
+ List<ResolveInfo> infoList = mContext.getPackageManager().queryIntentActivitiesAsUser(
+ intent, MATCH_FLAGS, userHandle);
ApplicationInfo systemAppInfo = null;
for (ResolveInfo info : infoList) {
- if ((info.activityInfo.applicationInfo.flags
- & ApplicationInfo.FLAG_SYSTEM) != 0) {
+ if ((info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
if (systemAppInfo == null) {
systemAppInfo = info.activityInfo.applicationInfo;
} else {
@@ -568,13 +573,13 @@ public final class PinnerService extends SystemService {
}
private void sendPinAppsMessage(int userHandle) {
- mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::pinApps, this,
- userHandle));
+ mPinnerHandler.sendMessage(
+ PooledLambda.obtainMessage(PinnerService::pinApps, this, userHandle));
}
private void sendPinAppsWithUpdatedKeysMessage(int userHandle) {
- mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::pinAppsWithUpdatedKeys,
- this, userHandle));
+ mPinnerHandler.sendMessage(PooledLambda.obtainMessage(
+ PinnerService::pinAppsWithUpdatedKeys, this, userHandle));
}
private void sendUnpinAppsMessage() {
mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::unpinApps, this));
@@ -586,8 +591,7 @@ public final class PinnerService extends SystemService {
// phenotype property is not set.
boolean shouldPinCamera = mConfiguredToPinCamera
&& mDeviceConfigInterface.getBoolean(DeviceConfig.NAMESPACE_RUNTIME_NATIVE_BOOT,
- "pin_camera",
- SystemProperties.getBoolean("pinner.pin_camera", true));
+ "pin_camera", SystemProperties.getBoolean("pinner.pin_camera", true));
if (shouldPinCamera) {
pinKeys.add(KEY_CAMERA);
} else if (DEBUG) {
@@ -626,8 +630,9 @@ public final class PinnerService extends SystemService {
synchronized (this) {
// This code path demands preceding unpinApps() call.
if (!mPinnedApps.isEmpty()) {
- Slog.e(TAG, "Attempted to update a list of apps, "
- + "but apps were already pinned. Skipping.");
+ Slog.e(TAG,
+ "Attempted to update a list of apps, "
+ + "but apps were already pinned. Skipping.");
return;
}
@@ -646,8 +651,8 @@ public final class PinnerService extends SystemService {
* @see #pinApp(int, int, boolean)
*/
private void sendPinAppMessage(int key, int userHandle, boolean force) {
- mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::pinApp, this,
- key, userHandle, force));
+ mPinnerHandler.sendMessage(
+ PooledLambda.obtainMessage(PinnerService::pinApp, this, key, userHandle, force));
}
/**
@@ -667,10 +672,10 @@ public final class PinnerService extends SystemService {
}
return;
}
- unpinApp(key);
ApplicationInfo info = getInfoForKey(key, userHandle);
+ unpinApp(key);
if (info != null) {
- pinApp(key, info);
+ pinAppInternal(key, info);
}
}
@@ -682,9 +687,7 @@ public final class PinnerService extends SystemService {
private int getUidForKey(@AppKey int key) {
synchronized (this) {
PinnedApp existing = mPinnedApps.get(key);
- return existing != null && existing.active
- ? existing.uid
- : -1;
+ return existing != null && existing.active ? existing.uid : -1;
}
}
@@ -727,11 +730,8 @@ public final class PinnerService extends SystemService {
* Handle any changes in the anon region pinner config.
*/
private void refreshPinAnonConfig() {
- long newPinAnonSize =
- mDeviceConfigInterface.getLong(
- DEVICE_CONFIG_NAMESPACE_ANON_SIZE,
- DEVICE_CONFIG_KEY_ANON_SIZE,
- DEFAULT_ANON_SIZE);
+ long newPinAnonSize = mDeviceConfigInterface.getLong(
+ DEVICE_CONFIG_NAMESPACE_ANON_SIZE, DEVICE_CONFIG_KEY_ANON_SIZE, DEFAULT_ANON_SIZE);
newPinAnonSize = Math.max(0, Math.min(newPinAnonSize, MAX_ANON_SIZE));
if (newPinAnonSize != mPinAnonSize) {
mPinAnonSize = newPinAnonSize;
@@ -765,10 +765,9 @@ public final class PinnerService extends SystemService {
try {
// Map as SHARED to avoid changing rss.anon for system_server (per /proc/*/status).
// The mapping is visible in other rss metrics, and as private dirty in smaps/meminfo.
- address = Os.mmap(0, alignedPinSize,
- OsConstants.PROT_READ | OsConstants.PROT_WRITE,
- OsConstants.MAP_SHARED | OsConstants.MAP_ANONYMOUS,
- new FileDescriptor(), /*offset=*/0);
+ address = Os.mmap(0, alignedPinSize, OsConstants.PROT_READ | OsConstants.PROT_WRITE,
+ OsConstants.MAP_SHARED | OsConstants.MAP_ANONYMOUS, new FileDescriptor(),
+ /*offset=*/0);
Unsafe tempUnsafe = null;
Class<sun.misc.Unsafe> clazz = sun.misc.Unsafe.class;
@@ -794,14 +793,14 @@ public final class PinnerService extends SystemService {
return;
} finally {
if (address >= 0) {
- safeMunmap(address, alignedPinSize);
+ PinnerUtils.safeMunmap(address, alignedPinSize);
}
}
}
private void unpinAnonRegion() {
if (mPinAnonAddress != 0) {
- safeMunmap(mPinAnonAddress, mCurrentlyPinnedAnonSize);
+ PinnerUtils.safeMunmap(mPinAnonAddress, mCurrentlyPinnedAnonSize);
}
mPinAnonAddress = 0;
mCurrentlyPinnedAnonSize = 0;
@@ -824,12 +823,20 @@ public final class PinnerService extends SystemService {
}
/**
+ * Retrieves remaining quota for pinner service, once it reaches 0 it will no longer
+ * pin any file.
+ */
+ private long getAvailableGlobalQuota() {
+ return mConfiguredMaxPinnedMemory - mCurrentPinnedMemory;
+ }
+
+ /**
* Pins an application.
*
* @param key The key of the app to pin.
* @param appInfo The corresponding app info.
*/
- private void pinApp(@AppKey int key, @Nullable ApplicationInfo appInfo) {
+ private void pinAppInternal(@AppKey int key, @Nullable ApplicationInfo appInfo) {
if (appInfo == null) {
return;
}
@@ -839,7 +846,6 @@ public final class PinnerService extends SystemService {
mPinnedApps.put(key, pinnedApp);
}
-
// pin APK
final int pinSizeLimit = getSizeLimitForKey(key);
List<String> apks = new ArrayList<>();
@@ -851,36 +857,31 @@ public final class PinnerService extends SystemService {
}
}
- int apkPinSizeLimit = pinSizeLimit;
-
- boolean shouldSkipArtPins = key == KEY_HOME && skipHomeArtPins();
+ long apkPinSizeLimit = pinSizeLimit;
- for (String apk: apks) {
+ for (String apk : apks) {
if (apkPinSizeLimit <= 0) {
Slog.w(TAG, "Reached to the pin size limit. Skipping: " + apk);
// Continue instead of break to print all skipped APK names.
continue;
}
- PinnedFile pf = mInjector.pinFileInternal(apk, apkPinSizeLimit, /*attemptPinIntrospection=*/true);
+ String pinGroup = getNameForKey(key);
+ boolean shouldPinDeps = apk.equals(appInfo.sourceDir);
+ PinnedFile pf = pinFile(apk, apkPinSizeLimit, appInfo, pinGroup, shouldPinDeps);
if (pf == null) {
Slog.e(TAG, "Failed to pin " + apk);
continue;
}
- pf.groupName = getNameForKey(key);
if (DEBUG) {
Slog.i(TAG, "Pinned " + pf.fileName);
}
synchronized (this) {
pinnedApp.mFiles.add(pf);
- mPinnedFiles.put(pf.fileName, pf);
}
apkPinSizeLimit -= pf.bytesPinned;
- if (apk.equals(appInfo.sourceDir) && !shouldSkipArtPins) {
- pinOptimizedDexDependencies(pf, Integer.MAX_VALUE, appInfo);
- }
}
}
@@ -892,19 +893,23 @@ public final class PinnerService extends SystemService {
* that related to the file but not within itself.
*
* @param fileToPin File to pin
- * @param maxBytesToPin maximum quota allowed for pinning
- * @return total bytes that were pinned.
+ * @param bytesRequestedToPin maximum bytes requested to pin for {@code fileToPin}.
+ * @param pinOptimizedDeps whether optimized dependencies such as odex,vdex, etc be pinned.
+ * Note: {@code bytesRequestedToPin} limit will not apply to optimized
+ * dependencies pinned, only global quotas will apply instead.
+ * @return pinned file
*/
- public int pinFile(String fileToPin, int maxBytesToPin, @Nullable ApplicationInfo appInfo,
- @Nullable String groupName) {
+ public PinnedFile pinFile(String fileToPin, long bytesRequestedToPin,
+ @Nullable ApplicationInfo appInfo, @Nullable String groupName,
+ boolean pinOptimizedDeps) {
PinnedFile existingPin;
- synchronized(this) {
+ synchronized (this) {
existingPin = mPinnedFiles.get(fileToPin);
}
if (existingPin != null) {
- if (existingPin.bytesPinned == maxBytesToPin) {
+ if (existingPin.bytesPinned == bytesRequestedToPin) {
// Duplicate pin requesting same amount of bytes, lets just bail out.
- return 0;
+ return null;
} else {
// User decided to pin a different amount of bytes than currently pinned
// so this is a valid pin request. Unpin the previous version before repining.
@@ -915,26 +920,38 @@ public final class PinnerService extends SystemService {
}
}
+ long remainingQuota = getAvailableGlobalQuota();
+
+ if (pinGlobalQuota()) {
+ if (remainingQuota <= 0) {
+ Slog.w(TAG, "Reached pin quota, skipping file: " + fileToPin);
+ return null;
+ }
+ bytesRequestedToPin = Math.min(bytesRequestedToPin, remainingQuota);
+ }
+
boolean isApk = fileToPin.endsWith(".apk");
- int bytesPinned = 0;
- PinnedFile pf = mInjector.pinFileInternal(fileToPin, maxBytesToPin,
+
+ PinnedFile pf = mInjector.pinFileInternal(this, fileToPin, bytesRequestedToPin,
/*attemptPinIntrospection=*/isApk);
if (pf == null) {
Slog.e(TAG, "Failed to pin file = " + fileToPin);
- return 0;
+ return null;
}
pf.groupName = groupName != null ? groupName : "";
- bytesPinned += pf.bytesPinned;
- maxBytesToPin -= bytesPinned;
+ mCurrentPinnedMemory += pf.bytesPinned;
synchronized (this) {
mPinnedFiles.put(pf.fileName, pf);
}
- if (maxBytesToPin > 0) {
- pinOptimizedDexDependencies(pf, maxBytesToPin, appInfo);
+
+ if (pinOptimizedDeps) {
+ mCurrentPinnedMemory +=
+ pinOptimizedDexDependencies(pf, getAvailableGlobalQuota(), appInfo);
}
- return bytesPinned;
+
+ return pf;
}
/**
@@ -945,13 +962,13 @@ public final class PinnerService extends SystemService {
* to null it will use the default supported ABI by the device.
* @return total bytes pinned.
*/
- private int pinOptimizedDexDependencies(
- PinnedFile pinnedFile, int maxBytesToPin, @Nullable ApplicationInfo appInfo) {
+ private long pinOptimizedDexDependencies(
+ PinnedFile pinnedFile, long maxBytesToPin, @Nullable ApplicationInfo appInfo) {
if (pinnedFile == null) {
return 0;
}
- int bytesPinned = 0;
+ long bytesPinned = 0;
if (pinnedFile.fileName.endsWith(".jar") | pinnedFile.fileName.endsWith(".apk")) {
String abi = null;
if (appInfo != null) {
@@ -974,7 +991,7 @@ public final class PinnerService extends SystemService {
// Unpin if it was already pinned prior to re-pinning.
unpinFile(file);
- PinnedFile df = mInjector.pinFileInternal(file, maxBytesToPin,
+ PinnedFile df = mInjector.pinFileInternal(this, file, maxBytesToPin,
/*attemptPinIntrospection=*/false);
if (df == null) {
Slog.i(TAG, "Failed to pin ART file = " + file);
@@ -992,7 +1009,8 @@ public final class PinnerService extends SystemService {
return bytesPinned;
}
- /** mlock length bytes of fileToPin in memory
+ /**
+ * mlock length bytes of fileToPin in memory
*
* If attemptPinIntrospection is true, then treat the file to pin as a zip file and
* look for a "pinlist.meta" file in the archive root directory. The structure of this
@@ -1029,8 +1047,8 @@ public final class PinnerService extends SystemService {
* zip in order to extract the
* @return Pinned memory resource owner thing or null on error
*/
- private static PinnedFile pinFileInternal(
- String fileToPin, int maxBytesToPin, boolean attemptPinIntrospection) {
+ private PinnedFile pinFileInternal(
+ String fileToPin, long maxBytesToPin, boolean attemptPinIntrospection) {
if (DEBUG) {
Slog.d(TAG, "pin file: " + fileToPin + " use-pinlist: " + attemptPinIntrospection);
}
@@ -1054,8 +1072,8 @@ public final class PinnerService extends SystemService {
}
return pinnedFile;
} finally {
- safeClose(pinRangeStream);
- safeClose(fileAsZip); // Also closes any streams we've opened
+ PinnerUtils.safeClose(pinRangeStream);
+ PinnerUtils.safeClose(fileAsZip); // Also closes any streams we've opened
}
}
@@ -1068,11 +1086,8 @@ public final class PinnerService extends SystemService {
try {
zip = new ZipFile(fileName);
} catch (IOException ex) {
- Slog.w(TAG,
- String.format(
- "could not open \"%s\" as zip: pinning as blob",
- fileName),
- ex);
+ Slog.w(TAG, String.format("could not open \"%s\" as zip: pinning as blob", fileName),
+ ex);
}
return zip;
}
@@ -1112,9 +1127,9 @@ public final class PinnerService extends SystemService {
pinMetaStream = zipFile.getInputStream(pinMetaEntry);
} catch (IOException ex) {
Slog.w(TAG,
- String.format("error reading pin metadata \"%s\": pinning as blob",
- fileName),
- ex);
+ String.format(
+ "error reading pin metadata \"%s\": pinning as blob", fileName),
+ ex);
}
} else {
Slog.w(TAG,
@@ -1124,57 +1139,6 @@ public final class PinnerService extends SystemService {
return pinMetaStream;
}
- private static abstract class PinRangeSource {
- /** Retrive a range to pin.
- *
- * @param outPinRange Receives the pin region
- * @return True if we filled in outPinRange or false if we're out of pin entries
- */
- abstract boolean read(PinRange outPinRange);
- }
-
- private static final class PinRangeSourceStatic extends PinRangeSource {
- private final int mPinStart;
- private final int mPinLength;
- private boolean mDone = false;
-
- PinRangeSourceStatic(int pinStart, int pinLength) {
- mPinStart = pinStart;
- mPinLength = pinLength;
- }
-
- @Override
- boolean read(PinRange outPinRange) {
- outPinRange.start = mPinStart;
- outPinRange.length = mPinLength;
- boolean done = mDone;
- mDone = true;
- return !done;
- }
- }
-
- private static final class PinRangeSourceStream extends PinRangeSource {
- private final DataInputStream mStream;
- private boolean mDone = false;
-
- PinRangeSourceStream(InputStream stream) {
- mStream = new DataInputStream(stream);
- }
-
- @Override
- boolean read(PinRange outPinRange) {
- if (!mDone) {
- try {
- outPinRange.start = mStream.readInt();
- outPinRange.length = mStream.readInt();
- } catch (IOException ex) {
- mDone = true;
- }
- }
- return !mDone;
- }
- }
-
/**
* Helper for pinFile.
*
@@ -1185,25 +1149,20 @@ public final class PinnerService extends SystemService {
* @return PinnedFile or null on error
*/
private static PinnedFile pinFileRanges(
- String fileToPin,
- int maxBytesToPin,
- PinRangeSource pinRangeSource)
- {
+ String fileToPin, long maxBytesToPin, PinRangeSource pinRangeSource) {
FileDescriptor fd = new FileDescriptor();
long address = -1;
- int mapSize = 0;
+ long mapSize = 0;
try {
int openFlags = (OsConstants.O_RDONLY | OsConstants.O_CLOEXEC);
fd = Os.open(fileToPin, openFlags, 0);
mapSize = (int) Math.min(Os.fstat(fd).st_size, Integer.MAX_VALUE);
- address = Os.mmap(0, mapSize,
- OsConstants.PROT_READ,
- OsConstants.MAP_SHARED,
- fd, /*offset=*/0);
+ address = Os.mmap(
+ 0, mapSize, OsConstants.PROT_READ, OsConstants.MAP_SHARED, fd, /*offset=*/0);
PinRange pinRange = new PinRange();
- int bytesPinned = 0;
+ long bytesPinned = 0;
// We pin at page granularity, so make sure the limit is page-aligned
if (maxBytesToPin % PAGE_SIZE != 0) {
@@ -1211,10 +1170,10 @@ public final class PinnerService extends SystemService {
}
while (bytesPinned < maxBytesToPin && pinRangeSource.read(pinRange)) {
- int pinStart = pinRange.start;
- int pinLength = pinRange.length;
- pinStart = clamp(0, pinStart, mapSize);
- pinLength = clamp(0, pinLength, mapSize - pinStart);
+ long pinStart = pinRange.start;
+ long pinLength = pinRange.length;
+ pinStart = PinnerUtils.clamp(0, pinStart, mapSize);
+ pinLength = PinnerUtils.clamp(0, pinLength, mapSize - pinStart);
pinLength = Math.min(maxBytesToPin - bytesPinned, pinLength);
// mlock doesn't require the region to be page-aligned, but we snap the
@@ -1229,14 +1188,13 @@ public final class PinnerService extends SystemService {
if (pinLength % PAGE_SIZE != 0) {
pinLength += PAGE_SIZE - pinLength % PAGE_SIZE;
}
- pinLength = clamp(0, pinLength, maxBytesToPin - bytesPinned);
+ pinLength = PinnerUtils.clamp(0, pinLength, maxBytesToPin - bytesPinned);
if (pinLength > 0) {
if (DEBUG) {
Slog.d(TAG,
- String.format(
- "pinning at %s %s bytes of %s",
- pinStart, pinLength, fileToPin));
+ String.format("pinning at %s %s bytes of %s", pinStart, pinLength,
+ fileToPin));
}
Os.mlock(address + pinStart, pinLength);
}
@@ -1244,15 +1202,15 @@ public final class PinnerService extends SystemService {
}
PinnedFile pinnedFile = new PinnedFile(address, mapSize, fileToPin, bytesPinned);
- address = -1; // Ownership transferred
+ address = -1; // Ownership transferred
return pinnedFile;
} catch (ErrnoException ex) {
Slog.e(TAG, "Could not pin file " + fileToPin, ex);
return null;
} finally {
- safeClose(fd);
+ PinnerUtils.safeClose(fd);
if (address >= 0) {
- safeMunmap(address, mapSize);
+ PinnerUtils.safeMunmap(address, mapSize);
}
}
}
@@ -1273,81 +1231,50 @@ public final class PinnerService extends SystemService {
}
}
- public void unpinFile(String filename) {
+ /**
+ * Unpin a file and its optimized dependencies.
+ *
+ * @param filename file to unpin.
+ * @return number of bytes unpinned, 0 in case of failure or nothing to unpin.
+ */
+ public long unpinFile(String filename) {
PinnedFile pinnedFile;
synchronized (this) {
pinnedFile = mPinnedFiles.get(filename);
}
if (pinnedFile == null) {
// File not pinned, nothing to do.
- return;
+ return 0;
}
+ long unpinnedBytes = pinnedFile.bytesPinned;
pinnedFile.close();
synchronized (this) {
if (DEBUG) {
Slog.d(TAG, "Unpinned file: " + filename);
}
+ mCurrentPinnedMemory -= pinnedFile.bytesPinned;
+
mPinnedFiles.remove(pinnedFile.fileName);
for (PinnedFile dep : pinnedFile.pinnedDeps) {
if (dep == null) {
continue;
}
+ unpinnedBytes -= dep.bytesPinned;
+ mCurrentPinnedMemory -= dep.bytesPinned;
mPinnedFiles.remove(dep.fileName);
if (DEBUG) {
Slog.d(TAG, "Unpinned dependency: " + dep.fileName);
}
}
}
- }
- private static int clamp(int min, int value, int max) {
- return Math.max(min, Math.min(value, max));
- }
-
- private static void safeMunmap(long address, long mapSize) {
- try {
- Os.munmap(address, mapSize);
- } catch (ErrnoException ex) {
- Slog.w(TAG, "ignoring error in unmap", ex);
- }
- }
-
- /**
- * Close FD, swallowing irrelevant errors.
- */
- private static void safeClose(@Nullable FileDescriptor fd) {
- if (fd != null && fd.valid()) {
- try {
- Os.close(fd);
- } catch (ErrnoException ex) {
- // Swallow the exception: non-EBADF errors in close(2)
- // indicate deferred paging write errors, which we
- // don't care about here. The underlying file
- // descriptor is always closed.
- if (ex.errno == OsConstants.EBADF) {
- throw new AssertionError(ex);
- }
- }
- }
- }
-
- /**
- * Close closeable thing, swallowing errors.
- */
- private static void safeClose(@Nullable Closeable thing) {
- if (thing != null) {
- try {
- thing.close();
- } catch (IOException ex) {
- Slog.w(TAG, "ignoring error closing resource: " + thing, ex);
- }
- }
+ return unpinnedBytes;
}
public List<PinnedFileStat> getPinnerStats() {
ArrayList<PinnedFileStat> stats = new ArrayList<>();
Collection<PinnedFile> pinnedFiles;
- synchronized(this) {
+ synchronized (this) {
pinnedFiles = mPinnedFiles.values();
}
for (PinnedFile pf : pinnedFiles) {
@@ -1355,8 +1282,8 @@ public final class PinnerService extends SystemService {
stats.add(stat);
}
if (mCurrentlyPinnedAnonSize > 0) {
- stats.add(new PinnedFileStat(ANON_REGION_STAT_NAME,
- mCurrentlyPinnedAnonSize, ANON_REGION_STAT_NAME));
+ stats.add(new PinnedFileStat(
+ ANON_REGION_STAT_NAME, mCurrentlyPinnedAnonSize, ANON_REGION_STAT_NAME));
}
return stats;
}
@@ -1364,71 +1291,124 @@ public final class PinnerService extends SystemService {
public final class BinderService extends IPinnerService.Stub {
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
- if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+ if (!DumpUtils.checkDumpPermission(mContext, TAG, pw))
+ return;
HashSet<PinnedFile> shownPins = new HashSet<>();
- HashSet<String> groups = new HashSet<>();
- final int bytesPerMB = 1024 * 1024;
+ HashSet<String> shownGroups = new HashSet<>();
+ HashSet<String> groupsToPrint = new HashSet<>();
+ final double bytesPerMB = 1024 * 1024;
+ pw.format("Pinner Configs:\n");
+ pw.format(" Total Pinner quota: %d%% of total device memory\n",
+ mConfiguredMaxPinnedMemoryPercentage);
+ pw.format(" Maximum Pinner quota: %d bytes (%.2f MB)\n", mConfiguredMaxPinnedMemory,
+ mConfiguredMaxPinnedMemory / bytesPerMB);
+ pw.format(" Max Home App Pin Bytes (without deps): %d\n", mConfiguredHomePinBytes);
+ pw.format("\nPinned Files:\n");
synchronized (PinnerService.this) {
long totalSize = 0;
+
+ // We print apps separately from regular pins as they contain extra information that
+ // other pins do not.
for (int key : mPinnedApps.keySet()) {
PinnedApp app = mPinnedApps.get(key);
pw.print(getNameForKey(key));
- pw.print(" uid="); pw.print(app.uid);
- pw.print(" active="); pw.print(app.active);
+ pw.print(" uid=");
+ pw.print(app.uid);
+ pw.print(" active=");
+ pw.print(app.active);
+
+ if (!app.mFiles.isEmpty()) {
+ shownGroups.add(app.mFiles.getFirst().groupName);
+ }
pw.println();
+ long bytesPinnedForApp = 0;
+ long bytesPinnedForAppDeps = 0;
for (PinnedFile pf : mPinnedApps.get(key).mFiles) {
pw.print(" ");
- pw.format("%s pinned:%d bytes (%d MB) pinlist:%b\n", pf.fileName,
+ pw.format("%s pinned:%d bytes (%.2f MB) pinlist:%b\n", pf.fileName,
pf.bytesPinned, pf.bytesPinned / bytesPerMB, pf.used_pinlist);
totalSize += pf.bytesPinned;
+ bytesPinnedForApp += pf.bytesPinned;
shownPins.add(pf);
for (PinnedFile dep : pf.pinnedDeps) {
pw.print(" ");
- pw.format("%s pinned:%d bytes (%d MB) pinlist:%b (Dependency)\n", dep.fileName,
- dep.bytesPinned, dep.bytesPinned / bytesPerMB, dep.used_pinlist);
+ pw.format("%s pinned:%d bytes (%.2f MB) pinlist:%b (Dependency)\n",
+ dep.fileName, dep.bytesPinned, dep.bytesPinned / bytesPerMB,
+ dep.used_pinlist);
totalSize += dep.bytesPinned;
+ bytesPinnedForAppDeps += dep.bytesPinned;
shownPins.add(dep);
}
}
+ long bytesPinnedForAppAndDeps = bytesPinnedForApp + bytesPinnedForAppDeps;
+ pw.format("Total Pinned = %d (%.2f MB) [App=%d (%.2f MB), "
+ + "Dependencies=%d (%.2f MB)]\n\n",
+ bytesPinnedForAppAndDeps, bytesPinnedForAppAndDeps / bytesPerMB,
+ bytesPinnedForApp, bytesPinnedForApp / bytesPerMB,
+ bytesPinnedForAppDeps, bytesPinnedForAppDeps / bytesPerMB);
}
pw.println();
for (PinnedFile pinnedFile : mPinnedFiles.values()) {
- if (!groups.contains(pinnedFile.groupName)) {
- groups.add(pinnedFile.groupName);
+ if (!groupsToPrint.contains(pinnedFile.groupName)
+ && !shownGroups.contains(pinnedFile.groupName)) {
+ groupsToPrint.add(pinnedFile.groupName);
}
}
- boolean firstPinInGroup = true;
- for (String group : groups) {
+
+ // Print all the non app groups.
+ for (String group : groupsToPrint) {
List<PinnedFile> groupPins = getAllPinsForGroup(group);
+ pw.print("\nGroup:" + group);
+ long bytesPinnedForGroupNoDeps = 0;
+ long bytesPinnedForGroupDeps = 0;
+ pw.println();
for (PinnedFile pinnedFile : groupPins) {
if (shownPins.contains(pinnedFile)) {
- // Already showed in the dump and accounted for, skip.
+ // Already displayed and accounted for, skip.
continue;
}
- if (firstPinInGroup) {
- firstPinInGroup = false;
- // Ensure we only print when there are pins for groups not yet shown
- // in the pinned app section.
- pw.print("Group:" + group);
- pw.println();
- }
- pw.format(" %s pinned:%d bytes (%d MB) pinlist:%b\n", pinnedFile.fileName,
- pinnedFile.bytesPinned, pinnedFile.bytesPinned / bytesPerMB,
- pinnedFile.used_pinlist);
+ pw.format(" %s pinned: %d bytes (%.2f MB) pinlist:%b\n",
+ pinnedFile.fileName, pinnedFile.bytesPinned,
+ pinnedFile.bytesPinned / bytesPerMB, pinnedFile.used_pinlist);
totalSize += pinnedFile.bytesPinned;
+ bytesPinnedForGroupNoDeps += pinnedFile.bytesPinned;
+ shownPins.add(pinnedFile);
+ for (PinnedFile dep : pinnedFile.pinnedDeps) {
+ if (shownPins.contains(dep)) {
+ // Already displayed and accounted for, skip.
+ continue;
+ }
+ pw.print(" ");
+ pw.format("%s pinned:%d bytes (%.2f MB) pinlist:%b (Dependency)\n",
+ dep.fileName, dep.bytesPinned, dep.bytesPinned / bytesPerMB,
+ dep.used_pinlist);
+ totalSize += dep.bytesPinned;
+ bytesPinnedForGroupDeps += dep.bytesPinned;
+ shownPins.add(dep);
+ }
}
+ long bytesPinnedForGroup = bytesPinnedForGroupNoDeps + bytesPinnedForGroupDeps;
+ pw.format("Total Pinned = %d (%.2f MB) [Main=%d (%.2f MB), "
+ + "Dependencies=%d (%.2f MB)]\n\n",
+ bytesPinnedForGroup, bytesPinnedForGroup / bytesPerMB,
+ bytesPinnedForGroupNoDeps, bytesPinnedForGroupNoDeps / bytesPerMB,
+ bytesPinnedForGroupDeps, bytesPinnedForGroupDeps / bytesPerMB);
}
pw.println();
if (mPinAnonAddress != 0) {
- pw.format("Pinned anon region: %d (%d MB)\n", mCurrentlyPinnedAnonSize, mCurrentlyPinnedAnonSize / bytesPerMB);
+ pw.format("Pinned anon region: %d (%.2f MB)\n", mCurrentlyPinnedAnonSize,
+ mCurrentlyPinnedAnonSize / bytesPerMB);
totalSize += mCurrentlyPinnedAnonSize;
}
- pw.format("Total pinned: %s bytes (%s MB)\n", totalSize, totalSize / bytesPerMB);
+ pw.format("Total pinned: %d bytes (%.2f MB)\n", totalSize, totalSize / bytesPerMB);
+ pw.format("Available Pinner quota: %d bytes (%.2f MB)\n", getAvailableGlobalQuota(),
+ getAvailableGlobalQuota() / bytesPerMB);
pw.println();
if (!mPendingRepin.isEmpty()) {
pw.print("Pending repin: ");
for (int key : mPendingRepin.values()) {
- pw.print(getNameForKey(key)); pw.print(' ');
+ pw.print(getNameForKey(key));
+ pw.print(' ');
}
pw.println();
}
@@ -1462,8 +1442,9 @@ public final class PinnerService extends SystemService {
repin();
break;
default:
- printError(out, String.format(
- "Unknown pinner command: %s. Supported commands: repin", command));
+ printError(out,
+ String.format("Unknown pinner command: %s. Supported commands: repin",
+ command));
resultReceiver.send(-1, null);
return;
}
@@ -1479,46 +1460,6 @@ public final class PinnerService extends SystemService {
}
}
- @VisibleForTesting
- public static final class PinnedFile implements AutoCloseable {
- private long mAddress;
- final int mapSize;
- final String fileName;
- final int bytesPinned;
-
- // Whether this file was pinned using a pinlist
- boolean used_pinlist;
-
- // User defined group name for pinner accounting
- String groupName = "";
- ArrayList<PinnedFile> pinnedDeps = new ArrayList<>();
-
- PinnedFile(long address, int mapSize, String fileName, int bytesPinned) {
- mAddress = address;
- this.mapSize = mapSize;
- this.fileName = fileName;
- this.bytesPinned = bytesPinned;
- }
-
- @Override
- public void close() {
- if (mAddress >= 0) {
- safeMunmap(mAddress, mapSize);
- mAddress = -1;
- }
- for (PinnedFile dep : pinnedDeps) {
- if (dep != null) {
- dep.close();
- }
- }
- }
-
- @Override
- public void finalize() {
- close();
- }
- }
-
final static class PinRange {
int start;
int length;
@@ -1528,7 +1469,6 @@ public final class PinnerService extends SystemService {
* Represents an app that was pinned.
*/
private final class PinnedApp {
-
/**
* The uid of the package being pinned. This stays constant while the package stays
* installed.
@@ -1557,11 +1497,9 @@ public final class PinnerService extends SystemService {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
- case PIN_ONSTART_MSG:
- {
+ case PIN_ONSTART_MSG: {
handlePinOnStart();
- }
- break;
+ } break;
default:
super.handleMessage(msg);
diff --git a/services/core/java/com/android/server/pinner/PinnerUtils.java b/services/core/java/com/android/server/pinner/PinnerUtils.java
new file mode 100644
index 000000000000..a836a83dedab
--- /dev/null
+++ b/services/core/java/com/android/server/pinner/PinnerUtils.java
@@ -0,0 +1,75 @@
+/*
+ * 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.pinner;
+
+import android.annotation.Nullable;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Slog;
+
+import java.io.Closeable;
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/* package */ final class PinnerUtils {
+ private static final String TAG = "PinnerUtils";
+
+ public static long clamp(long min, long value, long max) {
+ return Math.max(min, Math.min(value, max));
+ }
+
+ public static void safeMunmap(long address, long mapSize) {
+ try {
+ Os.munmap(address, mapSize);
+ } catch (ErrnoException ex) {
+ Slog.w(TAG, "ignoring error in unmap", ex);
+ }
+ }
+
+ /**
+ * Close FD, swallowing irrelevant errors.
+ */
+ public static void safeClose(@Nullable FileDescriptor fd) {
+ if (fd != null && fd.valid()) {
+ try {
+ Os.close(fd);
+ } catch (ErrnoException ex) {
+ // Swallow the exception: non-EBADF errors in close(2)
+ // indicate deferred paging write errors, which we
+ // don't care about here. The underlying file
+ // descriptor is always closed.
+ if (ex.errno == OsConstants.EBADF) {
+ throw new AssertionError(ex);
+ }
+ }
+ }
+ }
+
+ /**
+ * Close closeable thing, swallowing errors.
+ */
+ public static void safeClose(@Nullable Closeable thing) {
+ if (thing != null) {
+ try {
+ thing.close();
+ } catch (IOException ex) {
+ Slog.w(TAG, "ignoring error closing resource: " + thing, ex);
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index 9ecc7b9a805d..1569fa0aa8d7 100644
--- a/services/core/java/com/android/server/pm/DexOptHelper.java
+++ b/services/core/java/com/android/server/pm/DexOptHelper.java
@@ -70,13 +70,13 @@ import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.LocalManagerRegistry;
import com.android.server.LocalServices;
-import com.android.server.PinnerService;
import com.android.server.art.ArtManagerLocal;
import com.android.server.art.DexUseManagerLocal;
import com.android.server.art.ReasonMapping;
import com.android.server.art.model.ArtFlags;
import com.android.server.art.model.DexoptParams;
import com.android.server.art.model.DexoptResult;
+import com.android.server.pinner.PinnerService;
import com.android.server.pm.PackageDexOptimizer.DexOptResult;
import com.android.server.pm.dex.DexManager;
import com.android.server.pm.dex.DexoptOptions;
diff --git a/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java b/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java
index f047f564538d..ab630eef4644 100644
--- a/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java
+++ b/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java
@@ -17,6 +17,7 @@
package com.android.server.power.stats.wakeups;
import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_ALARM;
+import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_BLUETOOTH;
import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA;
import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SENSOR;
import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER;
@@ -63,6 +64,7 @@ public class CpuWakeupStats {
private static final String SUBSYSTEM_SOUND_TRIGGER_STRING = "Sound_trigger";
private static final String SUBSYSTEM_SENSOR_STRING = "Sensor";
private static final String SUBSYSTEM_CELLULAR_DATA_STRING = "Cellular_data";
+ private static final String SUBSYSTEM_BLUETOOTH_STRING = "Bluetooth";
private static final String TRACE_TRACK_WAKEUP_ATTRIBUTION = "wakeup_attribution";
private static final long WAKEUP_WRITE_DELAY_MS = TimeUnit.SECONDS.toMillis(30);
@@ -512,6 +514,8 @@ public class CpuWakeupStats {
return CPU_WAKEUP_SUBSYSTEM_SENSOR;
case SUBSYSTEM_CELLULAR_DATA_STRING:
return CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA;
+ case SUBSYSTEM_BLUETOOTH_STRING:
+ return CPU_WAKEUP_SUBSYSTEM_BLUETOOTH;
}
return CPU_WAKEUP_SUBSYSTEM_UNKNOWN;
}
@@ -528,6 +532,8 @@ public class CpuWakeupStats {
return SUBSYSTEM_SENSOR_STRING;
case CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA:
return SUBSYSTEM_CELLULAR_DATA_STRING;
+ case CPU_WAKEUP_SUBSYSTEM_BLUETOOTH:
+ return SUBSYSTEM_BLUETOOTH_STRING;
case CPU_WAKEUP_SUBSYSTEM_UNKNOWN:
return "Unknown";
}
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index b35a0a772ff2..74c1124e1f16 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -216,13 +216,13 @@ import com.android.role.RoleManagerLocal;
import com.android.server.BinderCallsStatsService;
import com.android.server.LocalManagerRegistry;
import com.android.server.LocalServices;
-import com.android.server.PinnerService;
-import com.android.server.PinnerService.PinnedFileStats;
import com.android.server.SystemService;
import com.android.server.SystemServiceManager;
import com.android.server.am.MemoryStatUtil.MemoryStat;
import com.android.server.health.HealthServiceWrapper;
import com.android.server.notification.NotificationManagerService;
+import com.android.server.pinner.PinnerService;
+import com.android.server.pinner.PinnerService.PinnedFileStats;
import com.android.server.pm.UserManagerInternal;
import com.android.server.power.stats.KernelWakelockReader;
import com.android.server.power.stats.KernelWakelockStats;
diff --git a/services/core/java/com/android/server/vibrator/VibratorController.java b/services/core/java/com/android/server/vibrator/VibratorController.java
index c120fc7d82f5..6aed00e0e32b 100644
--- a/services/core/java/com/android/server/vibrator/VibratorController.java
+++ b/services/core/java/com/android/server/vibrator/VibratorController.java
@@ -57,8 +57,7 @@ final class VibratorController {
// for a snippet of the current known vibrator state/info.
private volatile VibratorInfo mVibratorInfo;
private volatile boolean mVibratorInfoLoadSuccessful;
- private volatile boolean mIsVibrating;
- private volatile boolean mIsUnderExternalControl;
+ private volatile VibratorState mCurrentState;
private volatile float mCurrentAmplitude;
/**
@@ -75,6 +74,11 @@ final class VibratorController {
void onComplete(int vibratorId, long vibrationId);
}
+ /** Representation of the vibrator state based on the interactions through this controller. */
+ private enum VibratorState {
+ IDLE, VIBRATING, UNDER_EXTERNAL_CONTROL
+ }
+
VibratorController(int vibratorId, OnVibrationCompleteListener listener) {
this(vibratorId, listener, new NativeWrapper());
}
@@ -87,6 +91,7 @@ final class VibratorController {
VibratorInfo.Builder vibratorInfoBuilder = new VibratorInfo.Builder(vibratorId);
mVibratorInfoLoadSuccessful = mNativeWrapper.getInfo(vibratorInfoBuilder);
mVibratorInfo = vibratorInfoBuilder.build();
+ mCurrentState = VibratorState.IDLE;
if (!mVibratorInfoLoadSuccessful) {
Slog.e(TAG,
@@ -106,7 +111,7 @@ final class VibratorController {
return false;
}
// Notify its callback after new client registered.
- notifyStateListener(listener, mIsVibrating);
+ notifyStateListener(listener, isVibrating(mCurrentState));
}
return true;
} finally {
@@ -166,7 +171,7 @@ final class VibratorController {
* automatically notified to any registered {@link IVibratorStateListener} on change.
*/
public boolean isVibrating() {
- return mIsVibrating;
+ return isVibrating(mCurrentState);
}
/**
@@ -184,11 +189,6 @@ final class VibratorController {
return mCurrentAmplitude;
}
- /** Return {@code true} if this vibrator is under external control, false otherwise. */
- public boolean isUnderExternalControl() {
- return mIsUnderExternalControl;
- }
-
/**
* Check against this vibrator capabilities.
*
@@ -214,7 +214,7 @@ final class VibratorController {
/**
* Set the vibrator control to be external or not, based on given flag.
*
- * <p>This will affect the state of {@link #isUnderExternalControl()}.
+ * <p>This will affect the state of {@link #isVibrating()}.
*/
public void setExternalControl(boolean externalControl) {
Trace.traceBegin(TRACE_TAG_VIBRATOR,
@@ -224,9 +224,11 @@ final class VibratorController {
if (!mVibratorInfo.hasCapability(IVibrator.CAP_EXTERNAL_CONTROL)) {
return;
}
+ VibratorState newState =
+ externalControl ? VibratorState.UNDER_EXTERNAL_CONTROL : VibratorState.IDLE;
synchronized (mLock) {
- mIsUnderExternalControl = externalControl;
mNativeWrapper.setExternalControl(externalControl);
+ updateStateAndNotifyListenersLocked(newState);
}
} finally {
Trace.traceEnd(TRACE_TAG_VIBRATOR);
@@ -264,7 +266,7 @@ final class VibratorController {
if (mVibratorInfo.hasCapability(IVibrator.CAP_AMPLITUDE_CONTROL)) {
mNativeWrapper.setAmplitude(amplitude);
}
- if (mIsVibrating) {
+ if (mCurrentState == VibratorState.VIBRATING) {
mCurrentAmplitude = amplitude;
}
}
@@ -289,7 +291,7 @@ final class VibratorController {
long duration = mNativeWrapper.on(milliseconds, vibrationId);
if (duration > 0) {
mCurrentAmplitude = -1;
- notifyListenerOnVibrating(true);
+ updateStateAndNotifyListenersLocked(VibratorState.VIBRATING);
}
return duration;
}
@@ -319,7 +321,7 @@ final class VibratorController {
vendorEffect.getAdaptiveScale(), vibrationId);
if (duration > 0) {
mCurrentAmplitude = -1;
- notifyListenerOnVibrating(true);
+ updateStateAndNotifyListenersLocked(VibratorState.VIBRATING);
}
return duration;
} finally {
@@ -346,7 +348,7 @@ final class VibratorController {
prebaked.getEffectStrength(), vibrationId);
if (duration > 0) {
mCurrentAmplitude = -1;
- notifyListenerOnVibrating(true);
+ updateStateAndNotifyListenersLocked(VibratorState.VIBRATING);
}
return duration;
}
@@ -374,7 +376,7 @@ final class VibratorController {
long duration = mNativeWrapper.compose(primitives, vibrationId);
if (duration > 0) {
mCurrentAmplitude = -1;
- notifyListenerOnVibrating(true);
+ updateStateAndNotifyListenersLocked(VibratorState.VIBRATING);
}
return duration;
}
@@ -402,7 +404,7 @@ final class VibratorController {
long duration = mNativeWrapper.composePwle(primitives, braking, vibrationId);
if (duration > 0) {
mCurrentAmplitude = -1;
- notifyListenerOnVibrating(true);
+ updateStateAndNotifyListenersLocked(VibratorState.VIBRATING);
}
return duration;
}
@@ -422,7 +424,7 @@ final class VibratorController {
synchronized (mLock) {
mNativeWrapper.off();
mCurrentAmplitude = 0;
- notifyListenerOnVibrating(false);
+ updateStateAndNotifyListenersLocked(VibratorState.IDLE);
}
} finally {
Trace.traceEnd(TRACE_TAG_VIBRATOR);
@@ -443,9 +445,8 @@ final class VibratorController {
return "VibratorController{"
+ "mVibratorInfo=" + mVibratorInfo
+ ", mVibratorInfoLoadSuccessful=" + mVibratorInfoLoadSuccessful
- + ", mIsVibrating=" + mIsVibrating
+ + ", mCurrentState=" + mCurrentState.name()
+ ", mCurrentAmplitude=" + mCurrentAmplitude
- + ", mIsUnderExternalControl=" + mIsUnderExternalControl
+ ", mVibratorStateListeners count="
+ mVibratorStateListeners.getRegisteredCallbackCount()
+ '}';
@@ -454,8 +455,7 @@ final class VibratorController {
void dump(IndentingPrintWriter pw) {
pw.println("Vibrator (id=" + mVibratorInfo.getId() + "):");
pw.increaseIndent();
- pw.println("isVibrating = " + mIsVibrating);
- pw.println("isUnderExternalControl = " + mIsUnderExternalControl);
+ pw.println("currentState = " + mCurrentState.name());
pw.println("currentAmplitude = " + mCurrentAmplitude);
pw.println("vibratorInfoLoadSuccessful = " + mVibratorInfoLoadSuccessful);
pw.println("vibratorStateListener size = "
@@ -464,14 +464,19 @@ final class VibratorController {
pw.decreaseIndent();
}
+ /**
+ * Updates current vibrator state and notify listeners if {@link #isVibrating()} result changed.
+ */
@GuardedBy("mLock")
- private void notifyListenerOnVibrating(boolean isVibrating) {
- if (mIsVibrating != isVibrating) {
- mIsVibrating = isVibrating;
+ private void updateStateAndNotifyListenersLocked(VibratorState state) {
+ boolean previousIsVibrating = isVibrating(mCurrentState);
+ final boolean newIsVibrating = isVibrating(state);
+ mCurrentState = state;
+ if (previousIsVibrating != newIsVibrating) {
// The broadcast method is safe w.r.t. register/unregister listener methods, but lock
// is required here to guarantee delivery order.
mVibratorStateListeners.broadcast(
- listener -> notifyStateListener(listener, isVibrating));
+ listener -> notifyStateListener(listener, newIsVibrating));
}
}
@@ -483,6 +488,11 @@ final class VibratorController {
}
}
+ /** Returns true only if given state is not {@link VibratorState#IDLE}. */
+ private static boolean isVibrating(VibratorState state) {
+ return state != VibratorState.IDLE;
+ }
+
/** Wrapper around the static-native methods of {@link VibratorController} for tests. */
@VisibleForTesting
public static class NativeWrapper {
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 95c648334327..07473d10b217 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -809,17 +809,9 @@ public class VibratorManagerService extends IVibratorManagerService.Stub {
mCurrentExternalVibration.getDebugInfo().dump(proto,
VibratorManagerServiceDumpProto.CURRENT_EXTERNAL_VIBRATION);
}
-
- boolean isVibrating = false;
- boolean isUnderExternalControl = false;
for (int i = 0; i < mVibrators.size(); i++) {
proto.write(VibratorManagerServiceDumpProto.VIBRATOR_IDS, mVibrators.keyAt(i));
- isVibrating |= mVibrators.valueAt(i).isVibrating();
- isUnderExternalControl |= mVibrators.valueAt(i).isUnderExternalControl();
}
- proto.write(VibratorManagerServiceDumpProto.IS_VIBRATING, isVibrating);
- proto.write(VibratorManagerServiceDumpProto.VIBRATOR_UNDER_EXTERNAL_CONTROL,
- isUnderExternalControl);
}
mVibratorManagerRecords.dump(proto);
mVibratorControlService.dump(proto);
diff --git a/services/core/java/com/android/server/webkit/SystemImpl.java b/services/core/java/com/android/server/webkit/SystemImpl.java
index 67401530763b..ab5316f46d78 100644
--- a/services/core/java/com/android/server/webkit/SystemImpl.java
+++ b/services/core/java/com/android/server/webkit/SystemImpl.java
@@ -41,7 +41,8 @@ import android.webkit.WebViewZygote;
import com.android.internal.util.XmlUtils;
import com.android.server.LocalServices;
-import com.android.server.PinnerService;
+import com.android.server.pinner.PinnedFile;
+import com.android.server.pinner.PinnerService;
import org.xmlpull.v1.XmlPullParserException;
@@ -318,8 +319,9 @@ public class SystemImpl implements SystemInterface {
if (webviewPinQuota <= 0) {
break;
}
- int bytesPinned = pinnerService.pinFile(apk, webviewPinQuota, appInfo, PIN_GROUP);
- webviewPinQuota -= bytesPinned;
+ PinnedFile pf = pinnerService.pinFile(
+ apk, webviewPinQuota, appInfo, PIN_GROUP, /*pinOptimizedDeps=*/true);
+ webviewPinQuota -= pf.bytesPinned;
}
}
diff --git a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
index 241390c12818..fbf9478b4fd9 100644
--- a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
@@ -170,7 +170,7 @@ class AppCompatCameraOverrides {
* </ul>
*/
boolean shouldApplyFreeformTreatmentForCameraCompat() {
- return Flags.cameraCompatForFreeform() && !isChangeEnabled(mActivityRecord,
+ return Flags.enableCameraCompatForDesktopWindowing() && !isChangeEnabled(mActivityRecord,
OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT);
}
diff --git a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
index 67bfd7605128..5338c01666fe 100644
--- a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
@@ -48,8 +48,9 @@ class AppCompatCameraPolicy {
// without the need to restart the device.
final boolean needsDisplayRotationCompatPolicy =
wmService.mAppCompatConfiguration.isCameraCompatTreatmentEnabledAtBuildTime();
- final boolean needsCameraCompatFreeformPolicy = Flags.cameraCompatForFreeform()
- && DesktopModeHelper.canEnterDesktopMode(wmService.mContext);
+ final boolean needsCameraCompatFreeformPolicy =
+ Flags.enableCameraCompatForDesktopWindowing()
+ && DesktopModeHelper.canEnterDesktopMode(wmService.mContext);
if (needsDisplayRotationCompatPolicy || needsCameraCompatFreeformPolicy) {
mCameraStateMonitor = new CameraStateMonitor(displayContent, wmService.mH);
mActivityRefresher = new ActivityRefresher(wmService, wmService.mH);
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 2259b5a5b08c..515f148ac2ff 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -1120,7 +1120,9 @@ public class BackgroundActivityStartController {
@Nullable Task targetTask, int launchFlags, int balCode, int callingUid,
int realCallingUid, TaskDisplayArea preferredTaskDisplayArea) {
// BAL Exception allowed in all cases
- if (balCode == BAL_ALLOW_ALLOWLISTED_UID) {
+ if (balCode == BAL_ALLOW_ALLOWLISTED_UID
+ || (android.security.Flags.asmReintroduceGracePeriod()
+ && balCode == BAL_ALLOW_GRACE_PERIOD)) {
return true;
}
@@ -1173,10 +1175,15 @@ public class BackgroundActivityStartController {
ArrayList<Task> visibleTasks = displayArea.getVisibleTasks();
for (int i = 0; i < visibleTasks.size(); i++) {
Task task = visibleTasks.get(i);
- if (visibleTasks.size() == 1 && task.isActivityTypeHomeOrRecents()) {
- bas.optedIn(task.getTopMostActivity());
- } else {
+ if (android.security.Flags.asmReintroduceGracePeriod()) {
bas = checkTopActivityForAsm(task, callingUid, /*sourceRecord*/null, bas);
+ } else {
+ if (visibleTasks.size() == 1 && task.isActivityTypeHomeOrRecents()) {
+ bas.optedIn(task.getTopMostActivity());
+ } else {
+ bas = checkTopActivityForAsm(
+ task, callingUid, /*sourceRecord*/null, bas);
+ }
}
}
}
diff --git a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
index e3232e08749e..d6caa1a248b4 100644
--- a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
+++ b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
@@ -124,7 +124,7 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa
*/
@VisibleForTesting
boolean shouldApplyFreeformTreatmentForCameraCompat(@NonNull ActivityRecord activity) {
- return Flags.cameraCompatForFreeform() && !activity.info.isChangeEnabled(
+ return Flags.enableCameraCompatForDesktopWindowing() && !activity.info.isChangeEnabled(
ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT);
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index b8f47cce6005..942634704ff5 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -9007,7 +9007,9 @@ public class WindowManagerService extends IWindowManager.Stub
final boolean isInputTargetNotFocused =
mFocusedInputTarget != t && mFocusedInputTarget != null;
- if (!isInputTargetNotFocused) {
+ final boolean isTouchOnFocusedDisplay = mFocusedInputTarget != null
+ && t.getDisplayId() == mFocusedInputTarget.getDisplayId();
+ if (!(isInputTargetNotFocused && isTouchOnFocusedDisplay)) {
return false;
}
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 4c4b4f65edf5..0e3ab63aefb9 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1969,6 +1969,13 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
boolean isReadyForDisplay() {
final boolean parentAndClientVisible = !isParentWindowHidden()
&& mViewVisibility == View.VISIBLE;
+ // TODO(b/338426357): Remove this once the last target using legacy transitions is moved to
+ // shell transitions
+ if (!mTransitionController.isShellTransitionsEnabled()) {
+ return mHasSurface && isVisibleByPolicy() && !mDestroying
+ && ((parentAndClientVisible && mToken.isVisible())
+ || isAnimating(TRANSITION | PARENTS));
+ }
return mHasSurface && isVisibleByPolicy() && !mDestroying && mToken.isVisible()
&& (parentAndClientVisible || isAnimating(TRANSITION | PARENTS));
}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 5cf260adece6..ce6f1ecc9463 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -205,6 +205,7 @@ import com.android.server.os.SchedulingPolicyService;
import com.android.server.pdb.PersistentDataBlockService;
import com.android.server.people.PeopleService;
import com.android.server.permission.access.AccessCheckingService;
+import com.android.server.pinner.PinnerService;
import com.android.server.pm.ApexManager;
import com.android.server.pm.ApexSystemServiceInfo;
import com.android.server.pm.BackgroundInstallControlService;
diff --git a/services/tests/powerstatstests/res/xml/irq_device_map_3.xml b/services/tests/powerstatstests/res/xml/irq_device_map_3.xml
index fd55428c48df..c3df0785bd9b 100644
--- a/services/tests/powerstatstests/res/xml/irq_device_map_3.xml
+++ b/services/tests/powerstatstests/res/xml/irq_device_map_3.xml
@@ -32,4 +32,7 @@
<device name="test.sensor.device">
<subsystem>Sensor</subsystem>
</device>
+ <device name="test.bluetooth.device">
+ <subsystem>Bluetooth</subsystem>
+ </device>
</irq-device-map> \ No newline at end of file
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java
index 0dc836ba0400..fe4d971face5 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java
@@ -17,6 +17,7 @@
package com.android.server.power.stats.wakeups;
import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_ALARM;
+import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_BLUETOOTH;
import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA;
import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SENSOR;
import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER;
@@ -52,6 +53,7 @@ public class CpuWakeupStatsTest {
private static final String KERNEL_REASON_SOUND_TRIGGER_IRQ = "129 test.sound_trigger.device";
private static final String KERNEL_REASON_SENSOR_IRQ = "15 test.sensor.device";
private static final String KERNEL_REASON_CELLULAR_DATA_IRQ = "18 test.cellular_data.device";
+ private static final String KERNEL_REASON_BLUETOOTH_IRQ = "19 test.bluetooth.device";
private static final String KERNEL_REASON_UNKNOWN_IRQ = "140 test.unknown.device";
private static final String KERNEL_REASON_UNKNOWN_FORMAT = "free-form-reason test.alarm.device";
private static final String KERNEL_REASON_ALARM_ABNORMAL = "-1 test.alarm.device";
@@ -62,12 +64,14 @@ public class CpuWakeupStatsTest {
private static final int TEST_UID_3 = 92261423;
private static final int TEST_UID_4 = 56926423;
private static final int TEST_UID_5 = 76421423;
+ private static final int TEST_UID_6 = 62345353;
private static final int TEST_PROC_STATE_1 = 72331;
private static final int TEST_PROC_STATE_2 = 792351;
private static final int TEST_PROC_STATE_3 = 138831;
private static final int TEST_PROC_STATE_4 = 23231;
private static final int TEST_PROC_STATE_5 = 42;
+ private static final int TEST_PROC_STATE_6 = 129942;
private static final Context sContext = InstrumentationRegistry.getTargetContext();
private final Handler mHandler = Mockito.mock(Handler.class);
@@ -79,6 +83,7 @@ public class CpuWakeupStatsTest {
obj.mUidProcStates.put(TEST_UID_3, TEST_PROC_STATE_3);
obj.mUidProcStates.put(TEST_UID_4, TEST_PROC_STATE_4);
obj.mUidProcStates.put(TEST_UID_5, TEST_PROC_STATE_5);
+ obj.mUidProcStates.put(TEST_UID_6, TEST_PROC_STATE_6);
}
@Test
@@ -118,6 +123,7 @@ public class CpuWakeupStatsTest {
CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER,
CPU_WAKEUP_SUBSYSTEM_SENSOR,
CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA,
+ CPU_WAKEUP_SUBSYSTEM_BLUETOOTH,
};
final String[] kernelReasons = new String[] {
@@ -126,10 +132,11 @@ public class CpuWakeupStatsTest {
KERNEL_REASON_SOUND_TRIGGER_IRQ,
KERNEL_REASON_SENSOR_IRQ,
KERNEL_REASON_CELLULAR_DATA_IRQ,
+ KERNEL_REASON_BLUETOOTH_IRQ,
};
final int[] uids = new int[] {
- TEST_UID_2, TEST_UID_3, TEST_UID_4, TEST_UID_1, TEST_UID_5
+ TEST_UID_2, TEST_UID_3, TEST_UID_4, TEST_UID_1, TEST_UID_5, TEST_UID_6
};
final int[] procStates = new int[] {
@@ -137,7 +144,8 @@ public class CpuWakeupStatsTest {
TEST_PROC_STATE_3,
TEST_PROC_STATE_4,
TEST_PROC_STATE_1,
- TEST_PROC_STATE_5
+ TEST_PROC_STATE_5,
+ TEST_PROC_STATE_6
};
final int total = subsystems.length;
@@ -285,6 +293,40 @@ public class CpuWakeupStatsTest {
}
@Test
+ public void bluetoothIrqAttributionSolo() {
+ final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler);
+ final long wakeupTime = 1236121;
+
+ populateDefaultProcStates(obj);
+
+ obj.noteWakeupTimeAndReason(wakeupTime, 1, KERNEL_REASON_BLUETOOTH_IRQ);
+
+ // Outside the window, so should be ignored.
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH,
+ wakeupTime - obj.mConfig.WAKEUP_MATCHING_WINDOW_MS - 1, TEST_UID_1);
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH,
+ wakeupTime + obj.mConfig.WAKEUP_MATCHING_WINDOW_MS + 1, TEST_UID_2);
+ // Should be attributed
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, wakeupTime + 5, TEST_UID_3,
+ TEST_UID_5);
+
+ final SparseArray<SparseIntArray> attribution = obj.mWakeupAttribution.get(wakeupTime);
+ assertThat(attribution).isNotNull();
+ assertThat(attribution.size()).isEqualTo(1);
+ assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH)).isTrue();
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).indexOfKey(
+ TEST_UID_1)).isLessThan(0);
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).indexOfKey(
+ TEST_UID_2)).isLessThan(0);
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_3)).isEqualTo(
+ TEST_PROC_STATE_3);
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).indexOfKey(
+ TEST_UID_4)).isLessThan(0);
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_5)).isEqualTo(
+ TEST_PROC_STATE_5);
+ }
+
+ @Test
public void alarmAndWifiIrqAttribution() {
final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler);
final long wakeupTime = 92123210;
@@ -400,6 +442,47 @@ public class CpuWakeupStatsTest {
}
@Test
+ public void unknownAndBluetoothAttribution() {
+ final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler);
+ final long wakeupTime = 92123520;
+
+ populateDefaultProcStates(obj);
+
+ obj.noteWakeupTimeAndReason(wakeupTime, 24,
+ KERNEL_REASON_UNKNOWN_IRQ + ":" + KERNEL_REASON_BLUETOOTH_IRQ);
+
+ // Bluetooth activity
+ // Outside the window, so should be ignored.
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH,
+ wakeupTime - obj.mConfig.WAKEUP_MATCHING_WINDOW_MS - 1, TEST_UID_4);
+ // Should be attributed
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, wakeupTime + 2, TEST_UID_1);
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, wakeupTime - 1, TEST_UID_3,
+ TEST_UID_5);
+
+ // Unrelated, should be ignored.
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 5, TEST_UID_3);
+
+ final SparseArray<SparseIntArray> attribution = obj.mWakeupAttribution.get(wakeupTime);
+ assertThat(attribution).isNotNull();
+ assertThat(attribution.size()).isEqualTo(2);
+ assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH)).isTrue();
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_1)).isEqualTo(
+ TEST_PROC_STATE_1);
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH)
+ .indexOfKey(TEST_UID_2)).isLessThan(0);
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_3)).isEqualTo(
+ TEST_PROC_STATE_3);
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH)
+ .indexOfKey(TEST_UID_4)).isLessThan(0);
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_5)).isEqualTo(
+ TEST_PROC_STATE_5);
+ assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_UNKNOWN)).isTrue();
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_UNKNOWN)).isNull();
+ assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_ALARM)).isFalse();
+ }
+
+ @Test
public void unknownFormatWakeupIgnored() {
final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler);
final long wakeupTime = 72123210;
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index ac1b7c6876f7..cbe6700f4d41 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -52,6 +52,7 @@ android_test {
"services.credentials",
"services.devicepolicy",
"services.flags",
+ "com.android.server.flags.services-aconfig-java",
"services.net",
"services.people",
"services.supervision",
@@ -81,6 +82,7 @@ android_test {
// TODO: remove once Android migrates to JUnit 4.12,
// which provides assertThrows
"testng",
+ "flag-junit",
"junit",
"junit-params",
"ActivityContext",
diff --git a/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java b/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java
index ec78bcea7539..c18faef2c028 100644
--- a/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java
@@ -31,6 +31,9 @@ import android.content.pm.ResolveInfo;
import android.os.Binder;
import android.os.Handler;
import android.os.Looper;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.DeviceConfig;
import android.provider.DeviceConfigInterface;
import android.testing.TestableContext;
@@ -43,6 +46,9 @@ import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.server.flags.Flags;
+import com.android.server.pinner.PinnedFile;
+import com.android.server.pinner.PinnerService;
import com.android.server.testutils.FakeDeviceConfigInterface;
import com.android.server.wm.ActivityTaskManagerInternal;
@@ -73,15 +79,18 @@ public class PinnerServiceTest {
private static final long WAIT_FOR_PINNER_TIMEOUT = TimeUnit.SECONDS.toMillis(2);
+ private static final int MEMORY_PERCENTAGE_FOR_QUOTA = 10;
+
@Rule
public TestableContext mContext =
new TestableContext(InstrumentationRegistry.getContext(), null);
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
private final ArraySet<String> mUpdatedPackages = new ArraySet<>();
private ResolveInfo mHomePackageResolveInfo;
private FakeDeviceConfigInterface mFakeDeviceConfigInterface;
private PinnerService.Injector mInjector;
-
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
@@ -114,6 +123,8 @@ public class PinnerServiceTest {
resources.addOverride(com.android.internal.R.bool.config_pinnerCameraApp, false);
resources.addOverride(com.android.internal.R.integer.config_pinnerHomePinBytes, 0);
resources.addOverride(com.android.internal.R.bool.config_pinnerAssistantApp, false);
+ resources.addOverride(com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage,
+ MEMORY_PERCENTAGE_FOR_QUOTA);
mFakeDeviceConfigInterface = new FakeDeviceConfigInterface();
setDeviceConfigPinnedAnonSize(0);
@@ -138,10 +149,9 @@ public class PinnerServiceTest {
}
@Override
- protected PinnerService.PinnedFile pinFileInternal(String fileToPin,
- int maxBytesToPin, boolean attemptPinIntrospection) {
- return new PinnerService.PinnedFile(-1,
- maxBytesToPin, fileToPin, maxBytesToPin);
+ protected PinnedFile pinFileInternal(PinnerService service, String fileToPin,
+ long maxBytesToPin, boolean attemptPinIntrospection) {
+ return new PinnedFile(-1, maxBytesToPin, fileToPin, maxBytesToPin);
}
};
}
@@ -167,6 +177,12 @@ public class PinnerServiceTest {
unpinAnonRegionMethod.invoke(pinnerService);
}
+ private long getGlobalPinQuota(PinnerService service) throws Exception {
+ Method getQuotaMethod = PinnerService.class.getDeclaredMethod("getAvailableGlobalQuota");
+ getQuotaMethod.setAccessible(true);
+ return (long) getQuotaMethod.invoke(service);
+ }
+
private void waitForPinnerService(PinnerService pinnerService)
throws NoSuchFieldException, IllegalAccessException {
// There's no notification/callback when pinning finished
@@ -315,15 +331,121 @@ public class PinnerServiceTest {
PinnerService pinnerService = new PinnerService(mContext, mInjector);
pinnerService.onStart();
- pinnerService.pinFile("test_file", 4096, null, "my_group");
+ pinnerService.pinFile("test_file", 4096, null, "my_group", false);
- assertThat(getPinnedSize(pinnerService)).isGreaterThan(0);
- assertThat(getTotalPinnedFiles(pinnerService)).isGreaterThan(0);
+ assertThat(getPinnedSize(pinnerService)).isEqualTo(4096);
+ assertThat(getTotalPinnedFiles(pinnerService)).isEqualTo(1);
+
+ unpinAll(pinnerService);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+ public void testPinAllQuota() throws Exception {
+ PinnerService pinnerService = new PinnerService(mContext, mInjector);
+ pinnerService.onStart();
+
+ long quota = getGlobalPinQuota(pinnerService);
+
+ pinnerService.pinFile("test_file", Long.MAX_VALUE, null, "my_group", false);
+
+ assertThat(getPinnedSize(pinnerService)).isEqualTo(quota);
unpinAll(pinnerService);
}
@Test
+ @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+ public void testGlobalPinQuotaAsDevicePercentage() throws Exception {
+ PinnerService pinnerService = new PinnerService(mContext, mInjector);
+ pinnerService.onStart();
+ long origQuota = getGlobalPinQuota(pinnerService);
+
+ long totalMem = android.os.Process.getTotalMemory();
+
+ // Verify that pin quota is the set percentage of device total memory
+ assertThat(origQuota).isEqualTo((totalMem * MEMORY_PERCENTAGE_FOR_QUOTA) / 100);
+
+ pinnerService.pinFile("test_file", 4096, null, "my_group", false);
+ assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(origQuota - 4096);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+ public void testGlobalPinWhenNoQuota() throws Exception {
+ TestableResources resources = mContext.getOrCreateTestableResources();
+ resources.addOverride(
+ com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage, 0);
+
+ PinnerService pinnerService = new PinnerService(mContext, mInjector);
+ pinnerService.onStart();
+
+ // Verify that pin quota is zero
+ assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(0);
+
+ pinnerService.pinFile("test_file", 4096, null, "my_group", false);
+ assertThat(getTotalPinnedFiles(pinnerService)).isEqualTo(0);
+ }
+
+ /**
+ * This test is temporary, it should be cleaned up when removing the pin_global_quota bugfix
+ * flag.
+ */
+ @Test
+ @DisableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+ public void testGlobalQuotaDisabled() throws Exception {
+ TestableResources resources = mContext.getOrCreateTestableResources();
+ resources.addOverride(
+ com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage, 0);
+
+ PinnerService pinnerService = new PinnerService(mContext, mInjector);
+ pinnerService.onStart();
+
+ // The quota parameter exists but it should have no effect on pinning
+ long quota = getGlobalPinQuota(pinnerService);
+
+ pinnerService.pinFile("test_file", quota + 1, null, "my_group", false);
+
+ // Verify that we can pin past the quota as it is disabled
+ assertThat(getPinnedSize(pinnerService)).isEqualTo(quota + 1);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+ public void testUnpinReleasesQuota() throws Exception {
+ PinnerService pinnerService = new PinnerService(mContext, mInjector);
+ pinnerService.onStart();
+ long origQuota = getGlobalPinQuota(pinnerService);
+
+ // Verify that pin quota exists and is non zero.
+ assertThat(getGlobalPinQuota(pinnerService)).isGreaterThan(0);
+
+ pinnerService.pinFile("test_file", origQuota, null, "my_group", false);
+
+ // Make sure all the quota was consumed
+ assertThat(getPinnedSize(pinnerService)).isEqualTo(origQuota);
+
+ // Unpin the file and verify that the quota has been released.
+ pinnerService.unpinFile("test_file");
+ assertThat(getPinnedSize(pinnerService)).isEqualTo(0);
+ assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(origQuota);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+ public void testGlobalPinQuotaNegative() throws Exception {
+ TestableResources resources = mContext.getOrCreateTestableResources();
+ resources.addOverride(
+ com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage, -10);
+
+ PinnerService pinnerService = new PinnerService(mContext, mInjector);
+ pinnerService.onStart();
+
+ // Verify that pin quota is zero
+ assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(0);
+ }
+
+ @Test
public void testPinAnonRegion() throws Exception {
setDeviceConfigPinnedAnonSize(32768);
diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
index abc9ce3fdc36..ee63d5d32ff1 100644
--- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
@@ -38,6 +38,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
@@ -91,6 +92,7 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
@@ -174,8 +176,8 @@ public class MediaProjectionManagerServiceTest {
private PackageManager mPackageManager;
@Mock
private KeyguardManager mKeyguardManager;
- @Mock
- AppOpsManager mAppOpsManager;
+
+ private AppOpsManager mAppOpsManager;
@Mock
private IMediaProjectionWatcherCallback mWatcherCallback;
@Mock
@@ -193,6 +195,7 @@ public class MediaProjectionManagerServiceTest {
LocalServices.removeServiceForTest(WindowManagerInternal.class);
LocalServices.addService(WindowManagerInternal.class, mWindowManagerInternal);
+ mAppOpsManager = mockAppOpsManager();
mContext.addMockSystemService(AppOpsManager.class, mAppOpsManager);
mContext.addMockSystemService(KeyguardManager.class, mKeyguardManager);
mContext.setMockPackageManager(mPackageManager);
@@ -206,6 +209,17 @@ public class MediaProjectionManagerServiceTest {
mService = new MediaProjectionManagerService(mContext);
}
+ private static AppOpsManager mockAppOpsManager() {
+ return mock(AppOpsManager.class, invocationOnMock -> {
+ if (invocationOnMock.getMethod().getName().startsWith("noteOp")) {
+ // Mockito will return 0 for non-stubbed method which corresponds to MODE_ALLOWED
+ // and is not what we want.
+ return AppOpsManager.MODE_IGNORED;
+ }
+ return Answers.RETURNS_DEFAULTS.answer(invocationOnMock);
+ });
+ }
+
@After
public void tearDown() {
LocalServices.removeServiceForTest(ActivityManagerInternal.class);
@@ -305,8 +319,10 @@ public class MediaProjectionManagerServiceTest {
public void testCreateProjection_keyguardLocked_AppOpMediaProjection()
throws NameNotFoundException {
MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
- doReturn(true).when(mAppOpsManager).isOperationActive(eq(AppOpsManager.OP_PROJECT_MEDIA),
- eq(projection.uid), eq(projection.packageName));
+ doReturn(AppOpsManager.MODE_ALLOWED).when(mAppOpsManager)
+ .noteOpNoThrow(eq(AppOpsManager.OP_PROJECT_MEDIA),
+ eq(projection.uid), eq(projection.packageName), nullable(String.class),
+ nullable(String.class));
doReturn(true).when(mKeyguardManager).isKeyguardLocked();
doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager).checkPermission(
@@ -1159,7 +1175,7 @@ public class MediaProjectionManagerServiceTest {
doReturn(mAppInfo).when(mPackageManager).getApplicationInfoAsUser(anyString(),
any(ApplicationInfoFlags.class), any(UserHandle.class));
return service.createProjectionInternal(UID, PACKAGE_NAME,
- TYPE_MIRRORING, /* isPermanentGrant= */ true, UserHandle.CURRENT);
+ TYPE_MIRRORING, /* isPermanentGrant= */ false, UserHandle.CURRENT);
}
// Set up preconditions for starting a projection, with no foreground service requirements.
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 6c9015d72d5a..44770d21892c 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -4652,7 +4652,42 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
doThrow(new SecurityException("no access")).when(mUgmInternal)
.checkGrantUriPermission(eq(Process.myUid()), any(), eq(soundUri),
- anyInt(), eq(Process.myUserHandle().getIdentifier()));
+ anyInt(), eq(Process.myUserHandle().getIdentifier()));
+
+ mBinderService.updateNotificationChannelFromPrivilegedListener(
+ null, mPkg, Process.myUserHandle(), updatedNotificationChannel);
+
+ verify(mPreferencesHelper, times(1)).updateNotificationChannel(
+ anyString(), anyInt(), any(), anyBoolean(), anyInt(), anyBoolean());
+
+ verify(mListeners, never()).notifyNotificationChannelChanged(eq(mPkg),
+ eq(Process.myUserHandle()), eq(mTestNotificationChannel),
+ eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED));
+ }
+
+ @Test
+ public void
+ testUpdateNotificationChannelFromPrivilegedListener_oldSoundNoUriPerm_newSoundHasUriPerm()
+ throws Exception {
+ mService.setPreferencesHelper(mPreferencesHelper);
+ when(mCompanionMgr.getAssociations(mPkg, mUserId))
+ .thenReturn(singletonList(mock(AssociationInfo.class)));
+ when(mPreferencesHelper.getNotificationChannel(eq(mPkg), anyInt(),
+ eq(mTestNotificationChannel.getId()), anyBoolean()))
+ .thenReturn(mTestNotificationChannel);
+
+ // Missing Uri permissions for the old channel sound
+ final Uri oldSoundUri = Settings.System.DEFAULT_NOTIFICATION_URI;
+ doThrow(new SecurityException("no access")).when(mUgmInternal)
+ .checkGrantUriPermission(eq(Process.myUid()), any(), eq(oldSoundUri),
+ anyInt(), eq(Process.myUserHandle().getIdentifier()));
+
+ // Has Uri permissions for the old channel sound
+ final Uri newSoundUri = Uri.parse("content://media/test/sound/uri");
+ final NotificationChannel updatedNotificationChannel = new NotificationChannel(
+ TEST_CHANNEL_ID, TEST_CHANNEL_ID, IMPORTANCE_DEFAULT);
+ updatedNotificationChannel.setSound(newSoundUri,
+ updatedNotificationChannel.getAudioAttributes());
mBinderService.updateNotificationChannelFromPrivilegedListener(
null, mPkg, Process.myUserHandle(), updatedNotificationChannel);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index a0c0df8853f9..d64b9e858c64 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -45,11 +45,13 @@ import static android.app.NotificationManager.IMPORTANCE_MAX;
import static android.app.NotificationManager.IMPORTANCE_NONE;
import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
import static android.app.NotificationManager.VISIBILITY_NO_OVERRIDE;
+import static android.content.ContentResolver.SCHEME_ANDROID_RESOURCE;
+import static android.content.ContentResolver.SCHEME_CONTENT;
+import static android.content.ContentResolver.SCHEME_FILE;
import static android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION;
import static android.media.AudioAttributes.USAGE_NOTIFICATION;
import static android.os.UserHandle.USER_ALL;
import static android.os.UserHandle.USER_SYSTEM;
-
import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
import static android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION;
import static android.service.notification.Flags.notificationClassification;
@@ -59,6 +61,7 @@ import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_P
import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED;
import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED;
import static com.android.server.notification.Flags.FLAG_ALL_NOTIFS_NEED_TTL;
+import static com.android.server.notification.Flags.FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI;
import static com.android.server.notification.Flags.FLAG_PERSIST_INCOMPLETE_RESTORE_DATA;
import static com.android.server.notification.NotificationChannelLogger.NotificationChannelEvent.NOTIFICATION_CHANNEL_UPDATED_BY_USER;
import static com.android.server.notification.PreferencesHelper.DEFAULT_BUBBLE_PREFERENCE;
@@ -84,6 +87,7 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
@@ -369,10 +373,10 @@ public class PreferencesHelperTest extends UiServiceTestCase {
mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
- false, mClock);
+ mUgmInternal, false, mClock);
mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
- false, mClock);
+ mUgmInternal, false, mClock);
resetZenModeHelper();
mAudioAttributes = new AudioAttributes.Builder()
@@ -783,7 +787,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
public void testReadXml_oldXml_migrates() throws Exception {
mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
- /* showReviewPermissionsNotification= */ true, mClock);
+ mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock);
String xml = "<ranking version=\"2\">\n"
+ "<package name=\"" + PKG_N_MR1 + "\" uid=\"" + UID_N_MR1
@@ -919,7 +923,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
public void testReadXml_newXml_noMigration_showPermissionNotification() throws Exception {
mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
- /* showReviewPermissionsNotification= */ true, mClock);
+ mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock);
String xml = "<ranking version=\"3\">\n"
+ "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n"
@@ -978,7 +982,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
public void testReadXml_newXml_permissionNotificationOff() throws Exception {
mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
- /* showReviewPermissionsNotification= */ false, mClock);
+ mUgmInternal, /* showReviewPermissionsNotification= */ false, mClock);
String xml = "<ranking version=\"3\">\n"
+ "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n"
@@ -1037,7 +1041,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
public void testReadXml_newXml_noMigration_noPermissionNotification() throws Exception {
mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
- /* showReviewPermissionsNotification= */ true, mClock);
+ mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock);
String xml = "<ranking version=\"4\">\n"
+ "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n"
@@ -1709,7 +1713,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
// simulate load after reboot
mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
- false, mClock);
+ mUgmInternal, false, mClock);
loadByteArrayXml(baos.toByteArray(), false, USER_ALL);
// Trigger 2nd restore pass
@@ -1764,7 +1768,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
// simulate load after reboot
mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
- false, mClock);
+ mUgmInternal, false, mClock);
loadByteArrayXml(xml.getBytes(), false, USER_ALL);
// Trigger 2nd restore pass
@@ -1842,10 +1846,10 @@ public class PreferencesHelperTest extends UiServiceTestCase {
mHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
- false, mClock);
+ mUgmInternal, false, mClock);
mXmlHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
- false, mClock);
+ mUgmInternal, false, mClock);
NotificationChannel channel =
new NotificationChannel("id", "name", IMPORTANCE_LOW);
@@ -3049,6 +3053,64 @@ public class PreferencesHelperTest extends UiServiceTestCase {
}
@Test
+ @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI)
+ public void testCreateChannel_noSoundUriPermission_contentSchemeVerified() {
+ final Uri sound = Uri.parse(SCHEME_CONTENT + "://media/test/sound/uri");
+
+ doThrow(new SecurityException("no access")).when(mUgmInternal)
+ .checkGrantUriPermission(eq(UID_N_MR1), any(), eq(sound),
+ anyInt(), eq(Process.myUserHandle().getIdentifier()));
+
+ final NotificationChannel channel = new NotificationChannel("id2", "name2",
+ NotificationManager.IMPORTANCE_DEFAULT);
+ channel.setSound(sound, mAudioAttributes);
+
+ assertThrows(SecurityException.class,
+ () -> mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel,
+ true, false, UID_N_MR1, false));
+ assertThat(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), true))
+ .isNull();
+ }
+
+ @Test
+ @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI)
+ public void testCreateChannel_noSoundUriPermission_fileSchemaIgnored() {
+ final Uri sound = Uri.parse(SCHEME_FILE + "://path/sound");
+
+ doThrow(new SecurityException("no access")).when(mUgmInternal)
+ .checkGrantUriPermission(eq(UID_N_MR1), any(), any(),
+ anyInt(), eq(Process.myUserHandle().getIdentifier()));
+
+ final NotificationChannel channel = new NotificationChannel("id2", "name2",
+ NotificationManager.IMPORTANCE_DEFAULT);
+ channel.setSound(sound, mAudioAttributes);
+
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1,
+ false);
+ assertThat(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), true)
+ .getSound()).isEqualTo(sound);
+ }
+
+ @Test
+ @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI)
+ public void testCreateChannel_noSoundUriPermission_resourceSchemaIgnored() {
+ final Uri sound = Uri.parse(SCHEME_ANDROID_RESOURCE + "://resId/sound");
+
+ doThrow(new SecurityException("no access")).when(mUgmInternal)
+ .checkGrantUriPermission(eq(UID_N_MR1), any(), any(),
+ anyInt(), eq(Process.myUserHandle().getIdentifier()));
+
+ final NotificationChannel channel = new NotificationChannel("id2", "name2",
+ NotificationManager.IMPORTANCE_DEFAULT);
+ channel.setSound(sound, mAudioAttributes);
+
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1,
+ false);
+ assertThat(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), true)
+ .getSound()).isEqualTo(sound);
+ }
+
+ @Test
public void testPermanentlyDeleteChannels() throws Exception {
NotificationChannel channel1 =
new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH);
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java
index 0d13be6d5ab2..e8ca8bf8ec63 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java
@@ -127,13 +127,13 @@ public class VibratorControllerTest {
public void setExternalControl_withCapability_enablesExternalControl() {
mockVibratorCapabilities(IVibrator.CAP_EXTERNAL_CONTROL);
VibratorController controller = createController();
- assertFalse(controller.isUnderExternalControl());
+ assertFalse(controller.isVibrating());
controller.setExternalControl(true);
- assertTrue(controller.isUnderExternalControl());
+ assertTrue(controller.isVibrating());
controller.setExternalControl(false);
- assertFalse(controller.isUnderExternalControl());
+ assertFalse(controller.isVibrating());
InOrder inOrderVerifier = inOrder(mNativeWrapperMock);
inOrderVerifier.verify(mNativeWrapperMock).setExternalControl(eq(true));
@@ -143,10 +143,10 @@ public class VibratorControllerTest {
@Test
public void setExternalControl_withNoCapability_ignoresExternalControl() {
VibratorController controller = createController();
- assertFalse(controller.isUnderExternalControl());
+ assertFalse(controller.isVibrating());
controller.setExternalControl(true);
- assertFalse(controller.isUnderExternalControl());
+ assertFalse(controller.isVibrating());
verify(mNativeWrapperMock, never()).setExternalControl(anyBoolean());
}
@@ -181,6 +181,38 @@ public class VibratorControllerTest {
}
@Test
+ public void setAmplitude_vibratorIdle_ignoresAmplitude() {
+ VibratorController controller = createController();
+ assertFalse(controller.isVibrating());
+
+ controller.setAmplitude(1);
+ assertEquals(0, controller.getCurrentAmplitude(), /* delta= */ 0);
+ }
+
+ @Test
+ public void setAmplitude_vibratorUnderExternalControl_ignoresAmplitude() {
+ mockVibratorCapabilities(IVibrator.CAP_EXTERNAL_CONTROL);
+ VibratorController controller = createController();
+ controller.setExternalControl(true);
+ assertTrue(controller.isVibrating());
+
+ controller.setAmplitude(1);
+ assertEquals(0, controller.getCurrentAmplitude(), /* delta= */ 0);
+ }
+
+ @Test
+ public void setAmplitude_vibratorVibrating_setsAmplitude() {
+ when(mNativeWrapperMock.on(anyLong(), anyLong())).thenAnswer(args -> args.getArgument(0));
+ VibratorController controller = createController();
+ controller.on(100, /* vibrationId= */ 1);
+ assertTrue(controller.isVibrating());
+ assertEquals(-1, controller.getCurrentAmplitude(), /* delta= */ 0);
+
+ controller.setAmplitude(1);
+ assertEquals(1, controller.getCurrentAmplitude(), /* delta= */ 0);
+ }
+
+ @Test
public void on_withDuration_turnsVibratorOn() {
when(mNativeWrapperMock.on(anyLong(), anyLong())).thenAnswer(args -> args.getArgument(0));
VibratorController controller = createController();
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index d99b20c689dd..538c3fc2ddae 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -266,12 +266,13 @@ public class VibratorManagerServiceTest {
@After
public void tearDown() throws Exception {
if (mService != null) {
- if (!mPendingVibrations.stream().allMatch(HalVibration::hasEnded)) {
- // Cancel any pending vibration from tests.
- cancelVibrate(mService);
- for (HalVibration vibration : mPendingVibrations) {
- vibration.waitForEnd();
- }
+ // Make sure we have permission to cancel test vibrations, even if the test denied them.
+ grantPermission(android.Manifest.permission.VIBRATE);
+ // Cancel any pending vibration from tests, including external vibrations.
+ cancelVibrate(mService);
+ // Wait until pending vibrations end asynchronously.
+ for (HalVibration vibration : mPendingVibrations) {
+ vibration.waitForEnd();
}
// Wait until all vibrators have stopped vibrating, waiting for ramp-down.
// Note: if a test is flaky here something is wrong with the vibration finalization.
@@ -2242,7 +2243,7 @@ public class VibratorManagerServiceTest {
VibratorManagerService service = createSystemReadyService();
VibrationEffect effect = VibrationEffect.createOneShot(10 * TEST_TIMEOUT_MILLIS, 100);
- vibrate(service, effect, HAPTIC_FEEDBACK_ATTRS);
+ HalVibration vibration = vibrate(service, effect, HAPTIC_FEEDBACK_ATTRS);
// VibrationThread will start this vibration async, so wait until vibration is triggered.
assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
@@ -2255,7 +2256,8 @@ public class VibratorManagerServiceTest {
assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
// Vibration is cancelled.
- assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
+ vibration.waitForEnd();
+ assertThat(vibration.getStatus()).isEqualTo(Status.CANCELLED_SUPERSEDED);
assertEquals(Arrays.asList(false, true),
mVibratorProviders.get(1).getExternalControlStates());
}
@@ -2296,7 +2298,7 @@ public class VibratorManagerServiceTest {
VibrationEffect repeatingEffect = VibrationEffect.createWaveform(
new long[]{100, 200, 300}, new int[]{128, 255, 255}, 1);
- vibrate(service, repeatingEffect, ALARM_ATTRS);
+ HalVibration repeatingVibration = vibrate(service, repeatingEffect, ALARM_ATTRS);
// VibrationThread will start this vibration async, so wait until vibration is triggered.
assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
@@ -2308,7 +2310,8 @@ public class VibratorManagerServiceTest {
assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
// Vibration is cancelled.
- assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
+ repeatingVibration.waitForEnd();
+ assertThat(repeatingVibration.getStatus()).isEqualTo(Status.CANCELLED_SUPERSEDED);
assertEquals(Arrays.asList(false, true),
mVibratorProviders.get(1).getExternalControlStates());
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java
index d66c21a77fcd..b91a5b7afe26 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java
@@ -28,7 +28,7 @@ import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_V
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM;
+import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
import android.compat.testing.PlatformCompatChangeRule;
import android.platform.test.annotations.DisableFlags;
@@ -218,7 +218,7 @@ public class AppCompatCameraOverridesTest extends WindowTestsBase {
}
@Test
- @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testShouldApplyCameraCompatFreeformTreatment_flagIsDisabled_returnsFalse() {
runTestScenario((robot) -> {
robot.activity().createActivityWithComponentInNewTask();
@@ -229,7 +229,7 @@ public class AppCompatCameraOverridesTest extends WindowTestsBase {
@Test
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT})
- @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testShouldApplyCameraCompatFreeformTreatment_overrideEnabled_returnsFalse() {
runTestScenario((robot) -> {
robot.activity().createActivityWithComponentInNewTask();
@@ -240,7 +240,7 @@ public class AppCompatCameraOverridesTest extends WindowTestsBase {
@Test
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT})
- @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testShouldApplyCameraCompatFreeformTreatment_disabledByOverride_returnsFalse() {
runTestScenario((robot) -> {
robot.activity().createActivityWithComponentInNewTask();
@@ -250,7 +250,7 @@ public class AppCompatCameraOverridesTest extends WindowTestsBase {
}
@Test
- @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testShouldApplyCameraCompatFreeformTreatment_notDisabledByOverride_returnsTrue() {
runTestScenario((robot) -> {
robot.activity().createActivityWithComponentInNewTask();
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
index d91b38efd40b..41102d6922da 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
@@ -20,7 +20,7 @@ import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM;
+import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -85,7 +85,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase {
}
@Test
- @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testCameraCompatFreeformPolicy_presentWhenEnabledAndDW() {
runTestScenario((robot) -> {
robot.allowEnterDesktopMode(/* isAllowed= */ true);
@@ -95,7 +95,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase {
}
@Test
- @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testCameraCompatFreeformPolicy_notPresentWhenNoDW() {
runTestScenario((robot) -> {
robot.allowEnterDesktopMode(/* isAllowed= */ false);
@@ -105,7 +105,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase {
}
@Test
- @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testCameraCompatFreeformPolicy_notPresentWhenNoFlag() {
runTestScenario((robot) -> {
robot.allowEnterDesktopMode(/* isAllowed= */ true);
@@ -115,7 +115,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase {
}
@Test
- @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testCameraCompatFreeformPolicy_notPresentWhenNoFlagAndNoDW() {
runTestScenario((robot) -> {
robot.allowEnterDesktopMode(/* isAllowed= */ false);
@@ -125,7 +125,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase {
}
@Test
- @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testCameraCompatFreeformPolicy_startedWhenEnabledAndDW() {
runTestScenario((robot) -> {
robot.allowEnterDesktopMode(/* isAllowed= */ true);
@@ -136,7 +136,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase {
}
@Test
- @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testCameraStateManager_existsWhenCameraCompatFreeformExists() {
runTestScenario((robot) -> {
robot.allowEnterDesktopMode(true);
@@ -147,7 +147,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase {
}
@Test
- @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testCameraStateManager_startedWhenCameraCompatFreeformExists() {
runTestScenario((robot) -> {
robot.allowEnterDesktopMode(true);
@@ -180,7 +180,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase {
}
@Test
- @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testCameraStateManager_doesNotExistWhenNoPolicyExists() {
runTestScenario((robot) -> {
robot.conf().enableCameraCompatTreatmentAtBuildTime(/* enabled= */ false);
diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
index a48813d775d1..dbcef10a6be2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
@@ -35,7 +35,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
-import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM;
+import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -60,6 +60,7 @@ import android.content.res.Configuration.Orientation;
import android.graphics.Rect;
import android.hardware.camera2.CameraManager;
import android.os.Handler;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.view.DisplayInfo;
import android.view.Surface;
@@ -135,7 +136,6 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
});
mActivityRefresher = new ActivityRefresher(mDisplayContent.mWmService, mMockHandler);
- mSetFlagsRule.enableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM);
CameraStateMonitor cameraStateMonitor =
new CameraStateMonitor(mDisplayContent, mMockHandler);
mCameraCompatFreeformPolicy =
@@ -147,6 +147,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
cameraStateMonitor.startListeningToCameraState();
}
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@Test
public void testFullscreen_doesNotActivateCameraCompatMode() {
configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
@@ -157,6 +158,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
assertNotInCameraCompatMode();
}
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@Test
public void testOrientationUnspecified_doesNotActivateCameraCompatMode() {
configureActivity(SCREEN_ORIENTATION_UNSPECIFIED);
@@ -164,12 +166,14 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
assertNotInCameraCompatMode();
}
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@Test
public void testNoCameraConnection_doesNotActivateCameraCompatMode() {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
assertNotInCameraCompatMode();
}
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@Test
public void testCameraConnected_deviceInPortrait_portraitCameraCompatMode() throws Exception {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -210,6 +214,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
assertActivityRefreshRequested(/* refreshRequested */ false);
}
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@Test
public void testCameraReconnected_cameraCompatModeAndRefresh() throws Exception {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -235,6 +240,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
}
@Test
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT})
public void testShouldApplyCameraCompatFreeformTreatment_overrideEnabled_returnsFalse() {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -246,6 +252,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
}
@Test
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testShouldApplyCameraCompatFreeformTreatment_notDisabledByOverride_returnsTrue() {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -254,6 +261,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
}
@Test
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh()
throws Exception {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -268,6 +276,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
}
@Test
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception {
when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
.thenReturn(false);
@@ -281,6 +290,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
}
@Test
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp()
throws Exception {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
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 85cb1bcc01fb..5c0d424f4f42 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -83,7 +83,7 @@ import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_TOKEN_TRANSFO
import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
import static com.android.server.wm.WindowContainer.POSITION_TOP;
import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL;
-import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM;
+import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE;
import static com.google.common.truth.Truth.assertThat;
@@ -2820,7 +2820,7 @@ public class DisplayContentTests extends WindowTestsBase {
verify(mWm.mUmInternal, never()).isUserVisible(userId2, displayId);
}
- @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@Test
public void cameraCompatFreeformFlagEnabled_cameraCompatFreeformPolicyNotNull() {
doReturn(true).when(() ->
@@ -2829,7 +2829,7 @@ public class DisplayContentTests extends WindowTestsBase {
assertTrue(createNewDisplay().mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy());
}
- @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@Test
public void cameraCompatFreeformFlagNotEnabled_cameraCompatFreeformPolicyIsNull() {
doReturn(true).when(() ->
@@ -2838,7 +2838,7 @@ public class DisplayContentTests extends WindowTestsBase {
assertFalse(createNewDisplay().mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy());
}
- @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
@Test
public void desktopWindowingFlagNotEnabled_cameraCompatFreeformPolicyIsNull() {