summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/api/current.txt1
-rw-r--r--core/api/system-current.txt13
-rw-r--r--core/api/test-current.txt5
-rw-r--r--core/java/android/app/ActivityManager.java8
-rw-r--r--core/java/android/app/ActivityOptions.java34
-rw-r--r--core/java/android/app/GrammaticalInflectionManager.java5
-rw-r--r--core/java/android/app/IGrammaticalInflectionManager.aidl21
-rw-r--r--core/java/android/app/IWallpaperManager.aidl48
-rw-r--r--core/java/android/app/WallpaperManager.java333
-rw-r--r--core/java/android/app/grammatical_inflection_manager.aconfig2
-rw-r--r--core/java/android/companion/virtual/IVirtualDeviceManager.aidl5
-rw-r--r--core/java/android/companion/virtual/VirtualDeviceManager.java28
-rw-r--r--core/java/android/content/Context.java2
-rw-r--r--core/java/android/hardware/biometrics/flags.aconfig7
-rw-r--r--core/java/android/hardware/face/FaceManager.java240
-rw-r--r--core/java/android/hardware/face/IFaceService.aidl9
-rw-r--r--core/java/android/provider/Settings.java9
-rw-r--r--core/java/com/android/internal/widget/LockPatternUtils.java4
-rw-r--r--core/res/AndroidManifest.xml17
-rw-r--r--core/tests/coretests/src/android/hardware/face/FaceManagerTest.java23
-rw-r--r--core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java41
-rw-r--r--data/etc/privapp-permissions-platform.xml2
-rw-r--r--data/etc/services.core.protolog.json6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java25
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java1
-rw-r--r--media/java/android/media/MediaRouter2.java38
-rw-r--r--packages/SettingsLib/Spa/build.gradle.kts2
-rw-r--r--packages/SettingsLib/Spa/gradle/libs.versions.toml2
-rw-r--r--packages/SettingsLib/Spa/spa/build.gradle.kts4
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt3
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt2
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavGraphBuilder.kt13
-rw-r--r--packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/LiveDataTestUtil.kt50
-rw-r--r--packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PermissionsChangedFlow.kt46
-rw-r--r--packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PermissionsChangedFlowTest.kt81
-rw-r--r--packages/Shell/AndroidManifest.xml3
-rw-r--r--packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt2
-rw-r--r--packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt3
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt83
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt10
-rw-r--r--packages/SystemUI/docs/imgs/ribbon.pngbin32027 -> 12880 bytes
-rw-r--r--packages/SystemUI/docs/scene.md170
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt55
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt56
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetConfigurationControllerTest.kt111
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt115
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt23
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt14
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt2
-rw-r--r--packages/SystemUI/res/values/strings.xml2
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/BiometricDisplayListener.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt66
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt32
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt17
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt78
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetConfigurationController.kt87
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetConfigurator.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepository.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepositoryImpl.kt68
-rw-r--r--packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt62
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapterDelegate.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/settings/DisplayTrackerImpl.kt33
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java13
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt42
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt152
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java7
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt11
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt18
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/communal/widgets/FakeWidgetConfigurator.kt30
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/communal/widgets/WidgetConfiguratorKosmos.kt30
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt1
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt31
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt7
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt2
-rw-r--r--packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt20
-rw-r--r--packages/WallpaperBackup/AndroidManifest.xml14
-rw-r--r--packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java67
-rw-r--r--packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java11
-rw-r--r--services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java23
-rw-r--r--services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java2
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java48
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/face/FaceService.java35
-rw-r--r--services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionManagerInternal.java10
-rw-r--r--services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java48
-rw-r--r--services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionShellCommand.java11
-rw-r--r--services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionUtils.java46
-rwxr-xr-xservices/core/java/com/android/server/notification/NotificationManagerService.java50
-rw-r--r--services/core/java/com/android/server/notification/ZenModeHelper.java12
-rw-r--r--services/core/java/com/android/server/pm/UserManagerService.java18
-rw-r--r--services/core/java/com/android/server/utils/AnrTimer.java55
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperCropper.java399
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperData.java31
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperDataParser.java158
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java105
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperManagerService.java167
-rw-r--r--services/core/java/com/android/server/wm/WallpaperController.java137
-rw-r--r--services/core/java/com/android/server/wm/WallpaperWindowToken.java16
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerInternal.java17
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerService.java25
-rw-r--r--services/core/jni/com_android_server_utils_AnrTimer.cpp27
-rw-r--r--services/tests/servicestests/AndroidManifest.xml1
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java21
-rw-r--r--services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java42
-rwxr-xr-xservices/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java53
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java22
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java3
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java13
-rw-r--r--telecomm/java/android/telecom/Response.java40
-rw-r--r--telephony/java/android/telephony/CarrierInfo.java236
-rw-r--r--telephony/java/android/telephony/CarrierRestrictionRules.java141
-rw-r--r--tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt22
126 files changed, 3975 insertions, 930 deletions
diff --git a/core/api/current.txt b/core/api/current.txt
index 40dcc427732b..865afc060736 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -16704,7 +16704,6 @@ package android.graphics {
}
public final class RecordingCanvas extends android.graphics.Canvas {
- method public final void drawMesh(@NonNull android.graphics.Mesh, android.graphics.BlendMode, @NonNull android.graphics.Paint);
}
public final class Rect implements android.os.Parcelable {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 2c01e3f42631..debf1bfdfc8c 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -56,6 +56,7 @@ package android {
field public static final String BIND_CONTENT_SUGGESTIONS_SERVICE = "android.permission.BIND_CONTENT_SUGGESTIONS_SERVICE";
field public static final String BIND_DIRECTORY_SEARCH = "android.permission.BIND_DIRECTORY_SEARCH";
field public static final String BIND_DISPLAY_HASHING_SERVICE = "android.permission.BIND_DISPLAY_HASHING_SERVICE";
+ field @FlaggedApi("com.android.internal.telephony.flags.ap_domain_selection_enabled") public static final String BIND_DOMAIN_SELECTION_SERVICE = "android.permission.BIND_DOMAIN_SELECTION_SERVICE";
field public static final String BIND_DOMAIN_VERIFICATION_AGENT = "android.permission.BIND_DOMAIN_VERIFICATION_AGENT";
field public static final String BIND_EUICC_SERVICE = "android.permission.BIND_EUICC_SERVICE";
field public static final String BIND_EXTERNAL_STORAGE_SERVICE = "android.permission.BIND_EXTERNAL_STORAGE_SERVICE";
@@ -392,6 +393,7 @@ package android {
field @Deprecated public static final String UPDATE_TIME_ZONE_RULES = "android.permission.UPDATE_TIME_ZONE_RULES";
field public static final String UPGRADE_RUNTIME_PERMISSIONS = "android.permission.UPGRADE_RUNTIME_PERMISSIONS";
field public static final String USER_ACTIVITY = "android.permission.USER_ACTIVITY";
+ field @FlaggedApi("android.hardware.biometrics.face_background_authentication") public static final String USE_BACKGROUND_FACE_AUTHENTICATION = "android.permission.USE_BACKGROUND_FACE_AUTHENTICATION";
field public static final String USE_COLORIZED_NOTIFICATIONS = "android.permission.USE_COLORIZED_NOTIFICATIONS";
field public static final String USE_RESERVED_DISK = "android.permission.USE_RESERVED_DISK";
field public static final String UWB_PRIVILEGED = "android.permission.UWB_PRIVILEGED";
@@ -3201,6 +3203,7 @@ package android.companion.virtual {
public final class VirtualDeviceManager {
method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.companion.virtual.VirtualDeviceManager.VirtualDevice createVirtualDevice(int, @NonNull android.companion.virtual.VirtualDeviceParams);
+ method @FlaggedApi("android.companion.virtual.flags.persistent_device_id_api") @Nullable public CharSequence getDisplayNameForPersistentDeviceId(@NonNull String);
field public static final int LAUNCH_FAILURE_NO_ACTIVITY = 2; // 0x2
field public static final int LAUNCH_FAILURE_PENDING_INTENT_CANCELED = 1; // 0x1
field public static final int LAUNCH_SUCCESS = 0; // 0x0
@@ -3551,6 +3554,7 @@ package android.content {
field @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") public static final String ECM_ENHANCED_CONFIRMATION_SERVICE = "ecm_enhanced_confirmation";
field public static final String ETHERNET_SERVICE = "ethernet";
field public static final String EUICC_CARD_SERVICE = "euicc_card";
+ field @FlaggedApi("android.hardware.biometrics.face_background_authentication") public static final String FACE_SERVICE = "face";
field public static final String FONT_SERVICE = "font";
field public static final String HDMI_CONTROL_SERVICE = "hdmi_control";
field public static final String MEDIA_TRANSCODING_SERVICE = "media_transcoding";
@@ -4731,6 +4735,15 @@ package android.hardware.display {
}
+package android.hardware.face {
+
+ @FlaggedApi("android.hardware.biometrics.face_background_authentication") public class FaceManager {
+ method @FlaggedApi("android.hardware.biometrics.face_background_authentication") @RequiresPermission(android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION) public void authenticateInBackground(@Nullable java.util.concurrent.Executor, @Nullable android.hardware.biometrics.BiometricPrompt.CryptoObject, @Nullable android.os.CancellationSignal, @NonNull android.hardware.biometrics.BiometricPrompt.AuthenticationCallback);
+ method @FlaggedApi("android.hardware.biometrics.face_background_authentication") @RequiresPermission(anyOf={"android.permission.USE_BIOMETRIC_INTERNAL", android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION}) public boolean hasEnrolledTemplates();
+ }
+
+}
+
package android.hardware.hdmi {
public abstract class HdmiClient {
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index a840fc2b394f..a866a34166f7 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -186,6 +186,7 @@ package android.app {
method public void setEligibleForLegacyPermissionPrompt(boolean);
method public static void setExitTransitionTimeout(long);
method public void setLaunchActivityType(int);
+ method public void setLaunchCookie(@NonNull android.app.ActivityOptions.LaunchCookie);
method public void setLaunchTaskDisplayAreaFeatureId(int);
method public void setLaunchWindowingMode(int);
method public void setLaunchedFromBubble(boolean);
@@ -193,6 +194,10 @@ package android.app {
method public void setTaskOverlay(boolean, boolean);
}
+ public static final class ActivityOptions.LaunchCookie {
+ ctor public ActivityOptions.LaunchCookie();
+ }
+
public static interface ActivityOptions.OnAnimationFinishedListener {
method public void onAnimationFinished(long);
}
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 5318bb722b5f..b2c64756e4bf 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -4413,6 +4413,10 @@ public class ActivityManager {
* <p>The caller must hold the {@link android.Manifest.permission#PACKAGE_USAGE_STATS}
* permission to use this feature.</p>
*
+ * <p>Calling this API with the same instance of {@code listener} without
+ * unregistering with {@link #removeOnUidImportanceListener} before it will result in
+ * an {@link IllegalArgumentException}.</p>
+ *
* @throws IllegalArgumentException If the listener is already registered.
* @throws SecurityException If the caller does not hold
* {@link android.Manifest.permission#PACKAGE_USAGE_STATS}.
@@ -4438,6 +4442,10 @@ public class ActivityManager {
* all UIDs will be monitored by this listener, this will be equivalent to the
* {@link #addOnUidImportanceListener(OnUidImportanceListener, int)} in this case.
*
+ * <p>Calling this API with the same instance of {@code listener} without
+ * unregistering with {@link #removeOnUidImportanceListener} before it will result in
+ * an {@link IllegalArgumentException}.</p>
+ *
* @throws IllegalArgumentException If the listener is already registered.
* @hide
*/
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index 8af7ed1bc8d3..57fca74e7e59 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -29,6 +29,7 @@ import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.app.ExitTransitionCoordinator.ActivityExitTransitionCallbacks;
@@ -41,6 +42,7 @@ import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Rect;
import android.hardware.HardwareBuffer;
+import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -1921,6 +1923,38 @@ public class ActivityOptions extends ComponentOptions {
}
/**
+ * An opaque token to use with {@link #setLaunchCookie(LaunchCookie)}.
+ *
+ * @hide
+ */
+ @SuppressLint("UnflaggedApi")
+ @TestApi
+ public static final class LaunchCookie {
+ /** @hide */
+ public final IBinder binder = new Binder();
+
+ /** @hide */
+ @SuppressLint("UnflaggedApi")
+ @TestApi
+ public LaunchCookie() {}
+ }
+
+ /**
+ * Sets a launch cookie that can be used to track the {@link Activity} and task that are
+ * launched as a result of this option. If the launched activity is a trampoline that starts
+ * another activity immediately, the cookie will be transferred to the next activity.
+ *
+ * @param launchCookie a developer specified identifier for a specific task.
+ *
+ * @hide
+ */
+ @SuppressLint("UnflaggedApi")
+ @TestApi
+ public void setLaunchCookie(@NonNull LaunchCookie launchCookie) {
+ setLaunchCookie(launchCookie.binder);
+ }
+
+ /**
* Sets a launch cookie that can be used to track the activity and task that are launch as a
* result of this option. If the launched activity is a trampoline that starts another activity
* immediately, the cookie will be transferred to the next activity.
diff --git a/core/java/android/app/GrammaticalInflectionManager.java b/core/java/android/app/GrammaticalInflectionManager.java
index a55121aaa12c..483a6e11a42a 100644
--- a/core/java/android/app/GrammaticalInflectionManager.java
+++ b/core/java/android/app/GrammaticalInflectionManager.java
@@ -114,7 +114,7 @@ public class GrammaticalInflectionManager {
}
try {
- mService.setSystemWideGrammaticalGender(mContext.getUserId(), grammaticalGender);
+ mService.setSystemWideGrammaticalGender(grammaticalGender, mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -131,7 +131,8 @@ public class GrammaticalInflectionManager {
@Configuration.GrammaticalGender
public int getSystemGrammaticalGender() {
try {
- return mService.getSystemGrammaticalGender(mContext.getUserId());
+ return mService.getSystemGrammaticalGender(mContext.getAttributionSource(),
+ mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/app/IGrammaticalInflectionManager.aidl b/core/java/android/app/IGrammaticalInflectionManager.aidl
index 48a48416d592..86f2e9110889 100644
--- a/core/java/android/app/IGrammaticalInflectionManager.aidl
+++ b/core/java/android/app/IGrammaticalInflectionManager.aidl
@@ -1,5 +1,22 @@
+/**
+ * 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;
+import android.content.AttributionSource;
/**
* Internal interface used to control app-specific gender.
@@ -20,10 +37,10 @@ package android.app;
/**
* Sets the grammatical gender to system.
*/
- void setSystemWideGrammaticalGender(int userId, int gender);
+ void setSystemWideGrammaticalGender(int gender, int userId);
/**
* Gets the grammatical gender from system.
*/
- int getSystemGrammaticalGender(int userId);
+ int getSystemGrammaticalGender(in AttributionSource attributionSource, int userId);
}
diff --git a/core/java/android/app/IWallpaperManager.aidl b/core/java/android/app/IWallpaperManager.aidl
index d7d654672abc..5acb9b5cf9ae 100644
--- a/core/java/android/app/IWallpaperManager.aidl
+++ b/core/java/android/app/IWallpaperManager.aidl
@@ -16,6 +16,7 @@
package android.app;
+import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Bundle;
@@ -26,6 +27,8 @@ import android.app.WallpaperInfo;
import android.content.ComponentName;
import android.app.WallpaperColors;
+import java.util.List;
+
/** @hide */
interface IWallpaperManager {
@@ -39,15 +42,21 @@ interface IWallpaperManager {
* FLAG_SET_SYSTEM
* FLAG_SET_LOCK
*
- * A 'null' cropHint rectangle is explicitly permitted as a sentinel for "whatever
- * the source image's bounding rect is."
+ * 'screenOrientations' and 'crops' define how the wallpaper will be positioned for
+ * different screen orientations. If some screen orientations are missing, crops for these
+ * orientations will be added by the system.
+ *
+ * If 'screenOrientations' is null, 'crops' can be null or a singleton list. The system will
+ * fit the provided crop (or the whole image, if 'crops' is 'null') for the current device
+ * orientation, and add crops for the missing orientations.
*
* The completion callback's "onWallpaperChanged()" method is invoked when the
* new wallpaper content is ready to display.
*/
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.SET_WALLPAPER)")
ParcelFileDescriptor setWallpaper(String name, in String callingPackage,
- in Rect cropHint, boolean allowBackup, out Bundle extras, int which,
- IWallpaperManagerCallback completion, int userId);
+ in int[] screenOrientations, in List<Rect> crops, boolean allowBackup,
+ out Bundle extras, int which, IWallpaperManagerCallback completion, int userId);
/**
* Set the live wallpaper.
@@ -78,6 +87,30 @@ interface IWallpaperManager {
boolean getCropped);
/**
+ * For a given user and a list of display sizes, get a list of Rect representing the
+ * area of the current wallpaper that is displayed for each display size.
+ */
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.READ_WALLPAPER_INTERNAL)")
+ @SuppressWarnings(value={"untyped-collection"})
+ List getBitmapCrops(in List<Point> displaySizes, int which, boolean originalBitmap, int userId);
+
+ /**
+ * Return how a bitmap of a given size would be cropped for a given list of display sizes when
+ * set with the given suggested crops.
+ * @hide
+ */
+ @SuppressWarnings(value={"untyped-collection"})
+ List getFutureBitmapCrops(in Point bitmapSize, in List<Point> displaySizes,
+ in int[] screenOrientations, in List<Rect> crops);
+
+ /**
+ * Return how a bitmap of a given size would be cropped when set with the given suggested crops.
+ * @hide
+ */
+ @SuppressWarnings(value={"untyped-collection"})
+ Rect getBitmapCrop(in Point bitmapSize, in int[] screenOrientations, in List<Rect> crops);
+
+ /**
* Retrieve the given user's current wallpaper ID of the given kind.
*/
int getWallpaperIdForUser(int which, int userId);
@@ -245,11 +278,4 @@ interface IWallpaperManager {
* @hide
*/
boolean isStaticWallpaper(int which);
-
- /**
- * Temporary method for project b/270726737.
- * Return true if the wallpaper supports different crops for different display dimensions.
- * @hide
- */
- boolean isMultiCropEnabled();
}
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index 62db90f79091..63f37f150d33 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -18,11 +18,14 @@ package android.app;
import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE;
import static android.Manifest.permission.READ_WALLPAPER_INTERNAL;
+import static android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
+import static com.android.window.flags.Flags.FLAG_MULTI_CROP;
import static com.android.window.flags.Flags.multiCrop;
+import android.annotation.FlaggedApi;
import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.annotation.NonNull;
@@ -58,6 +61,7 @@ import android.graphics.ImageDecoder;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PixelFormat;
+import android.graphics.Point;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
@@ -84,6 +88,7 @@ import android.util.ArraySet;
import android.util.Log;
import android.util.MathUtils;
import android.util.Pair;
+import android.util.SparseArray;
import android.view.Display;
import android.view.WindowManagerGlobal;
@@ -104,6 +109,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -289,6 +295,79 @@ public class WallpaperManager {
public static final String EXTRA_FROM_FOREGROUND_APP =
"android.service.wallpaper.extra.FROM_FOREGROUND_APP";
+ /**
+ * The different screen orientations. {@link #getOrientation} provides their exact definition.
+ * This is only used internally by the framework and the WallpaperBackupAgent.
+ * @hide
+ */
+ @IntDef(value = {
+ ORIENTATION_UNKNOWN,
+ PORTRAIT,
+ LANDSCAPE,
+ SQUARE_PORTRAIT,
+ SQUARE_LANDSCAPE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ScreenOrientation {}
+
+ /**
+ * @hide
+ */
+ public static final int ORIENTATION_UNKNOWN = -1;
+
+ /**
+ * Portrait orientation of most screens
+ * @hide
+ */
+ public static final int PORTRAIT = 0;
+
+ /**
+ * Landscape orientation of most screens
+ * @hide
+ */
+ public static final int LANDSCAPE = 1;
+
+ /**
+ * Portrait orientation with similar width and height (e.g. the inner screen of a foldable)
+ * @hide
+ */
+ public static final int SQUARE_PORTRAIT = 2;
+
+ /**
+ * Landscape orientation with similar width and height (e.g. the inner screen of a foldable)
+ * @hide
+ */
+ public static final int SQUARE_LANDSCAPE = 3;
+
+ /**
+ * Converts a (width, height) screen size to a {@link ScreenOrientation}.
+ * @param screenSize the dimensions of a screen
+ * @return the corresponding {@link ScreenOrientation}.
+ * @hide
+ */
+ public static @ScreenOrientation int getOrientation(Point screenSize) {
+ float ratio = ((float) screenSize.x) / screenSize.y;
+ // ratios between 3/4 and 4/3 are considered square
+ return ratio >= 4 / 3f ? LANDSCAPE
+ : ratio > 1f ? SQUARE_LANDSCAPE
+ : ratio > 3 / 4f ? SQUARE_PORTRAIT
+ : PORTRAIT;
+ }
+
+ /**
+ * Get the 90° rotation of a given orientation
+ * @hide
+ */
+ public static @ScreenOrientation int getRotatedOrientation(@ScreenOrientation int orientation) {
+ switch (orientation) {
+ case PORTRAIT: return LANDSCAPE;
+ case LANDSCAPE: return PORTRAIT;
+ case SQUARE_PORTRAIT: return SQUARE_LANDSCAPE;
+ case SQUARE_LANDSCAPE: return SQUARE_PORTRAIT;
+ default: return ORIENTATION_UNKNOWN;
+ }
+ }
+
// flags for which kind of wallpaper to act on
/** @hide */
@@ -867,15 +946,8 @@ public class WallpaperManager {
* @hide
*/
public static boolean isMultiCropEnabled() {
- if (sGlobals == null) {
- sIsMultiCropEnabled = multiCrop();
- }
if (sIsMultiCropEnabled == null) {
- try {
- sIsMultiCropEnabled = sGlobals.mService.isMultiCropEnabled();
- } catch (RemoteException e) {
- e.rethrowFromSystemServer();
- }
+ sIsMultiCropEnabled = multiCrop();
}
return sIsMultiCropEnabled;
}
@@ -1502,6 +1574,99 @@ public class WallpaperManager {
}
/**
+ * For the current user, given a list of display sizes, return a list of rectangles representing
+ * the area of the current wallpaper that would be shown for each of these sizes.
+ *
+ * @param displaySizes the display sizes.
+ * @param which wallpaper type. Must be either {@link #FLAG_SYSTEM} or {@link #FLAG_LOCK}.
+ * @param originalBitmap If true, return areas relative to the original bitmap.
+ * If false, return areas relative to the cropped bitmap.
+ * @return A List of Rect where the Rect is within the cropped/original bitmap, and corresponds
+ * to what is displayed. The Rect may have a larger width/height ratio than the screen
+ * due to parallax. Return {@code null} if the wallpaper is not an ImageWallpaper.
+ * Also return {@code null} when called with which={@link #FLAG_LOCK} if there is a
+ * shared home + lock wallpaper.
+ * @hide
+ */
+ @FlaggedApi(FLAG_MULTI_CROP)
+ @RequiresPermission(READ_WALLPAPER_INTERNAL)
+ @Nullable
+ public List<Rect> getBitmapCrops(@NonNull List<Point> displaySizes,
+ @SetWallpaperFlags int which, boolean originalBitmap) {
+ checkExactlyOneWallpaperFlagSet(which);
+ try {
+ return sGlobals.mService.getBitmapCrops(displaySizes, which, originalBitmap,
+ mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * For preview purposes.
+ * Return how a bitmap of a given size would be cropped for a given list of display sizes, if
+ * it was set as wallpaper via {@link #setBitmapWithCrops(Bitmap, Map, boolean, int)} or
+ * {@link #setStreamWithCrops(InputStream, Map, boolean, int)}.
+ *
+ * @return A List of Rect where the Rect is within the bitmap, and corresponds to what is
+ * displayed for each display size. The Rect may have a larger width/height ratio than
+ * the display due to parallax.
+ * @hide
+ */
+ @FlaggedApi(FLAG_MULTI_CROP)
+ @Nullable
+ public List<Rect> getBitmapCrops(@NonNull Point bitmapSize, @NonNull List<Point> displaySizes,
+ @Nullable Map<Point, Rect> cropHints) {
+ try {
+ if (cropHints == null) cropHints = Map.of();
+ Set<Map.Entry<Point, Rect>> entries = cropHints.entrySet();
+ int[] screenOrientations = entries.stream().mapToInt(entry ->
+ getOrientation(entry.getKey())).toArray();
+ List<Rect> crops = entries.stream().map(Map.Entry::getValue).toList();
+ return sGlobals.mService.getFutureBitmapCrops(bitmapSize, displaySizes,
+ screenOrientations, crops);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * For preview purposes.
+ * Compute the wallpaper colors of the given bitmap, if it was set as wallpaper via
+ * {@link #setBitmapWithCrops(Bitmap, Map, boolean, int)} or
+ * {@link #setStreamWithCrops(InputStream, Map, boolean, int)}.
+ * Return {@code null} if an error occurred and the colors could not be computed.
+ *
+ * @hide
+ */
+ @FlaggedApi(FLAG_MULTI_CROP)
+ @RequiresPermission(SET_WALLPAPER_DIM_AMOUNT)
+ @Nullable
+ public WallpaperColors getWallpaperColors(@NonNull Bitmap bitmap,
+ @Nullable Map<Point, Rect> cropHints) {
+ if (sGlobals.mService == null) {
+ Log.w(TAG, "WallpaperService not running");
+ throw new RuntimeException(new DeadSystemException());
+ }
+ try {
+ if (cropHints == null) cropHints = Map.of();
+ Set<Map.Entry<Point, Rect>> entries = cropHints.entrySet();
+ int[] screenOrientations = entries.stream().mapToInt(entry ->
+ getOrientation(entry.getKey())).toArray();
+ List<Rect> crops = entries.stream().map(Map.Entry::getValue).toList();
+ Point bitmapSize = new Point(bitmap.getWidth(), bitmap.getHeight());
+ Rect crop = sGlobals.mService.getBitmapCrop(bitmapSize, screenOrientations, crops);
+ float dimAmount = getWallpaperDimAmount();
+ Bitmap croppedBitmap = Bitmap.createBitmap(
+ bitmap, crop.left, crop.top, crop.width(), crop.height());
+ WallpaperColors result = WallpaperColors.fromBitmap(croppedBitmap, dimAmount);
+ return result;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* <strong> Important note: </strong>
* <ul>
* <li>Up to version S, this method requires the
@@ -1971,7 +2136,7 @@ public class WallpaperManager {
/* Set the wallpaper to the default values */
ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(
"res:" + resources.getResourceName(resid),
- mContext.getOpPackageName(), null, false, result, which, completion,
+ mContext.getOpPackageName(), null, null, false, result, which, completion,
mContext.getUserId());
if (fd != null) {
FileOutputStream fos = null;
@@ -2089,6 +2254,11 @@ public class WallpaperManager {
public int setBitmap(Bitmap fullImage, Rect visibleCropHint,
boolean allowBackup, @SetWallpaperFlags int which, int userId)
throws IOException {
+ if (multiCrop()) {
+ SparseArray<Rect> cropMap = new SparseArray<>();
+ if (visibleCropHint != null) cropMap.put(ORIENTATION_UNKNOWN, visibleCropHint);
+ return setBitmapWithCrops(fullImage, cropMap, allowBackup, which, userId);
+ }
validateRect(visibleCropHint);
if (sGlobals.mService == null) {
Log.w(TAG, "WallpaperService not running");
@@ -2096,9 +2266,69 @@ public class WallpaperManager {
}
final Bundle result = new Bundle();
final WallpaperSetCompletion completion = new WallpaperSetCompletion();
+ final List<Rect> crops = visibleCropHint == null ? null : List.of(visibleCropHint);
try {
ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null,
- mContext.getOpPackageName(), visibleCropHint, allowBackup,
+ mContext.getOpPackageName(), null, crops, allowBackup, result, which,
+ completion, userId);
+ if (fd != null) {
+ FileOutputStream fos = null;
+ try {
+ fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd);
+ fullImage.compress(Bitmap.CompressFormat.PNG, 90, fos);
+ fos.close();
+ completion.waitForCompletion();
+ } finally {
+ IoUtils.closeQuietly(fos);
+ }
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ return result.getInt(EXTRA_NEW_WALLPAPER_ID, 0);
+ }
+
+ /**
+ * Version of setBitmap that defines how the wallpaper will be positioned for different
+ * display sizes.
+ * Requires permission {@link android.Manifest.permission#SET_WALLPAPER}.
+ * @param cropHints map from screen dimensions to a sub-region of the image to display for those
+ * dimensions. The {@code Rect} sub-region may have a larger width/height ratio
+ * than the screen dimensions to apply a horizontal parallax effect. If the
+ * map is empty or some entries are missing, the system will apply a default
+ * strategy to position the wallpaper for any unspecified screen dimensions.
+ * @hide
+ */
+ @FlaggedApi(FLAG_MULTI_CROP)
+ @RequiresPermission(android.Manifest.permission.SET_WALLPAPER)
+ public int setBitmapWithCrops(@Nullable Bitmap fullImage, @NonNull Map<Point, Rect> cropHints,
+ boolean allowBackup, @SetWallpaperFlags int which) throws IOException {
+ SparseArray<Rect> crops = new SparseArray<>();
+ cropHints.forEach((k, v) -> crops.put(getOrientation(k), v));
+ return setBitmapWithCrops(fullImage, crops, allowBackup, which, mContext.getUserId());
+ }
+
+ @RequiresPermission(android.Manifest.permission.SET_WALLPAPER)
+ private int setBitmapWithCrops(@Nullable Bitmap fullImage, @NonNull SparseArray<Rect> cropHints,
+ boolean allowBackup, @SetWallpaperFlags int which, int userId) throws IOException {
+ if (sGlobals.mService == null) {
+ Log.w(TAG, "WallpaperService not running");
+ throw new RuntimeException(new DeadSystemException());
+ }
+ int size = cropHints.size();
+ int[] screenOrientations = new int[size];
+ List<Rect> crops = new ArrayList<>(size);
+ for (int i = 0; i < size; i++) {
+ screenOrientations[i] = cropHints.keyAt(i);
+ Rect cropHint = cropHints.valueAt(i);
+ validateRect(cropHint);
+ crops.add(cropHint);
+ }
+ final Bundle result = new Bundle();
+ final WallpaperSetCompletion completion = new WallpaperSetCompletion();
+ try {
+ ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null,
+ mContext.getOpPackageName(), screenOrientations, crops, allowBackup,
result, which, completion, userId);
if (fd != null) {
FileOutputStream fos = null;
@@ -2214,6 +2444,11 @@ public class WallpaperManager {
public int setStream(InputStream bitmapData, Rect visibleCropHint,
boolean allowBackup, @SetWallpaperFlags int which)
throws IOException {
+ if (multiCrop()) {
+ SparseArray<Rect> cropMap = new SparseArray<>();
+ if (visibleCropHint != null) cropMap.put(ORIENTATION_UNKNOWN, visibleCropHint);
+ return setStreamWithCrops(bitmapData, cropMap, allowBackup, which);
+ }
validateRect(visibleCropHint);
if (sGlobals.mService == null) {
Log.w(TAG, "WallpaperService not running");
@@ -2221,10 +2456,11 @@ public class WallpaperManager {
}
final Bundle result = new Bundle();
final WallpaperSetCompletion completion = new WallpaperSetCompletion();
+ final List<Rect> crops = visibleCropHint == null ? null : List.of(visibleCropHint);
try {
ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null,
- mContext.getOpPackageName(), visibleCropHint, allowBackup,
- result, which, completion, mContext.getUserId());
+ mContext.getOpPackageName(), null, crops, allowBackup, result, which,
+ completion, mContext.getUserId());
if (fd != null) {
FileOutputStream fos = null;
try {
@@ -2244,6 +2480,75 @@ public class WallpaperManager {
}
/**
+ * Version of setStream that defines how the wallpaper will be positioned for different
+ * display sizes.
+ * Requires permission {@link android.Manifest.permission#SET_WALLPAPER}.
+ * @param cropHints map from screen dimensions to a sub-region of the image to display for those
+ * dimensions. The {@code Rect} sub-region may have a larger width/height ratio
+ * than the screen dimensions to apply a horizontal parallax effect. If the
+ * map is empty or some entries are missing, the system will apply a default
+ * strategy to position the wallpaper for any unspecified screen dimensions.
+ * @hide
+ */
+ @FlaggedApi(FLAG_MULTI_CROP)
+ @RequiresPermission(android.Manifest.permission.SET_WALLPAPER)
+ public int setStreamWithCrops(InputStream bitmapData, @NonNull Map<Point, Rect> cropHints,
+ boolean allowBackup, @SetWallpaperFlags int which) throws IOException {
+ SparseArray<Rect> crops = new SparseArray<>();
+ cropHints.forEach((k, v) -> crops.put(getOrientation(k), v));
+ return setStreamWithCrops(bitmapData, crops, allowBackup, which);
+ }
+
+ /**
+ * Similar to {@link #setStreamWithCrops(InputStream, Map, boolean, int)}, but using
+ * {@link ScreenOrientation} as keys of the cropHints map. Used for backup & restore, since
+ * WallpaperBackupAgent stores orientations rather than the exact display size.
+ * Requires permission {@link android.Manifest.permission#SET_WALLPAPER}.
+ * @param cropHints map from {@link ScreenOrientation} to a sub-region of the image to display
+ * for that screen orientation.
+ * @hide
+ */
+ @FlaggedApi(FLAG_MULTI_CROP)
+ @RequiresPermission(android.Manifest.permission.SET_WALLPAPER)
+ public int setStreamWithCrops(InputStream bitmapData, @NonNull SparseArray<Rect> cropHints,
+ boolean allowBackup, @SetWallpaperFlags int which) throws IOException {
+ if (sGlobals.mService == null) {
+ Log.w(TAG, "WallpaperService not running");
+ throw new RuntimeException(new DeadSystemException());
+ }
+ int size = cropHints.size();
+ int[] screenOrientations = new int[size];
+ List<Rect> crops = new ArrayList<>(size);
+ for (int i = 0; i < size; i++) {
+ screenOrientations[i] = cropHints.keyAt(i);
+ Rect cropHint = cropHints.valueAt(i);
+ validateRect(cropHint);
+ crops.add(cropHint);
+ }
+ final Bundle result = new Bundle();
+ final WallpaperSetCompletion completion = new WallpaperSetCompletion();
+ try {
+ ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null,
+ mContext.getOpPackageName(), screenOrientations, crops, allowBackup,
+ result, which, completion, mContext.getUserId());
+ if (fd != null) {
+ FileOutputStream fos = null;
+ try {
+ fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd);
+ copyStreamToWallpaperFile(bitmapData, fos);
+ fos.close();
+ completion.waitForCompletion();
+ } finally {
+ IoUtils.closeQuietly(fos);
+ }
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ return result.getInt(EXTRA_NEW_WALLPAPER_ID, 0);
+ }
+
+ /**
* Return whether any users are currently set to use the wallpaper
* with the given resource ID. That is, their wallpaper has been
* set through {@link #setResource(int)} with the same resource id.
@@ -2499,7 +2804,7 @@ public class WallpaperManager {
* @hide
*/
@SystemApi
- @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT)
+ @RequiresPermission(SET_WALLPAPER_DIM_AMOUNT)
public void setWallpaperDimAmount(@FloatRange (from = 0f, to = 1f) float dimAmount) {
if (sGlobals.mService == null) {
Log.w(TAG, "WallpaperService not running");
@@ -2519,7 +2824,7 @@ public class WallpaperManager {
* @hide
*/
@SystemApi
- @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT)
+ @RequiresPermission(SET_WALLPAPER_DIM_AMOUNT)
public @FloatRange (from = 0f, to = 1f) float getWallpaperDimAmount() {
if (sGlobals.mService == null) {
Log.w(TAG, "WallpaperService not running");
diff --git a/core/java/android/app/grammatical_inflection_manager.aconfig b/core/java/android/app/grammatical_inflection_manager.aconfig
index 989ce61337a3..68d12ba75560 100644
--- a/core/java/android/app/grammatical_inflection_manager.aconfig
+++ b/core/java/android/app/grammatical_inflection_manager.aconfig
@@ -2,7 +2,7 @@ package: "android.app"
flag {
name: "system_terms_of_address_enabled"
- namespace: "grammatical_gender"
+ namespace: "globalintl"
description: "Feature flag for System Terms of Address"
bug: "297798866"
}
diff --git a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl
index 04933126f294..325aa28fde08 100644
--- a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl
+++ b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl
@@ -78,6 +78,11 @@ interface IVirtualDeviceManager {
int getDeviceIdForDisplayId(int displayId);
/**
+ * Returns the display name corresponding to the given persistent device ID, if any.
+ */
+ CharSequence getDisplayNameForPersistentDeviceId(in String persistentDeviceId);
+
+ /**
* Checks whether the passed {@code deviceId} is a valid virtual device ID or not.
* {@link VirtualDeviceManager#DEVICE_ID_DEFAULT} is not valid as it is the ID of the default
* device which is not a virtual device. {@code deviceId} must correspond to a virtual device
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index a4cada28999e..90d251b04f67 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -361,6 +361,34 @@ public final class VirtualDeviceManager {
}
/**
+ * Get the display name for a given persistent device ID.
+ *
+ * <p>This will work even if currently there is no valid virtual device with the given
+ * persistent ID, as long as such a device has been created or can be created.</p>
+ *
+ * @return the display name associated with the given persistent device ID, or {@code null} if
+ * the persistent ID is invalid or does not correspond to a virtual device.
+ *
+ * @hide
+ */
+ // TODO(b/315481938): Link @see VirtualDevice#getPersistentDeviceId()
+ @FlaggedApi(Flags.FLAG_PERSISTENT_DEVICE_ID_API)
+ @SystemApi
+ @Nullable
+ public CharSequence getDisplayNameForPersistentDeviceId(@NonNull String persistentDeviceId) {
+ if (mService == null) {
+ Log.w(TAG, "Failed to retrieve virtual devices; no virtual device manager service.");
+ return null;
+ }
+ try {
+ return mService.getDisplayNameForPersistentDeviceId(
+ Objects.requireNonNull(persistentDeviceId));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Checks whether the passed {@code deviceId} is a valid virtual device ID or not.
* {@link Context#DEVICE_ID_DEFAULT} is not valid as it is the ID of the default
* device which is not a virtual device. {@code deviceId} must correspond to a virtual device
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 249c0e434e78..67a3627a399f 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -5079,6 +5079,8 @@ public abstract class Context {
* @see #getSystemService
* @see android.hardware.face.FaceManager
*/
+ @FlaggedApi(android.hardware.biometrics.Flags.FLAG_FACE_BACKGROUND_AUTHENTICATION)
+ @SystemApi
public static final String FACE_SERVICE = "face";
/**
diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig
index 3ba8be4cc2ab..8165d44b251a 100644
--- a/core/java/android/hardware/biometrics/flags.aconfig
+++ b/core/java/android/hardware/biometrics/flags.aconfig
@@ -28,3 +28,10 @@ flag {
bug: "302735104"
}
+flag {
+ name: "face_background_authentication"
+ namespace: "biometrics_framework"
+ description: "Feature flag for allowing face background authentication with USE_BACKGROUND_FACE_AUTHENTICATION."
+ bug: "318584190"
+}
+
diff --git a/core/java/android/hardware/face/FaceManager.java b/core/java/android/hardware/face/FaceManager.java
index 02304b5ba4f3..bae5e7f83569 100644
--- a/core/java/android/hardware/face/FaceManager.java
+++ b/core/java/android/hardware/face/FaceManager.java
@@ -18,18 +18,23 @@ package android.hardware.face;
import static android.Manifest.permission.INTERACT_ACROSS_USERS;
import static android.Manifest.permission.MANAGE_BIOMETRIC;
+import static android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION;
import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL;
import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_LOCKOUT_NONE;
+import static android.hardware.biometrics.Flags.FLAG_FACE_BACKGROUND_AUTHENTICATION;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.content.Context;
import android.content.pm.PackageManager;
import android.hardware.biometrics.BiometricAuthenticator;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricFaceConstants;
+import android.hardware.biometrics.BiometricPrompt;
import android.hardware.biometrics.BiometricStateListener;
import android.hardware.biometrics.CryptoObject;
import android.hardware.biometrics.IBiometricServiceLockoutResetCallback;
@@ -37,9 +42,9 @@ import android.os.Binder;
import android.os.CancellationSignal;
import android.os.CancellationSignal.OnCancelListener;
import android.os.Handler;
+import android.os.HandlerExecutor;
import android.os.IBinder;
import android.os.IRemoteCallback;
-import android.os.Looper;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.Trace;
@@ -49,15 +54,21 @@ import android.util.Slog;
import android.view.Surface;
import com.android.internal.R;
-import com.android.internal.os.SomeArgs;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.Executor;
/**
* A class that coordinates access to the face authentication hardware.
+ *
+ * <p>Please use {@link BiometricPrompt} for face authentication unless the experience must be
+ * customized for unique system-level utilities, like the lock screen or ambient background usage.
+ *
* @hide
*/
+@FlaggedApi(FLAG_FACE_BACKGROUND_AUTHENTICATION)
+@SystemApi
@SystemService(Context.FACE_SERVICE)
public class FaceManager implements BiometricAuthenticator, BiometricFaceConstants {
@@ -88,81 +99,76 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan
@Nullable private GenerateChallengeCallback mGenerateChallengeCallback;
private CryptoObject mCryptoObject;
private Face mRemovalFace;
- private Handler mHandler;
+ private Executor mExecutor;
private List<FaceSensorPropertiesInternal> mProps = new ArrayList<>();
private final IFaceServiceReceiver mServiceReceiver = new IFaceServiceReceiver.Stub() {
@Override // binder call
public void onEnrollResult(Face face, int remaining) {
- mHandler.obtainMessage(MSG_ENROLL_RESULT, remaining, 0, face).sendToTarget();
+ mExecutor.execute(() -> sendEnrollResult(face, remaining));
}
@Override // binder call
public void onAcquired(int acquireInfo, int vendorCode) {
- mHandler.obtainMessage(MSG_ACQUIRED, acquireInfo, vendorCode).sendToTarget();
+ mExecutor.execute(() -> sendAcquiredResult(acquireInfo, vendorCode));
}
@Override // binder call
public void onAuthenticationSucceeded(Face face, int userId, boolean isStrongBiometric) {
- mHandler.obtainMessage(MSG_AUTHENTICATION_SUCCEEDED, userId,
- isStrongBiometric ? 1 : 0, face).sendToTarget();
+ mExecutor.execute(() -> sendAuthenticatedSucceeded(face, userId, isStrongBiometric));
}
@Override // binder call
public void onFaceDetected(int sensorId, int userId, boolean isStrongBiometric) {
- mHandler.obtainMessage(MSG_FACE_DETECTED, sensorId, userId, isStrongBiometric)
- .sendToTarget();
+ mExecutor.execute(() -> sendFaceDetected(sensorId, userId, isStrongBiometric));
}
@Override // binder call
public void onAuthenticationFailed() {
- mHandler.obtainMessage(MSG_AUTHENTICATION_FAILED).sendToTarget();
+ mExecutor.execute(() -> sendAuthenticatedFailed());
}
@Override // binder call
public void onError(int error, int vendorCode) {
- mHandler.obtainMessage(MSG_ERROR, error, vendorCode).sendToTarget();
+ mExecutor.execute(() -> sendErrorResult(error, vendorCode));
}
@Override // binder call
public void onRemoved(Face face, int remaining) {
- mHandler.obtainMessage(MSG_REMOVED, remaining, 0, face).sendToTarget();
- if (remaining == 0) {
- Settings.Secure.putIntForUser(mContext.getContentResolver(),
- Settings.Secure.FACE_UNLOCK_RE_ENROLL, 0,
- UserHandle.USER_CURRENT);
- }
+ mExecutor.execute(() -> {
+ sendRemovedResult(face, remaining);
+ if (remaining == 0) {
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
+ Settings.Secure.FACE_UNLOCK_RE_ENROLL, 0,
+ UserHandle.USER_CURRENT);
+ }
+ });
}
@Override
public void onFeatureSet(boolean success, int feature) {
- mHandler.obtainMessage(MSG_SET_FEATURE_COMPLETED, feature, 0, success).sendToTarget();
+ mExecutor.execute(() -> sendSetFeatureCompleted(success, feature));
}
@Override
public void onFeatureGet(boolean success, int[] features, boolean[] featureState) {
- SomeArgs args = SomeArgs.obtain();
- args.arg1 = success;
- args.arg2 = features;
- args.arg3 = featureState;
- mHandler.obtainMessage(MSG_GET_FEATURE_COMPLETED, args).sendToTarget();
+ mExecutor.execute(() -> sendGetFeatureCompleted(success, features, featureState));
}
@Override
public void onChallengeGenerated(int sensorId, int userId, long challenge) {
- mHandler.obtainMessage(MSG_CHALLENGE_GENERATED, sensorId, userId, challenge)
- .sendToTarget();
+ mExecutor.execute(() -> sendChallengeGenerated(sensorId, userId, challenge));
}
@Override
public void onAuthenticationFrame(FaceAuthenticationFrame frame) {
- mHandler.obtainMessage(MSG_AUTHENTICATION_FRAME, frame).sendToTarget();
+ mExecutor.execute(() -> sendAuthenticationFrame(frame));
}
@Override
public void onEnrollmentFrame(FaceEnrollFrame frame) {
- mHandler.obtainMessage(MSG_ENROLLMENT_FRAME, frame).sendToTarget();
+ mExecutor.execute(() -> sendEnrollmentFrame(frame));
}
};
@@ -175,7 +181,7 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan
if (mService == null) {
Slog.v(TAG, "FaceAuthenticationManagerService was null");
}
- mHandler = new MyHandler(context);
+ mExecutor = context.getMainExecutor();
if (context.checkCallingOrSelfPermission(USE_BIOMETRIC_INTERNAL)
== PackageManager.PERMISSION_GRANTED) {
addAuthenticatorsRegisteredCallback(new IFaceAuthenticatorsRegisteredCallback.Stub() {
@@ -189,18 +195,16 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan
}
/**
- * Use the provided handler thread for events.
+ * Returns an {@link Executor} for the given {@link Handler} or the main {@link Executor} if
+ * {@code handler} is {@code null}.
*/
- private void useHandler(Handler handler) {
- if (handler != null) {
- mHandler = new MyHandler(handler.getLooper());
- } else if (mHandler.getLooper() != mContext.getMainLooper()) {
- mHandler = new MyHandler(mContext.getMainLooper());
- }
+ private @NonNull Executor createExecutorForHandlerIfNeeded(@Nullable Handler handler) {
+ return handler != null ? new HandlerExecutor(handler) : mContext.getMainExecutor();
}
/**
* @deprecated use {@link #authenticate(CryptoObject, CancellationSignal, AuthenticationCallback, Handler, FaceAuthenticateOptions)}.
+ * @hide
*/
@Deprecated
@RequiresPermission(USE_BIOMETRIC_INTERNAL)
@@ -212,17 +216,22 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan
}
/**
- * Request authentication. This call operates the face recognition hardware and starts capturing images.
+ * Request authentication.
+ *
+ * <p>This call operates the face recognition hardware and starts capturing images.
* It terminates when
* {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)} or
* {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult)} is called, at
* which point the object is no longer valid. The operation can be canceled by using the
- * provided cancel object.
+ * provided {@code cancel} object.
*
- * @param crypto object associated with the call or null if none required
- * @param cancel an object that can be used to cancel authentication
+ * @param crypto the cryptographic operations to use for authentication or {@code null} if
+ * none required
+ * @param cancel an object that can be used to cancel authentication or {@code null} if not
+ * needed
* @param callback an object to receive authentication events
- * @param handler an optional handler to handle callback events
+ * @param handler an optional handler to handle callback events or {@code null} to obtain main
+ * {@link Executor} from {@link Context}
* @param options additional options to customize this request
* @throws IllegalArgumentException if the crypto operation is not supported or is not backed
* by
@@ -235,6 +244,14 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan
public void authenticate(@Nullable CryptoObject crypto, @Nullable CancellationSignal cancel,
@NonNull AuthenticationCallback callback, @Nullable Handler handler,
@NonNull FaceAuthenticateOptions options) {
+ authenticate(crypto, cancel, callback, createExecutorForHandlerIfNeeded(handler),
+ options, false /* allowBackgroundAuthentication */);
+ }
+
+ @RequiresPermission(anyOf = {USE_BIOMETRIC_INTERNAL, USE_BACKGROUND_FACE_AUTHENTICATION})
+ private void authenticate(@Nullable CryptoObject crypto, @Nullable CancellationSignal cancel,
+ @NonNull AuthenticationCallback callback, @NonNull Executor executor,
+ @NonNull FaceAuthenticateOptions options, boolean allowBackgroundAuthentication) {
if (callback == null) {
throw new IllegalArgumentException("Must supply an authentication callback");
}
@@ -249,13 +266,15 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan
if (mService != null) {
try {
- useHandler(handler);
+ mExecutor = executor;
mAuthenticationCallback = callback;
mCryptoObject = crypto;
final long operationId = crypto != null ? crypto.getOpId() : 0;
Trace.beginSection("FaceManager#authenticate");
- final long authId = mService.authenticate(
- mToken, operationId, mServiceReceiver, options);
+ final long authId = allowBackgroundAuthentication
+ ? mService.authenticateInBackground(
+ mToken, operationId, mServiceReceiver, options)
+ : mService.authenticate(mToken, operationId, mServiceReceiver, options);
if (cancel != null) {
cancel.setOnCancelListener(new OnAuthenticationCancelListener(authId));
}
@@ -273,6 +292,67 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan
}
/**
+ * Request background face authentication.
+ *
+ * <p>This call operates the face recognition hardware and starts capturing images.
+ * It terminates when
+ * {@link BiometricPrompt.AuthenticationCallback#onAuthenticationError(int, CharSequence)} or
+ * {@link BiometricPrompt.AuthenticationCallback#onAuthenticationSucceeded(
+ * BiometricPrompt.AuthenticationResult)} is called, at which point the object is no longer
+ * valid. The operation can be canceled by using the provided cancel object.
+ *
+ * <p>See {@link BiometricPrompt#authenticate} for more details. Please use
+ * {@link BiometricPrompt} for face authentication unless the experience must be customized for
+ * unique system-level utilities, like the lock screen or ambient background usage.
+ *
+ * @param executor the specified {@link Executor} to handle callback events; if {@code null},
+ * the callback will be executed on the main {@link Executor}.
+ * @param crypto the cryptographic operations to use for authentication or {@code null} if
+ * none required.
+ * @param cancel an object that can be used to cancel authentication or {@code null} if not
+ * needed.
+ * @param callback an object to receive authentication events.
+ * @throws IllegalArgumentException if the crypto operation is not supported or is not backed
+ * by
+ * <a href="{@docRoot}training/articles/keystore.html">Android
+ * Keystore facility</a>.
+ * @hide
+ */
+ @RequiresPermission(USE_BACKGROUND_FACE_AUTHENTICATION)
+ @FlaggedApi(FLAG_FACE_BACKGROUND_AUTHENTICATION)
+ @SystemApi
+ public void authenticateInBackground(@Nullable Executor executor,
+ @Nullable BiometricPrompt.CryptoObject crypto, @Nullable CancellationSignal cancel,
+ @NonNull BiometricPrompt.AuthenticationCallback callback) {
+ authenticate(crypto, cancel, new AuthenticationCallback() {
+ @Override
+ public void onAuthenticationError(int errorCode, CharSequence errString) {
+ callback.onAuthenticationError(errorCode, errString);
+ }
+
+ @Override
+ public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
+ callback.onAuthenticationHelp(helpCode, helpString);
+ }
+
+ @Override
+ public void onAuthenticationSucceeded(AuthenticationResult result) {
+ callback.onAuthenticationSucceeded(
+ new BiometricPrompt.AuthenticationResult(
+ crypto,
+ BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC));
+ }
+
+ @Override
+ public void onAuthenticationFailed() {
+ callback.onAuthenticationFailed();
+ }
+ }, executor == null ? mContext.getMainExecutor() : executor,
+ new FaceAuthenticateOptions.Builder().build(),
+ true /* allowBackgroundAuthentication */);
+ }
+
+ /**
* Uses the face hardware to detect for the presence of a face, without giving details about
* accept/reject/lockout.
* @hide
@@ -628,12 +708,14 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan
}
/**
- * Determine if there is a face enrolled.
+ * Determine if there are enrolled {@link Face} templates.
*
- * @return true if a face is enrolled, false otherwise
+ * @return {@code true} if there are enrolled {@link Face} templates, {@code false} otherwise
* @hide
*/
- @RequiresPermission(USE_BIOMETRIC_INTERNAL)
+ @RequiresPermission(anyOf = {USE_BIOMETRIC_INTERNAL, USE_BACKGROUND_FACE_AUTHENTICATION})
+ @FlaggedApi(FLAG_FACE_BACKGROUND_AUTHENTICATION)
+ @SystemApi
public boolean hasEnrolledTemplates() {
return hasEnrolledTemplates(UserHandle.myUserId());
}
@@ -798,7 +880,7 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan
PowerManager.PARTIAL_WAKE_LOCK,
"faceLockoutResetCallback");
wakeLock.acquire();
- mHandler.post(() -> {
+ mExecutor.execute(() -> {
try {
callback.onLockoutReset(sensorId);
} finally {
@@ -1268,70 +1350,6 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan
}
}
- private class MyHandler extends Handler {
- private MyHandler(Context context) {
- super(context.getMainLooper());
- }
-
- private MyHandler(Looper looper) {
- super(looper);
- }
-
- @Override
- public void handleMessage(android.os.Message msg) {
- Trace.beginSection("FaceManager#handleMessage: " + Integer.toString(msg.what));
- switch (msg.what) {
- case MSG_ENROLL_RESULT:
- sendEnrollResult((Face) msg.obj, msg.arg1 /* remaining */);
- break;
- case MSG_ACQUIRED:
- sendAcquiredResult(msg.arg1 /* acquire info */, msg.arg2 /* vendorCode */);
- break;
- case MSG_AUTHENTICATION_SUCCEEDED:
- sendAuthenticatedSucceeded((Face) msg.obj, msg.arg1 /* userId */,
- msg.arg2 == 1 /* isStrongBiometric */);
- break;
- case MSG_AUTHENTICATION_FAILED:
- sendAuthenticatedFailed();
- break;
- case MSG_ERROR:
- sendErrorResult(msg.arg1 /* errMsgId */, msg.arg2 /* vendorCode */);
- break;
- case MSG_REMOVED:
- sendRemovedResult((Face) msg.obj, msg.arg1 /* remaining */);
- break;
- case MSG_SET_FEATURE_COMPLETED:
- sendSetFeatureCompleted((boolean) msg.obj /* success */,
- msg.arg1 /* feature */);
- break;
- case MSG_GET_FEATURE_COMPLETED:
- SomeArgs args = (SomeArgs) msg.obj;
- sendGetFeatureCompleted((boolean) args.arg1 /* success */,
- (int[]) args.arg2 /* features */,
- (boolean[]) args.arg3 /* featureState */);
- args.recycle();
- break;
- case MSG_CHALLENGE_GENERATED:
- sendChallengeGenerated(msg.arg1 /* sensorId */, msg.arg2 /* userId */,
- (long) msg.obj /* challenge */);
- break;
- case MSG_FACE_DETECTED:
- sendFaceDetected(msg.arg1 /* sensorId */, msg.arg2 /* userId */,
- (boolean) msg.obj /* isStrongBiometric */);
- break;
- case MSG_AUTHENTICATION_FRAME:
- sendAuthenticationFrame((FaceAuthenticationFrame) msg.obj /* frame */);
- break;
- case MSG_ENROLLMENT_FRAME:
- sendEnrollmentFrame((FaceEnrollFrame) msg.obj /* frame */);
- break;
- default:
- Slog.w(TAG, "Unknown message: " + msg.what);
- }
- Trace.endSection();
- }
- }
-
private void sendSetFeatureCompleted(boolean success, int feature) {
if (mSetFeatureCallback == null) {
return;
diff --git a/core/java/android/hardware/face/IFaceService.aidl b/core/java/android/hardware/face/IFaceService.aidl
index 0096877f548a..e267e6b22f9d 100644
--- a/core/java/android/hardware/face/IFaceService.aidl
+++ b/core/java/android/hardware/face/IFaceService.aidl
@@ -45,7 +45,7 @@ interface IFaceService {
byte[] dumpSensorServiceStateProto(int sensorId, boolean clearSchedulerBuffer);
// Retrieve static sensor properties for all face sensors
- @EnforcePermission("USE_BIOMETRIC_INTERNAL")
+ @EnforcePermission(anyOf = {"USE_BIOMETRIC_INTERNAL", "USE_BACKGROUND_FACE_AUTHENTICATION"})
List<FaceSensorPropertiesInternal> getSensorPropertiesInternal(String opPackageName);
// Retrieve static sensor properties for the specified sensor
@@ -57,6 +57,11 @@ interface IFaceService {
long authenticate(IBinder token, long operationId, IFaceServiceReceiver receiver,
in FaceAuthenticateOptions options);
+ // Authenticate with a face. A requestId is returned that can be used to cancel this operation.
+ @EnforcePermission("USE_BACKGROUND_FACE_AUTHENTICATION")
+ long authenticateInBackground(IBinder token, long operationId, IFaceServiceReceiver receiver,
+ in FaceAuthenticateOptions options);
+
// Uses the face hardware to detect for the presence of a face, without giving details
// about accept/reject/lockout. A requestId is returned that can be used to cancel this
// operation.
@@ -131,7 +136,7 @@ interface IFaceService {
void revokeChallenge(IBinder token, int sensorId, int userId, String opPackageName, long challenge);
// Determine if a user has at least one enrolled face
- @EnforcePermission("USE_BIOMETRIC_INTERNAL")
+ @EnforcePermission(anyOf = {"USE_BIOMETRIC_INTERNAL", "USE_BACKGROUND_FACE_AUTHENTICATION"})
boolean hasEnrolledFaces(int sensorId, int userId, String opPackageName);
// Return the LockoutTracker status for the specified user
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 58159c2b5693..ab98c94cba73 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -10162,7 +10162,9 @@ public final class Settings {
public static final int HUB_MODE_TUTORIAL_STARTED = 1;
/**
- * Indicates that the user has completed the hub mode tutorial.
+ * Any value greater than or equal to this value is considered that the user has
+ * completed the hub mode tutorial.
+ *
* One of the possible states for {@link #HUB_MODE_TUTORIAL_STATE}.
*
* @hide
@@ -10181,8 +10183,11 @@ public final class Settings {
/**
* Defines the user's current state of navigating through the hub mode tutorial.
- * The possible states are defined in {@link HubModeTutorialState}.
+ * Some possible states are defined in {@link HubModeTutorialState}.
*
+ * Any value greater than or equal to {@link HUB_MODE_TUTORIAL_COMPLETED} indicates that
+ * the user has completed that version of the hub mode tutorial. And tutorial may be
+ * shown again when a new version becomes available.
* @hide
*/
public static final String HUB_MODE_TUTORIAL_STATE = "hub_mode_tutorial_state";
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index bf8e6135fd01..757978b71a01 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -1384,8 +1384,8 @@ public class LockPatternUtils {
}
public boolean isUserInLockdown(int userId) {
- return getStrongAuthForUser(userId)
- == StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN;
+ return (getStrongAuthForUser(userId)
+ & StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN) != 0;
}
private static class WrappedCallback extends ICheckCredentialProgressCallback.Stub {
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index adea8000cebc..a3d5cf6e3ab5 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2956,6 +2956,16 @@
<permission android:name="android.permission.MANAGE_SENSORS"
android:protectionLevel="signature" />
+ <!-- Must be required by a DomainSelectionService to ensure that only the
+ system can bind to it.
+ <p>Protection level: signature
+ @SystemApi
+ @hide
+ @FlaggedApi("com.android.internal.telephony.flags.ap_domain_selection_enabled")
+ -->
+ <permission android:name="android.permission.BIND_DOMAIN_SELECTION_SERVICE"
+ android:protectionLevel="signature" />
+
<!-- Must be required by an ImsService to ensure that only the
system can bind to it.
<p>Protection level: signature|privileged|vendorPrivileged
@@ -6612,6 +6622,13 @@
<permission android:name="android.permission.USE_BIOMETRIC_INTERNAL"
android:protectionLevel="signature" />
+ <!-- Allows privileged apps to access the background face authentication.
+ @SystemApi
+ @FlaggedApi("android.hardware.biometrics.face_background_authentication")
+ @hide -->
+ <permission android:name="android.permission.USE_BACKGROUND_FACE_AUTHENTICATION"
+ android:protectionLevel="signature|privileged" />
+
<!-- Allows the system to control the BiometricDialog (SystemUI). Reserved for the system. @hide -->
<permission android:name="android.permission.MANAGE_BIOMETRIC_DIALOG"
android:protectionLevel="signature" />
diff --git a/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java b/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java
index b843ad75ac0f..d816d0853ab6 100644
--- a/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java
+++ b/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java
@@ -18,6 +18,7 @@ package android.hardware.face;
import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_HW_UNAVAILABLE;
import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_UNABLE_TO_PROCESS;
+import static android.hardware.biometrics.Flags.FLAG_FACE_BACKGROUND_AUTHENTICATION;
import static com.google.common.truth.Truth.assertThat;
@@ -35,12 +36,15 @@ import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.res.Resources;
+import android.hardware.biometrics.BiometricPrompt;
import android.os.CancellationSignal;
import android.os.Handler;
+import android.os.HandlerExecutor;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.test.TestLooper;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
import com.android.internal.R;
@@ -58,6 +62,7 @@ import org.mockito.junit.MockitoRule;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.Executor;
@Presubmit
@RunWith(MockitoJUnitRunner.class)
@@ -78,6 +83,8 @@ public class FaceManagerTest {
@Mock
private FaceManager.AuthenticationCallback mAuthCallback;
@Mock
+ private BiometricPrompt.AuthenticationCallback mBioAuthCallback;
+ @Mock
private FaceManager.EnrollmentCallback mEnrollmentCallback;
@Mock
private FaceManager.FaceDetectionCallback mFaceDetectionCallback;
@@ -91,13 +98,16 @@ public class FaceManagerTest {
private TestLooper mLooper;
private Handler mHandler;
private FaceManager mFaceManager;
+ private Executor mExecutor;
@Before
public void setUp() throws Exception {
mLooper = new TestLooper();
mHandler = new Handler(mLooper.getLooper());
+ mExecutor = new HandlerExecutor(mHandler);
when(mContext.getMainLooper()).thenReturn(mLooper.getLooper());
+ when(mContext.getMainExecutor()).thenReturn(mExecutor);
when(mContext.getOpPackageName()).thenReturn(PACKAGE_NAME);
when(mContext.getAttributionTag()).thenReturn(ATTRIBUTION_TAG);
when(mContext.getApplicationInfo()).thenReturn(new ApplicationInfo());
@@ -159,6 +169,19 @@ public class FaceManagerTest {
}
@Test
+ @RequiresFlagsEnabled(FLAG_FACE_BACKGROUND_AUTHENTICATION)
+ public void authenticateInBackground_errorWhenUnavailable() throws Exception {
+ when(mService.authenticateInBackground(any(), anyLong(), any(), any()))
+ .thenThrow(new RemoteException());
+
+ mFaceManager.authenticateInBackground(mExecutor, null, new CancellationSignal(),
+ mBioAuthCallback);
+ mLooper.dispatchAll();
+
+ verify(mBioAuthCallback).onAuthenticationError(eq(FACE_ERROR_HW_UNAVAILABLE), any());
+ }
+
+ @Test
public void enrollment_errorWhenFaceEnrollmentExists() throws RemoteException {
when(mResources.getInteger(R.integer.config_faceMaxTemplatesPerUser)).thenReturn(1);
when(mService.getEnrolledFaces(anyInt(), anyInt(), anyString()))
diff --git a/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java b/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java
index 0df5b0a4093e..dcaf67660ffa 100644
--- a/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java
+++ b/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java
@@ -19,8 +19,10 @@ package com.android.internal.util;
import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_MANAGED;
import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
-import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED;
+import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT;
+import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN;
+import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED;
import static com.google.common.truth.Truth.assertThat;
@@ -76,12 +78,15 @@ public class LockPatternUtilsTest {
@Rule
public final RavenwoodRule mRavenwood = new RavenwoodRule();
+ private ILockSettings mLockSettings;
+ private static final int USER_ID = 1;
private static final int DEMO_USER_ID = 5;
private LockPatternUtils mLockPatternUtils;
private void configureTest(boolean isSecure, boolean isDemoUser, int deviceDemoMode)
throws Exception {
+ mLockSettings = Mockito.mock(ILockSettings.class);
final Context context = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
final MockContentResolver cr = new MockContentResolver(context);
@@ -89,15 +94,14 @@ public class LockPatternUtilsTest {
when(context.getContentResolver()).thenReturn(cr);
Settings.Global.putInt(cr, Settings.Global.DEVICE_DEMO_MODE, deviceDemoMode);
- final ILockSettings ils = Mockito.mock(ILockSettings.class);
- when(ils.getCredentialType(DEMO_USER_ID)).thenReturn(
+ when(mLockSettings.getCredentialType(DEMO_USER_ID)).thenReturn(
isSecure ? LockPatternUtils.CREDENTIAL_TYPE_PASSWORD
: LockPatternUtils.CREDENTIAL_TYPE_NONE);
- when(ils.getLong("lockscreen.password_type", PASSWORD_QUALITY_UNSPECIFIED, DEMO_USER_ID))
- .thenReturn((long) PASSWORD_QUALITY_MANAGED);
+ when(mLockSettings.getLong("lockscreen.password_type", PASSWORD_QUALITY_UNSPECIFIED,
+ DEMO_USER_ID)).thenReturn((long) PASSWORD_QUALITY_MANAGED);
// TODO(b/63758238): stop spying the class under test
mLockPatternUtils = spy(new LockPatternUtils(context));
- when(mLockPatternUtils.getLockSettings()).thenReturn(ils);
+ when(mLockPatternUtils.getLockSettings()).thenReturn(mLockSettings);
doReturn(true).when(mLockPatternUtils).hasSecureLockScreen();
final UserInfo userInfo = Mockito.mock(UserInfo.class);
@@ -108,6 +112,31 @@ public class LockPatternUtilsTest {
}
@Test
+ public void isUserInLockDown() throws Exception {
+ configureTest(true, false, 2);
+
+ // GIVEN strong auth not required
+ when(mLockSettings.getStrongAuthForUser(USER_ID)).thenReturn(STRONG_AUTH_NOT_REQUIRED);
+
+ // THEN user isn't in lockdown
+ assertFalse(mLockPatternUtils.isUserInLockdown(USER_ID));
+
+ // GIVEN lockdown
+ when(mLockSettings.getStrongAuthForUser(USER_ID)).thenReturn(
+ STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN);
+
+ // THEN user is in lockdown
+ assertTrue(mLockPatternUtils.isUserInLockdown(USER_ID));
+
+ // GIVEN lockdown and lockout
+ when(mLockSettings.getStrongAuthForUser(USER_ID)).thenReturn(
+ STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN | STRONG_AUTH_REQUIRED_AFTER_LOCKOUT);
+
+ // THEN user is in lockdown
+ assertTrue(mLockPatternUtils.isUserInLockdown(USER_ID));
+ }
+
+ @Test
public void isLockScreenDisabled_isDemoUser_true() throws Exception {
configureTest(false, true, 2);
assertTrue(mLockPatternUtils.isLockScreenDisabled(DEMO_USER_ID));
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index f7c278c76838..607b4bdf71ca 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -121,6 +121,7 @@ applications that come with the platform
<permission name="android.permission.BIND_CARRIER_MESSAGING_SERVICE"/>
<permission name="android.permission.BIND_CARRIER_SERVICES"/>
<permission name="android.permission.BIND_CELL_BROADCAST_SERVICE"/>
+ <permission name="android.permission.BIND_DOMAIN_SELECTION_SERVICE"/>
<permission name="android.permission.BIND_IMS_SERVICE"/>
<permission name="android.permission.BIND_SATELLITE_GATEWAY_SERVICE"/>
<permission name="android.permission.BIND_SATELLITE_SERVICE"/>
@@ -427,6 +428,7 @@ applications that come with the platform
<permission name="android.permission.USE_BIOMETRIC" />
<permission name="android.permission.TEST_BIOMETRIC" />
<permission name="android.permission.MANAGE_BIOMETRIC_DIALOG" />
+ <permission name="android.permission.USE_BACKGROUND_FACE_AUTHENTICATION" />
<!-- Permissions required for CTS test - CtsContactsProviderTestCases -->
<permission name="android.contacts.permission.MANAGE_SIM_ACCOUNTS" />
<!-- Permissions required for CTS test - CtsHdmiCecHostTestCases -->
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 3a778c314606..c77004d4eb17 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -4333,6 +4333,12 @@
"group": "WM_DEBUG_ANIM",
"at": "com\/android\/server\/wm\/WindowStateAnimator.java"
},
+ "1810872941": {
+ "message": "setWallpaperCropHints: non-existent wallpaper token: %s",
+ "level": "WARN",
+ "group": "WM_ERROR",
+ "at": "com\/android\/server\/wm\/WindowManagerService.java"
+ },
"1820873642": {
"message": "SyncGroup %d: Unfinished dependencies: %s",
"level": "VERBOSE",
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index ca283128891c..c25d41275f2b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -300,6 +300,9 @@ public class BubbleStackView extends FrameLayout
*/
private int mPointerIndexDown = -1;
+ /** Indicates whether bubbles should be reordered at the end of a gesture. */
+ private boolean mShouldReorderBubblesAfterGestureCompletes = false;
+
@Nullable
private BubblesNavBarGestureTracker mBubblesNavBarGestureTracker;
@@ -708,6 +711,11 @@ public class BubbleStackView extends FrameLayout
// Hide the stack after a delay, if needed.
updateTemporarilyInvisibleAnimation(false /* hideImmediately */);
+
+ if (mShouldReorderBubblesAfterGestureCompletes) {
+ mShouldReorderBubblesAfterGestureCompletes = false;
+ updateBubbleOrderInternal(mBubbleData.getBubbles(), true);
+ }
}
};
@@ -1928,7 +1936,18 @@ public class BubbleStackView extends FrameLayout
/**
* Update bubble order and pointer position.
*/
- public void updateBubbleOrder(List<Bubble> bubbles, boolean updatePointerPositoion) {
+ public void updateBubbleOrder(List<Bubble> bubbles, boolean updatePointerPosition) {
+ // Don't reorder bubbles in the middle of a gesture because that would remove bubbles from
+ // view hierarchy and will cancel all touch events. Instead wait until the gesture is
+ // finished and then reorder.
+ if (mIsGestureInProgress) {
+ mShouldReorderBubblesAfterGestureCompletes = true;
+ return;
+ }
+ updateBubbleOrderInternal(bubbles, updatePointerPosition);
+ }
+
+ private void updateBubbleOrderInternal(List<Bubble> bubbles, boolean updatePointerPosition) {
final Runnable reorder = () -> {
for (int i = 0; i < bubbles.size(); i++) {
Bubble bubble = bubbles.get(i);
@@ -1939,13 +1958,13 @@ public class BubbleStackView extends FrameLayout
reorder.run();
updateBadges(false /* setBadgeForCollapsedStack */);
updateZOrder();
- } else if (!isExpansionAnimating()) {
+ } else {
List<View> bubbleViews = bubbles.stream()
.map(b -> b.getIconView()).collect(Collectors.toList());
mStackAnimationController.animateReorder(bubbleViews, reorder);
}
- if (updatePointerPositoion) {
+ if (updatePointerPosition) {
updatePointerPosition(false /* forIme */);
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
index 23a4e3956289..4ddc539eb220 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
@@ -127,6 +127,7 @@ public class CompatUILayoutTest extends ShellTestCase {
@Test
public void testOnClickForSizeCompatHint() {
mWindowManager.mHasSizeCompat = true;
+ doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(mTaskInfo);
mWindowManager.createLayout(/* canShow= */ true);
final LinearLayout sizeCompatHint = mLayout.findViewById(R.id.size_compat_hint);
sizeCompatHint.performClick();
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index 62412df97043..17f25255fd4b 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -3084,11 +3084,7 @@ public final class MediaRouter2 {
public void registerRouteCallback() {
synchronized (mLock) {
try {
- if (mStub == null) {
- MediaRouter2Stub stub = new MediaRouter2Stub();
- mMediaRouterService.registerRouter2(stub, mPackageName);
- mStub = stub;
- }
+ registerRouterStubIfNeededLocked();
if (updateDiscoveryPreferenceIfNeededLocked()) {
mMediaRouterService.setDiscoveryRequestWithRouter2(
@@ -3114,8 +3110,7 @@ public final class MediaRouter2 {
}
if (mRouteCallbackRecords.isEmpty() && mNonSystemRoutingControllers.isEmpty()) {
- mMediaRouterService.unregisterRouter2(mStub);
- mStub = null;
+ unregisterRouterStubLocked();
}
} catch (RemoteException ex) {
Log.e(TAG, "unregisterRouteCallback: Unable to set discovery request.", ex);
@@ -3132,11 +3127,7 @@ public final class MediaRouter2 {
}
mRouteListingPreference = preference;
try {
- if (mStub == null) {
- MediaRouter2Stub stub = new MediaRouter2Stub();
- mMediaRouterService.registerRouter2(stub, mImpl.getPackageName());
- mStub = stub;
- }
+ registerRouterStubIfNeededLocked();
mMediaRouterService.setRouteListingPreference(mStub, mRouteListingPreference);
} catch (RemoteException ex) {
ex.rethrowFromSystemServer();
@@ -3328,18 +3319,31 @@ public final class MediaRouter2 {
obtainMessage(MediaRouter2::notifyStop, MediaRouter2.this, controller));
}
- if (mRouteCallbackRecords.isEmpty()
- && mNonSystemRoutingControllers.isEmpty()
- && mStub != null) {
+ if (mRouteCallbackRecords.isEmpty() && mNonSystemRoutingControllers.isEmpty()) {
try {
- mMediaRouterService.unregisterRouter2(mStub);
+ unregisterRouterStubLocked();
} catch (RemoteException ex) {
ex.rethrowFromSystemServer();
}
- mStub = null;
}
}
}
+ @GuardedBy("mLock")
+ private void registerRouterStubIfNeededLocked() throws RemoteException {
+ if (mStub == null) {
+ MediaRouter2Stub stub = new MediaRouter2Stub();
+ mMediaRouterService.registerRouter2(stub, mPackageName);
+ mStub = stub;
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void unregisterRouterStubLocked() throws RemoteException {
+ if (mStub != null) {
+ mMediaRouterService.unregisterRouter2(mStub);
+ mStub = null;
+ }
+ }
}
}
diff --git a/packages/SettingsLib/Spa/build.gradle.kts b/packages/SettingsLib/Spa/build.gradle.kts
index 8b136da04405..6f9556f91bd6 100644
--- a/packages/SettingsLib/Spa/build.gradle.kts
+++ b/packages/SettingsLib/Spa/build.gradle.kts
@@ -29,7 +29,7 @@ val androidTop: String = File(rootDir, "../../../../..").canonicalPath
allprojects {
extra["androidTop"] = androidTop
- extra["jetpackComposeVersion"] = "1.6.0-beta02"
+ extra["jetpackComposeVersion"] = "1.6.0-rc01"
}
subprojects {
diff --git a/packages/SettingsLib/Spa/gradle/libs.versions.toml b/packages/SettingsLib/Spa/gradle/libs.versions.toml
index 9703c347859c..1f78a9c3ac07 100644
--- a/packages/SettingsLib/Spa/gradle/libs.versions.toml
+++ b/packages/SettingsLib/Spa/gradle/libs.versions.toml
@@ -15,7 +15,7 @@
#
[versions]
-agp = "8.2.0"
+agp = "8.2.1"
compose-compiler = "1.5.1"
dexmaker-mockito = "2.28.3"
jvm = "17"
diff --git a/packages/SettingsLib/Spa/spa/build.gradle.kts b/packages/SettingsLib/Spa/spa/build.gradle.kts
index 7eccfe5ed508..618dc37037aa 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle.kts
+++ b/packages/SettingsLib/Spa/spa/build.gradle.kts
@@ -57,13 +57,13 @@ dependencies {
api("androidx.slice:slice-builders:1.1.0-alpha02")
api("androidx.slice:slice-core:1.1.0-alpha02")
api("androidx.slice:slice-view:1.1.0-alpha02")
- api("androidx.compose.material3:material3:1.2.0-alpha12")
+ api("androidx.compose.material3:material3:1.2.0-beta02")
api("androidx.compose.material:material-icons-extended:$jetpackComposeVersion")
api("androidx.compose.runtime:runtime-livedata:$jetpackComposeVersion")
api("androidx.compose.ui:ui-tooling-preview:$jetpackComposeVersion")
api("androidx.lifecycle:lifecycle-livedata-ktx")
api("androidx.lifecycle:lifecycle-runtime-compose")
- api("androidx.navigation:navigation-compose:2.7.4")
+ api("androidx.navigation:navigation-compose:2.7.6")
api("com.github.PhilJay:MPAndroidChart:v3.1.0-alpha")
api("com.google.android.material:material:1.7.0-alpha03")
debugApi("androidx.compose.ui:ui-tooling:$jetpackComposeVersion")
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
index 5605485c4b84..da1ee77bcbfb 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
@@ -22,12 +22,14 @@ import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.annotation.VisibleForTesting
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Modifier
import androidx.core.view.WindowCompat
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavGraph.Companion.findStartDestination
@@ -133,6 +135,7 @@ private fun NavControllerWrapperImpl.NavContent(
NavHost(
navController = navController,
startDestination = NullPageProvider.name,
+ modifier = Modifier.fillMaxSize(),
) {
composable(NullPageProvider.name) {}
for (spp in allProvider) {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
index 81bee5ec0b94..0281ab817340 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
@@ -86,7 +86,7 @@ fun SettingsPageProvider.createSettingsPage(arguments: Bundle? = null): Settings
)
}
-object NullPageProvider : SettingsPageProvider {
+internal object NullPageProvider : SettingsPageProvider {
override val name = NULL_PAGE_NAME
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavGraphBuilder.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavGraphBuilder.kt
index 192b12500978..93ad644bf5de 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavGraphBuilder.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavGraphBuilder.kt
@@ -30,6 +30,7 @@ import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDeepLink
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
+import com.android.settingslib.spa.framework.common.NullPageProvider
/**
* Add the [Composable] to the [NavGraphBuilder] with animation
@@ -49,11 +50,13 @@ internal fun NavGraphBuilder.animatedComposable(
arguments = arguments,
deepLinks = deepLinks,
enterTransition = {
- slideIntoContainer(
- towards = AnimatedContentTransitionScope.SlideDirection.Start,
- animationSpec = slideInEffect,
- initialOffset = offsetFunc,
- ) + fadeIn(animationSpec = fadeInEffect)
+ if (initialState.destination.route != NullPageProvider.name) {
+ slideIntoContainer(
+ towards = AnimatedContentTransitionScope.SlideDirection.Start,
+ animationSpec = slideInEffect,
+ initialOffset = offsetFunc,
+ ) + fadeIn(animationSpec = fadeInEffect)
+ } else null
},
exitTransition = {
slideOutOfContainer(
diff --git a/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/LiveDataTestUtil.kt b/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/LiveDataTestUtil.kt
deleted file mode 100644
index dddda5511c7b..000000000000
--- a/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/LiveDataTestUtil.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.testutils
-
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.Observer
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.TimeoutException
-
-fun <T> LiveData<T>.getOrAwaitValue(
- timeout: Long = 1,
- timeUnit: TimeUnit = TimeUnit.SECONDS,
- afterObserve: () -> Unit = {},
-): T? {
- var data: T? = null
- val latch = CountDownLatch(1)
- val observer = Observer<T> { newData ->
- data = newData
- latch.countDown()
- }
- this.observeForever(observer)
-
- afterObserve()
-
- try {
- // Don't wait indefinitely if the LiveData is not set.
- if (!latch.await(timeout, timeUnit)) {
- throw TimeoutException("LiveData value was never set.")
- }
- } finally {
- this.removeObserver(observer)
- }
-
- return data
-}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PermissionsChangedFlow.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PermissionsChangedFlow.kt
new file mode 100644
index 000000000000..367244aa2cfe
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PermissionsChangedFlow.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.spaprivileged.model.app
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import com.android.settingslib.spaprivileged.framework.common.asUser
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flowOn
+
+/**
+ * Creates an instance of a cold Flow for permissions changed callback of given [app].
+ *
+ * An initial element will be always sent.
+ */
+fun Context.permissionsChangedFlow(app: ApplicationInfo) = callbackFlow {
+ val userPackageManager = asUser(app.userHandle).packageManager
+
+ val onPermissionsChangedListener = PackageManager.OnPermissionsChangedListener { uid ->
+ if (uid == app.uid) trySend(Unit)
+ }
+ userPackageManager.addOnPermissionsChangeListener(onPermissionsChangedListener)
+ trySend(Unit)
+
+ awaitClose {
+ userPackageManager.removeOnPermissionsChangeListener(onPermissionsChangedListener)
+ }
+}.conflate().flowOn(Dispatchers.Default)
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PermissionsChangedFlowTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PermissionsChangedFlowTest.kt
new file mode 100644
index 000000000000..31522c1209f7
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PermissionsChangedFlowTest.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.spaprivileged.model.app
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
+import com.android.settingslib.spa.testutils.toListWithTimeout
+import com.android.settingslib.spaprivileged.framework.common.asUser
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.async
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+
+@RunWith(AndroidJUnit4::class)
+class PermissionsChangedFlowTest {
+
+ private var onPermissionsChangedListener: PackageManager.OnPermissionsChangedListener? = null
+
+ private val mockPackageManager = mock<PackageManager> {
+ on { addOnPermissionsChangeListener(any()) } doAnswer {
+ onPermissionsChangedListener =
+ it.arguments[0] as PackageManager.OnPermissionsChangedListener
+ }
+ }
+
+ private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+ on { asUser(APP.userHandle) } doReturn mock
+ on { packageManager } doReturn mockPackageManager
+ }
+
+ @Test
+ fun permissionsChangedFlow_sendInitialValueTrue() = runBlocking {
+ val flow = context.permissionsChangedFlow(APP)
+
+ assertThat(flow.firstWithTimeoutOrNull()).isNotNull()
+ }
+
+ @Test
+ fun permissionsChangedFlow_collectChanged_getTwo() = runBlocking {
+ val listDeferred = async {
+ context.permissionsChangedFlow(APP).toListWithTimeout()
+ }
+ delay(100)
+
+ onPermissionsChangedListener?.onPermissionsChanged(APP.uid)
+
+ assertThat(listDeferred.await()).hasSize(2)
+ }
+
+ private companion object {
+ val APP = ApplicationInfo().apply {
+ packageName = "package.name"
+ uid = 10000
+ }
+ }
+}
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index db1ca95fd83e..cc63996494a0 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -562,6 +562,9 @@
<!-- Permission required for CTS test - android.server.biometrics -->
<uses-permission android:name="android.permission.MANAGE_BIOMETRIC_DIALOG" />
+ <!-- Permission required for CTS test - android.server.biometrics -->
+ <uses-permission android:name="android.permission.USE_BACKGROUND_FACE_AUTHENTICATION" />
+
<!-- Permissions required for CTS test - NotificationManagerTest -->
<uses-permission android:name="android.permission.MANAGE_NOTIFICATION_LISTENERS" />
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
index f4ffb3c66219..2052e2c01410 100644
--- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -25,6 +25,7 @@ import androidx.lifecycle.LifecycleOwner
import com.android.systemui.bouncer.ui.BouncerDialogFactory
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
+import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.scene.shared.model.Scene
@@ -52,6 +53,7 @@ object ComposeFacade : BaseComposeFacade {
override fun setCommunalEditWidgetActivityContent(
activity: ComponentActivity,
viewModel: BaseCommunalViewModel,
+ widgetConfigurator: WidgetConfigurator,
onOpenWidgetPicker: () -> Unit,
onEditDone: () -> Unit,
) {
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
index 43745f9bf027..b607d596390d 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -37,6 +37,7 @@ import com.android.systemui.common.ui.compose.windowinsets.DisplayCutoutProvider
import com.android.systemui.communal.ui.compose.CommunalContainer
import com.android.systemui.communal.ui.compose.CommunalHub
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
+import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.people.ui.compose.PeopleScreen
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.compose.FooterActions
@@ -69,6 +70,7 @@ object ComposeFacade : BaseComposeFacade {
override fun setCommunalEditWidgetActivityContent(
activity: ComponentActivity,
viewModel: BaseCommunalViewModel,
+ widgetConfigurator: WidgetConfigurator,
onOpenWidgetPicker: () -> Unit,
onEditDone: () -> Unit,
) {
@@ -77,6 +79,7 @@ object ComposeFacade : BaseComposeFacade {
CommunalHub(
viewModel = viewModel,
onOpenWidgetPicker = onOpenWidgetPicker,
+ widgetConfigurator = widgetConfigurator,
onEditDone = onEditDone,
)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index a390305b144e..556a315894b8 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -20,7 +20,10 @@ import android.appwidget.AppWidgetHostView
import android.os.Bundle
import android.util.SizeF
import android.widget.FrameLayout
+import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
@@ -47,6 +50,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.outlined.Delete
+import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.TouchApp
import androidx.compose.material.icons.outlined.Widgets
import androidx.compose.material3.Button
@@ -54,8 +58,10 @@ import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
+import androidx.compose.material3.IconButtonColors
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
@@ -66,6 +72,7 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -99,12 +106,15 @@ import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset
import com.android.systemui.communal.ui.compose.extensions.observeTapsWithoutConsuming
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
+import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.res.R
+import kotlinx.coroutines.launch
@Composable
fun CommunalHub(
modifier: Modifier = Modifier,
viewModel: BaseCommunalViewModel,
+ widgetConfigurator: WidgetConfigurator? = null,
onOpenWidgetPicker: (() -> Unit)? = null,
onEditDone: (() -> Unit)? = null,
) {
@@ -116,7 +126,7 @@ fun CommunalHub(
var gridCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) }
var isDraggingToRemove by remember { mutableStateOf(false) }
val gridState = rememberLazyGridState()
- val contentListState = rememberContentListState(communalContent, viewModel)
+ val contentListState = rememberContentListState(widgetConfigurator, communalContent, viewModel)
val reorderingWidgets by viewModel.reorderingWidgets.collectAsState()
val selectedIndex = viewModel.selectedIndex.collectAsState()
val removeButtonEnabled by remember {
@@ -167,7 +177,8 @@ fun CommunalHub(
onOpenWidgetPicker = onOpenWidgetPicker,
gridState = gridState,
contentListState = contentListState,
- selectedIndex = selectedIndex
+ selectedIndex = selectedIndex,
+ widgetConfigurator = widgetConfigurator,
)
if (viewModel.isEditMode && onOpenWidgetPicker != null && onEditDone != null) {
@@ -221,6 +232,7 @@ private fun BoxScope.CommunalHubLazyGrid(
setGridCoordinates: (coordinates: LayoutCoordinates) -> Unit,
updateDragPositionForRemove: (offset: Offset) -> Boolean,
onOpenWidgetPicker: (() -> Unit)? = null,
+ widgetConfigurator: WidgetConfigurator?,
) {
var gridModifier = Modifier.align(Alignment.CenterStart)
var list = communalContent
@@ -283,21 +295,24 @@ private fun BoxScope.CommunalHubLazyGrid(
enabled = list[index] is CommunalContentModel.Widget,
index = index,
size = size
- ) { _ ->
+ ) { isDragging ->
CommunalContent(
modifier = cardModifier,
model = list[index],
viewModel = viewModel,
size = size,
onOpenWidgetPicker = onOpenWidgetPicker,
+ selected = selected && !isDragging,
+ widgetConfigurator = widgetConfigurator,
)
}
} else {
CommunalContent(
- modifier = cardModifier,
model = list[index],
viewModel = viewModel,
size = size,
+ selected = false,
+ modifier = cardModifier,
)
}
}
@@ -453,11 +468,14 @@ private fun CommunalContent(
model: CommunalContentModel,
viewModel: BaseCommunalViewModel,
size: SizeF,
+ selected: Boolean,
modifier: Modifier = Modifier,
onOpenWidgetPicker: (() -> Unit)? = null,
+ widgetConfigurator: WidgetConfigurator? = null,
) {
when (model) {
- is CommunalContentModel.Widget -> WidgetContent(viewModel, model, size, modifier)
+ is CommunalContentModel.Widget ->
+ WidgetContent(viewModel, model, size, selected, widgetConfigurator, modifier)
is CommunalContentModel.WidgetPlaceholder -> HighlightedItem(size)
is CommunalContentModel.CtaTileInViewMode ->
CtaTileInViewModeContent(viewModel, size, modifier)
@@ -594,15 +612,17 @@ private fun WidgetContent(
viewModel: BaseCommunalViewModel,
model: CommunalContentModel.Widget,
size: SizeF,
+ selected: Boolean,
+ widgetConfigurator: WidgetConfigurator?,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier.height(size.height.dp),
- contentAlignment = Alignment.Center,
) {
val paddingInPx = with(LocalDensity.current) { CardOutlineWidth.toPx().toInt() }
AndroidView(
- modifier = modifier.allowGestures(allowed = !viewModel.isEditMode),
+ modifier =
+ modifier.align(Alignment.Center).allowGestures(allowed = !viewModel.isEditMode),
factory = { context ->
// The AppWidgetHostView will inherit the interaction handler from the
// AppWidgetHost. So set the interaction handler here before creating the view, and
@@ -624,6 +644,55 @@ private fun WidgetContent(
// For reusing composition in lazy lists.
onReset = {},
)
+ if (
+ viewModel is CommunalEditModeViewModel &&
+ model.reconfigurable &&
+ widgetConfigurator != null
+ ) {
+ WidgetConfigureButton(
+ visible = selected,
+ model = model,
+ widgetConfigurator = widgetConfigurator,
+ modifier = Modifier.align(Alignment.BottomEnd)
+ )
+ }
+ }
+}
+
+@Composable
+fun WidgetConfigureButton(
+ visible: Boolean,
+ model: CommunalContentModel.Widget,
+ modifier: Modifier = Modifier,
+ widgetConfigurator: WidgetConfigurator,
+) {
+ val colors = LocalAndroidColorScheme.current
+ val scope = rememberCoroutineScope()
+
+ AnimatedVisibility(
+ visible = visible,
+ enter = fadeIn(),
+ exit = fadeOut(),
+ modifier = modifier.padding(16.dp),
+ ) {
+ FilledIconButton(
+ shape = RoundedCornerShape(16.dp),
+ modifier = Modifier.size(48.dp),
+ colors =
+ IconButtonColors(
+ containerColor = colors.primary,
+ contentColor = colors.onPrimary,
+ disabledContainerColor = Color.Transparent,
+ disabledContentColor = Color.Transparent
+ ),
+ onClick = { scope.launch { widgetConfigurator.configureWidget(model.appWidgetId) } },
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.Edit,
+ contentDescription = stringResource(id = R.string.edit_widget),
+ modifier = Modifier.padding(12.dp)
+ )
+ }
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
index 45f98b879dd7..67b79a06b4a0 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
@@ -22,16 +22,24 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
+import com.android.systemui.communal.widgets.WidgetConfigurator
@Composable
fun rememberContentListState(
+ widgetConfigurator: WidgetConfigurator?,
communalContent: List<CommunalContentModel>,
viewModel: BaseCommunalViewModel,
): ContentListState {
return remember(communalContent) {
ContentListState(
communalContent,
- viewModel::onAddWidget,
+ { componentName, priority ->
+ viewModel.onAddWidget(
+ componentName,
+ priority,
+ widgetConfigurator,
+ )
+ },
viewModel::onDeleteWidget,
viewModel::onReorderWidgets,
)
diff --git a/packages/SystemUI/docs/imgs/ribbon.png b/packages/SystemUI/docs/imgs/ribbon.png
index 9f5765232aed..3379d3d95025 100644
--- a/packages/SystemUI/docs/imgs/ribbon.png
+++ b/packages/SystemUI/docs/imgs/ribbon.png
Binary files differ
diff --git a/packages/SystemUI/docs/scene.md b/packages/SystemUI/docs/scene.md
index 3e4a1b4a05c7..105e4385bb25 100644
--- a/packages/SystemUI/docs/scene.md
+++ b/packages/SystemUI/docs/scene.md
@@ -20,14 +20,16 @@ over several dimensions:
from one scene to another) are also pulled out and separated from the
content of the UI.
-In addition to the above, some of the **secondary goals** are: 4. Make
-**customization easier**: by separating scenes to standalone pieces, it becomes
-possible for variant owners and OEMs to exclude or replace certain scenes or to
-add brand-new scenes. 5. **Enable modularization**: by separating scenes to
-standalone pieces, it becomes possible to break down System UI into smaller
-codebases, each one of which could be built on its own. Note: this isn't part of
-the scene framework itself but is something that can be done more easily once
-the scene framework is in place.
+In addition to the above, some of the **secondary goals** are:
+
+4. Make **customization easier**: by separating scenes to standalone pieces, it
+becomes possible for variant owners and OEMs to exclude or replace certain scenes
+or to add brand-new scenes.
+5. **Enable modularization**: by separating scenes to standalone pieces, it
+becomes possible to break down System UI into smaller codebases, each one of
+which could be built on its own. Note: this isn't part of the scene framework
+itself but is something that can be done more easily once the scene framework
+is in place.
## Terminology
@@ -70,15 +72,17 @@ file evalutes to `true`.
running: `console $ adb shell statusbar cmd migrate_keyguard_status_bar_view
true`
3. Set a collection of **aconfig flags** to `true` by running the following
- commands: `console $ adb shell device_config put systemui
- com.android.systemui.scene_container true $ adb shell device_config put
- systemui com.android.systemui.keyguard_bottom_area_refactor true $ adb shell
- device_config put systemui
- com.android.systemui.keyguard_shade_migration_nssl true $ adb shell
- device_config put systemui com.android.systemui.media_in_scene_container
- true`
-4. **Restart** System UI by issuing the following command: `console $ adb shell
- am crash com.android.systemui`
+ commands:
+ ```console
+ $ adb shell device_config put systemui com.android.systemui.scene_container true
+ $ adb shell device_config put systemui com.android.systemui.keyguard_bottom_area_refactor true
+ $ adb shell device_config put systemui com.android.systemui.keyguard_shade_migration_nssl true
+ $ adb shell device_config put systemui com.android.systemui.media_in_scene_container true
+ ```
+4. **Restart** System UI by issuing the following command:
+ ```console
+ $ adb shell am crash com.android.systemui
+ ```
5. **Verify** that the scene framework was turned on. There are two ways to do
this:
@@ -94,15 +98,29 @@ file evalutes to `true`.
$ adb shell cmd statusbar echo -b SceneFramework:verbose
-# Look for the log statements from the framework:
+### Checking if the framework is enabled
+
+Look for the log statements from the framework:
-$ adb logcat -v time SceneFramework:* *:S ```
+```console
+$ adb logcat -v time SceneFramework:* *:S
+```
+
+### Disabling the framework
-To **disable** the framework, simply turn off the main aconfig flag: `console $
-adb shell device_config put systemui com.android.systemui.scene_container false`
+To **disable** the framework, simply turn off the main aconfig flag:
+
+```console
+$ adb shell device_config put systemui com.android.systemui.scene_container false
+```
## Defining a scene
+By default, the framework ships with fully functional scenes as enumarated
+[here](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneKey.kt).
+Should a variant owner or OEM want to replace or add a new scene, they could
+do so by defining their own scene. This section describes how to do that.
+
Each scene is defined as an implementation of the
[`ComposableScene`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposableScene.kt)
interface, which has three parts: 1. The `key` property returns the
@@ -118,28 +136,28 @@ between any two scenes. The Scene Framework has other ways to define how the
content of your UI changes with and throughout a transition to learn more please
see the [Scene transition animations](#Scene-transition-animations) section
-For example: ```kotlin @SysUISingleton class YourScene @Inject constructor( //
-your dependencies here ) : ComposableScene { override val key =
-SceneKey.YourScene
+For example:
-```
-override val destinationScenes: StateFlow<Map<UserAction, SceneModel>> =
- MutableStateFlow<Map<UserAction, SceneModel>>(
- mapOf(
- // This is where scene navigation is defined, more on that below.
- )
- ).asStateFlow()
-
-@Composable
-override fun SceneScope.Content(
- modifier: Modifier,
-) {
- // This is where the UI is defined using Jetpack Compose.
+```kotlin
+@SysUISingleton class YourScene @Inject constructor( /* your dependencies here */ ) : ComposableScene {
+ override val key = SceneKey.YourScene
+
+ override val destinationScenes: StateFlow<Map<UserAction, SceneModel>> =
+ MutableStateFlow<Map<UserAction, SceneModel>>(
+ mapOf(
+ // This is where scene navigation is defined, more on that below.
+ )
+ ).asStateFlow()
+
+ @Composable
+ override fun SceneScope.Content(
+ modifier: Modifier,
+ ) {
+ // This is where the UI is defined using Jetpack Compose.
+ }
}
```
-} ```
-
### Injecting scenes
Scenes are injected into the Dagger dependency graph from the
@@ -200,20 +218,21 @@ fun TransitionBuilder.lockscreenToShadeTransition() {
}
```
-Going through the example code: * The `spec` is the animation that should be
-invoked, in the example above, we use a `tween` animation with a duration of 500
-milliseconds * Then there's a series of function calls: `punchHole` applies a
-clip mask to the `Scrim` element in the destination scene (in this case it's the
-`Shade` scene) which has the position and size determined by the `bounds`
-parameter and the shape passed into the `shape` parameter. This lets the
-`Lockscreen` scene render "through" the `Shade` scene * The `translate` call
-shifts the `Scrim` element to/from the `Top` edge of the scene container * The
-first `fractionRange` wrapper tells the system to apply its contained functions
+Going through the example code:
+
+* The `spec` is the animation that should be invoked, in the example above, we use a `tween`
+animation with a duration of 500 milliseconds
+* Then there's a series of function calls: `punchHole` applies a clip mask to the `Scrim`
+element in the destination scene (in this case it's the `Shade` scene) which has the
+position and size determined by the `bounds` parameter and the shape passed into the `shape`
+parameter. This lets the `Lockscreen` scene render "through" the `Shade` scene
+* The `translate` call shifts the `Scrim` element to/from the `Top` edge of the scene container
+* The first `fractionRange` wrapper tells the system to apply its contained functions
only during the first half of the transition. Inside of it, we see a `fade` of
the `ScrimBackground` element and a `translate` o the `CollpasedGrid` element
-to/from the `Top` edge * The second `fractionRange` only starts at the second
-half of the transition (e.g. when the previous one ends) and applies a `fade` on
-the `Notifications` element
+to/from the `Top` edge
+* The second `fractionRange` only starts at the second half of the transition (e.g. when
+the previous one ends) and applies a `fade` on the `Notifications` element
You can find the actual documentation for this API
[here](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDsl.kt).
@@ -295,3 +314,52 @@ top-level Dagger module at
this puts together the scenes from `SceneModule`, the configuration from
`SceneContainerConfigModule`, and the startable from
`SceneContainerStartableModule`.
+
+## Integration Notes
+
+### Relationship to Jetpack Compose
+
+The scene framework depends on Jetpack Compose; therefore, compiling System UI with
+Jetpack Compose is required. However, because Jetpack Compose and Android Views
+[interoperate](https://developer.android.com/jetpack/compose/migrate/interoperability-apis/views-in-compose),
+the UI in each scene doesn't necessarily need to be a pure hierarchy of `@Composable`
+functions; instead, it's acceptable to use an `AndroidView` somewhere in the
+hierarchy of composable functions to include a `View` or `ViewGroup` subtree.
+
+#### Interoperability with Views
+The scene framework comes with built-in functionality to animate the entire scene and/or
+elements within the scene in-tandem with the actual scene transition progress.
+
+For example, as the user drags their finger down rom the top of the lockscreen,
+the shade scene becomes visible and gradually expands, the amount of expansion tracks
+the movement of the finger.
+
+That feature of the framework uses a custom `element(ElementKey)` Jetpack Compose
+`Modifier` to refer to elements within a scene.
+The transition builders then use the same `ElementKey` objects to refer to those elements
+and describe how they animate in-tandem with scene transitions. Because this is a
+Jetpack Compose `Modifier`, it means that, in order for an element in a scene to be
+animated automatically by the framework, that element must be nested within a pure
+`@Composable` hierarchy. The element itself is allowed to be a classic Android `View`
+(nested within a Jetpack Compose `AndroidView`) but all ancestors must be `@Composable`
+functions.
+
+### Notifications
+
+As of January 2024, the integration of notifications and heads-up notifications (HUNs)
+into the scene framework follows an unusual pattern. We chose this pattern due to migration
+risk and performance concerns but will eventually replace it with the more common element
+placement pattern that all other elements are following.
+
+The special pattern for notifications is that, instead of the notification list
+(`NotificationStackScrollLayout` or "NSSL", which also displays HUNs) being placed in the element
+hierarchy within the scenes that display notifications, the NSSL (which continues to be an Android View)
+"floats" above the scene container, rendering on top of everything. This is very similar to
+how NSSL is integrated with the legacy shade, prior to the scene framework.
+
+In order to render the NSSL as if it's part of the organic hierarchy of elements within its
+scenes, we control the NSSL's self-imposed effective bounds (e.g. position offsets, clip path,
+size) from `@Composable` elements within the normal scene hierarchy. These special
+"placeholder" elements can be found
+[here](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt).
+
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
index 2a793ea70292..030d41ddd8fb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
@@ -45,6 +45,7 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants
import com.android.systemui.classifier.FalsingA11yDelegate
import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
@@ -819,6 +820,8 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() {
// While listening, going from the bouncer scene to the gone scene, does dismiss the
// keyguard.
+ kosmos.fakeDeviceEntryRepository.setUnlocked(true)
+ runCurrent()
sceneInteractor.changeScene(SceneModel(SceneKey.Gone, null), "reason")
sceneTransitionStateFlow.value =
ObservableTransitionState.Transition(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
index fdb17c298e17..bb3429e72b35 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
@@ -18,6 +18,8 @@ package com.android.systemui.communal.data.repository
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
+import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL
+import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE
import android.content.ComponentName
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -28,6 +30,9 @@ import com.android.systemui.communal.data.db.CommunalWidgetItem
import com.android.systemui.communal.shared.CommunalWidgetHost
import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
import com.android.systemui.communal.widgets.CommunalAppWidgetHost
+import com.android.systemui.communal.widgets.WidgetConfigurator
+import com.android.systemui.communal.widgets.widgetConfiguratorFail
+import com.android.systemui.communal.widgets.widgetConfiguratorSuccess
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
@@ -71,12 +76,12 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
@Mock private lateinit var communalWidgetDao: CommunalWidgetDao
- private val kosmos = testKosmos()
+ private lateinit var logBuffer: LogBuffer
+ private val kosmos = testKosmos()
+ private val testDispatcher = kosmos.testDispatcher
private val testScope = kosmos.testScope
- private lateinit var logBuffer: LogBuffer
-
private val fakeAllowlist =
listOf(
"com.android.fake/WidgetProviderA",
@@ -157,10 +162,11 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
val provider = ComponentName("pkg_name", "cls_name")
val id = 1
val priority = 1
- whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(true)
+ whenever(communalWidgetHost.getAppWidgetInfo(id))
+ .thenReturn(PROVIDER_INFO_REQUIRES_CONFIGURATION)
whenever(communalWidgetHost.allocateIdAndBindWidget(any<ComponentName>()))
.thenReturn(id)
- underTest.addWidget(provider, priority) { true }
+ underTest.addWidget(provider, priority, kosmos.widgetConfiguratorSuccess)
runCurrent()
verify(communalWidgetHost).allocateIdAndBindWidget(provider)
@@ -175,9 +181,10 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
val provider = ComponentName("pkg_name", "cls_name")
val id = 1
val priority = 1
- whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(true)
+ whenever(communalWidgetHost.getAppWidgetInfo(id))
+ .thenReturn(PROVIDER_INFO_REQUIRES_CONFIGURATION)
whenever(communalWidgetHost.allocateIdAndBindWidget(provider)).thenReturn(id)
- underTest.addWidget(provider, priority) { false }
+ underTest.addWidget(provider, priority, kosmos.widgetConfiguratorFail)
runCurrent()
verify(communalWidgetHost).allocateIdAndBindWidget(provider)
@@ -193,9 +200,18 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
val provider = ComponentName("pkg_name", "cls_name")
val id = 1
val priority = 1
- whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(true)
+ whenever(communalWidgetHost.getAppWidgetInfo(id))
+ .thenReturn(PROVIDER_INFO_REQUIRES_CONFIGURATION)
whenever(communalWidgetHost.allocateIdAndBindWidget(provider)).thenReturn(id)
- underTest.addWidget(provider, priority) { throw IllegalStateException("some error") }
+ underTest.addWidget(
+ provider,
+ priority,
+ object : WidgetConfigurator {
+ override suspend fun configureWidget(appWidgetId: Int): Boolean {
+ throw IllegalStateException("some error")
+ }
+ }
+ )
runCurrent()
verify(communalWidgetHost).allocateIdAndBindWidget(provider)
@@ -211,19 +227,15 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
val provider = ComponentName("pkg_name", "cls_name")
val id = 1
val priority = 1
- whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(false)
+ whenever(communalWidgetHost.getAppWidgetInfo(id))
+ .thenReturn(PROVIDER_INFO_CONFIGURATION_OPTIONAL)
whenever(communalWidgetHost.allocateIdAndBindWidget(any<ComponentName>()))
.thenReturn(id)
- var configured = false
- underTest.addWidget(provider, priority) {
- configured = true
- true
- }
+ underTest.addWidget(provider, priority, kosmos.widgetConfiguratorFail)
runCurrent()
verify(communalWidgetHost).allocateIdAndBindWidget(provider)
verify(communalWidgetDao).addWidget(id, provider, priority)
- assertThat(configured).isFalse()
}
@Test
@@ -280,4 +292,15 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
private fun setAppWidgetIds(ids: List<Int>) {
whenever(appWidgetHost.appWidgetIds).thenReturn(ids.toIntArray())
}
+
+ private companion object {
+ val PROVIDER_INFO_REQUIRES_CONFIGURATION =
+ AppWidgetProviderInfo().apply { configure = ComponentName("test.pkg", "test.cmp") }
+ val PROVIDER_INFO_CONFIGURATION_OPTIONAL =
+ AppWidgetProviderInfo().apply {
+ configure = ComponentName("test.pkg", "test.cmp")
+ widgetFeatures =
+ WIDGET_FEATURE_CONFIGURATION_OPTIONAL or WIDGET_FEATURE_RECONFIGURABLE
+ }
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index 09243e5282da..125ede413784 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -16,10 +16,7 @@
package com.android.systemui.communal.view.viewmodel
-import android.app.Activity.RESULT_CANCELED
-import android.app.Activity.RESULT_OK
import android.app.smartspace.SmartspaceTarget
-import android.content.ComponentName
import android.provider.Settings
import android.widget.RemoteViews
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -35,7 +32,6 @@ import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.shared.log.CommunalUiEvent
import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
-import com.android.systemui.communal.widgets.CommunalAppWidgetHost
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.kosmos.testScope
@@ -46,7 +42,6 @@ import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -61,7 +56,6 @@ import org.mockito.MockitoAnnotations
@RunWith(AndroidJUnit4::class)
class CommunalEditModeViewModelTest : SysuiTestCase() {
@Mock private lateinit var mediaHost: MediaHost
- @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost
@Mock private lateinit var uiEventLogger: UiEventLogger
private val kosmos = testKosmos()
@@ -91,7 +85,6 @@ class CommunalEditModeViewModelTest : SysuiTestCase() {
underTest =
CommunalEditModeViewModel(
withDeps.communalInteractor,
- appWidgetHost,
mediaHost,
uiEventLogger,
)
@@ -154,55 +147,6 @@ class CommunalEditModeViewModelTest : SysuiTestCase() {
}
@Test
- fun addingWidgetTriggersConfiguration() =
- testScope.runTest {
- val provider = ComponentName("pkg.test", "testWidget")
- val widgetToConfigure by collectLastValue(underTest.widgetsToConfigure)
- assertThat(widgetToConfigure).isNull()
- underTest.onAddWidget(componentName = provider, priority = 0)
- assertThat(widgetToConfigure).isEqualTo(1)
- }
-
- @Test
- fun settingResultOkAddsWidget() =
- testScope.runTest {
- val provider = ComponentName("pkg.test", "testWidget")
- val widgetAdded by collectLastValue(widgetRepository.widgetAdded)
- assertThat(widgetAdded).isNull()
- underTest.onAddWidget(componentName = provider, priority = 0)
- assertThat(widgetAdded).isNull()
- underTest.setConfigurationResult(RESULT_OK)
- assertThat(widgetAdded).isEqualTo(1)
- }
-
- @Test
- fun settingResultCancelledDoesNotAddWidget() =
- testScope.runTest {
- val provider = ComponentName("pkg.test", "testWidget")
- val widgetAdded by collectLastValue(widgetRepository.widgetAdded)
- assertThat(widgetAdded).isNull()
- underTest.onAddWidget(componentName = provider, priority = 0)
- assertThat(widgetAdded).isNull()
- underTest.setConfigurationResult(RESULT_CANCELED)
- assertThat(widgetAdded).isNull()
- }
-
- @Test(expected = IllegalStateException::class)
- fun settingResultBeforeWidgetAddedThrowsException() {
- underTest.setConfigurationResult(RESULT_OK)
- }
-
- @Test(expected = IllegalStateException::class)
- fun addingWidgetWhileConfigurationActiveFails() =
- testScope.runTest {
- val providerOne = ComponentName("pkg.test", "testWidget")
- underTest.onAddWidget(componentName = providerOne, priority = 0)
- runCurrent()
- val providerTwo = ComponentName("pkg.test", "testWidget2")
- underTest.onAddWidget(componentName = providerTwo, priority = 0)
- }
-
- @Test
fun reorderWidget_uiEventLogging_start() {
underTest.onReorderWidgetStart()
verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetConfigurationControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetConfigurationControllerTest.kt
new file mode 100644
index 000000000000..55fafdff795f
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetConfigurationControllerTest.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.widgets
+
+import android.app.Activity
+import android.content.ActivityNotFoundException
+import androidx.activity.ComponentActivity
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WidgetConfigurationControllerTest : SysuiTestCase() {
+ @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost
+ @Mock private lateinit var ownerActivity: ComponentActivity
+
+ private val kosmos = testKosmos()
+
+ private lateinit var underTest: WidgetConfigurationController
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ underTest =
+ WidgetConfigurationController(ownerActivity, appWidgetHost, kosmos.testDispatcher)
+ }
+
+ @Test
+ fun configurationFailsWhenActivityNotFound() =
+ with(kosmos) {
+ testScope.runTest {
+ whenever(
+ appWidgetHost.startAppWidgetConfigureActivityForResult(
+ eq(ownerActivity),
+ eq(123),
+ anyInt(),
+ eq(WidgetConfigurationController.REQUEST_CODE),
+ any()
+ )
+ )
+ .thenThrow(ActivityNotFoundException())
+
+ assertThat(underTest.configureWidget(123)).isFalse()
+ }
+ }
+
+ @Test
+ fun configurationFails() =
+ with(kosmos) {
+ testScope.runTest {
+ val result = async { underTest.configureWidget(123) }
+ runCurrent()
+ assertThat(result.isCompleted).isFalse()
+
+ underTest.setConfigurationResult(Activity.RESULT_CANCELED)
+ runCurrent()
+
+ assertThat(result.await()).isFalse()
+ result.cancel()
+ }
+ }
+
+ @Test
+ fun configurationSuccessful() =
+ with(kosmos) {
+ testScope.runTest {
+ val result = async { underTest.configureWidget(123) }
+ runCurrent()
+ assertThat(result.isCompleted).isFalse()
+
+ underTest.setConfigurationResult(Activity.RESULT_OK)
+ runCurrent()
+
+ assertThat(result.await()).isTrue()
+ result.cancel()
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
index 62d23152b77a..52305b1f5212 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
@@ -336,6 +336,7 @@ class DeviceEntryInteractorTest : SysuiTestCase() {
kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.None
)
+ runCurrent()
underTest.attemptDeviceEntry()
@@ -353,6 +354,7 @@ class DeviceEntryInteractorTest : SysuiTestCase() {
kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.None
)
+ runCurrent()
underTest.attemptDeviceEntry()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
new file mode 100644
index 000000000000..32943a19be28
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.deviceentry.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DeviceUnlockedInteractorTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val authenticationRepository = kosmos.fakeAuthenticationRepository
+ private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository
+
+ val underTest =
+ DeviceUnlockedInteractor(
+ applicationScope = testScope.backgroundScope,
+ authenticationInteractor = kosmos.authenticationInteractor,
+ deviceEntryRepository = deviceEntryRepository,
+ )
+
+ @Test
+ fun isDeviceUnlocked_whenUnlockedAndAuthMethodIsNone_isTrue() =
+ testScope.runTest {
+ val isUnlocked by collectLastValue(underTest.isDeviceUnlocked)
+
+ deviceEntryRepository.setUnlocked(true)
+ authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
+
+ assertThat(isUnlocked).isTrue()
+ }
+
+ @Test
+ fun isDeviceUnlocked_whenUnlockedAndAuthMethodIsPin_isTrue() =
+ testScope.runTest {
+ val isUnlocked by collectLastValue(underTest.isDeviceUnlocked)
+
+ deviceEntryRepository.setUnlocked(true)
+ authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+
+ assertThat(isUnlocked).isTrue()
+ }
+
+ @Test
+ fun isDeviceUnlocked_whenUnlockedAndAuthMethodIsSim_isFalse() =
+ testScope.runTest {
+ val isUnlocked by collectLastValue(underTest.isDeviceUnlocked)
+
+ deviceEntryRepository.setUnlocked(true)
+ authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Sim)
+
+ assertThat(isUnlocked).isFalse()
+ }
+
+ @Test
+ fun isDeviceUnlocked_whenLockedAndAuthMethodIsNone_isTrue() =
+ testScope.runTest {
+ val isUnlocked by collectLastValue(underTest.isDeviceUnlocked)
+
+ deviceEntryRepository.setUnlocked(false)
+ authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
+
+ assertThat(isUnlocked).isTrue()
+ }
+
+ @Test
+ fun isDeviceUnlocked_whenLockedAndAuthMethodIsPin_isFalse() =
+ testScope.runTest {
+ val isUnlocked by collectLastValue(underTest.isDeviceUnlocked)
+
+ deviceEntryRepository.setUnlocked(false)
+ authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+
+ assertThat(isUnlocked).isFalse()
+ }
+
+ @Test
+ fun isDeviceUnlocked_whenLockedAndAuthMethodIsSim_isFalse() =
+ testScope.runTest {
+ val isUnlocked by collectLastValue(underTest.isDeviceUnlocked)
+
+ deviceEntryRepository.setUnlocked(false)
+ authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Sim)
+
+ assertThat(isUnlocked).isFalse()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index d159986015a0..bf99a8687aa4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -20,6 +20,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
import com.android.systemui.kosmos.testScope
import com.android.systemui.power.data.repository.fakePowerRepository
import com.android.systemui.scene.data.repository.sceneContainerRepository
@@ -33,6 +34,7 @@ import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -69,6 +71,27 @@ class SceneInteractorTest : SysuiTestCase() {
}
@Test
+ fun changeScene_toGoneWhenUnl_doesNotThrow() =
+ testScope.runTest {
+ val desiredScene by collectLastValue(underTest.desiredScene)
+ assertThat(desiredScene).isEqualTo(SceneModel(SceneKey.Lockscreen))
+
+ kosmos.fakeDeviceEntryRepository.setUnlocked(true)
+ runCurrent()
+
+ underTest.changeScene(SceneModel(SceneKey.Gone), "reason")
+ assertThat(desiredScene).isEqualTo(SceneModel(SceneKey.Gone))
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun changeScene_toGoneWhenStillLocked_throws() =
+ testScope.runTest {
+ kosmos.fakeDeviceEntryRepository.setUnlocked(false)
+
+ underTest.changeScene(SceneModel(SceneKey.Gone), "reason")
+ }
+
+ @Test
fun onSceneChanged() =
testScope.runTest {
val desiredScene by collectLastValue(underTest.desiredScene)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index 4afa5f2a44b9..fc0df1288553 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -311,6 +311,10 @@ class SceneContainerStartableTest : SysuiTestCase() {
SceneKey.QuickSettings,
)
.forEachIndexed { index, sceneKey ->
+ if (sceneKey == SceneKey.Gone) {
+ kosmos.fakeDeviceEntryRepository.setUnlocked(true)
+ runCurrent()
+ }
sceneInteractor.changeScene(SceneModel(sceneKey), "reason")
runCurrent()
verify(sysUiState, times(index)).commitUpdate(Display.DEFAULT_DISPLAY)
@@ -420,6 +424,8 @@ class SceneContainerStartableTest : SysuiTestCase() {
}
// Changing to the Gone scene should report a successful unlock.
+ kosmos.fakeDeviceEntryRepository.setUnlocked(true)
+ runCurrent()
sceneInteractor.changeScene(SceneModel(SceneKey.Gone), "reason")
runCurrent()
verify(falsingCollector).onSuccessfulUnlock()
@@ -613,6 +619,8 @@ class SceneContainerStartableTest : SysuiTestCase() {
runCurrent()
verify(falsingCollector).onBouncerShown()
+ kosmos.fakeDeviceEntryRepository.setUnlocked(true)
+ runCurrent()
sceneInteractor.changeScene(SceneModel(SceneKey.Gone), "reason")
runCurrent()
verify(falsingCollector, times(2)).onBouncerHidden()
@@ -741,9 +749,15 @@ class SceneContainerStartableTest : SysuiTestCase() {
"Lockscreen cannot be disabled while having a secure authentication method"
}
}
+
+ check(initialSceneKey != SceneKey.Gone || isDeviceUnlocked) {
+ "Cannot start on the Gone scene and have the device be locked at the same time."
+ }
+
sceneContainerFlags.enabled = true
kosmos.fakeDeviceEntryRepository.setUnlocked(isDeviceUnlocked)
kosmos.fakeDeviceEntryRepository.setBypassEnabled(isBypassEnabled)
+ runCurrent()
val transitionStateFlow =
MutableStateFlow<ObservableTransitionState>(
ObservableTransitionState.Idle(SceneKey.Lockscreen)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index e9801652f060..251daffe2d91 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -157,9 +157,11 @@ class ShadeSceneViewModelTest : SysuiTestCase() {
testScope.runTest {
val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
+ kosmos.fakeDeviceEntryRepository.setUnlocked(true)
kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.None
)
+ runCurrent()
sceneInteractor.changeScene(SceneModel(SceneKey.Gone), "reason")
sceneInteractor.onSceneChanged(SceneModel(SceneKey.Gone), "reason")
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 19895897ef31..7fa35dbb0575 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1089,6 +1089,8 @@
<string name="cta_label_to_open_widget_picker">Add more widgets</string>
<!-- Text for the popup to be displayed after dismissing the CTA tile. [CHAR LIMIT=50] -->
<string name="popup_on_dismiss_cta_tile_text">Long press to customize widgets</string>
+ <!-- Label for the button which configures widgets [CHAR LIMIT=NONE] -->
+ <string name="edit_widget">Edit widget</string>
<!-- Description for the button that removes a widget on click. [CHAR LIMIT=50] -->
<string name="button_to_remove_widget">Remove</string>
<!-- Text for the button that launches the hub mode widget picker. [CHAR LIMIT=50] -->
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDisplayListener.kt b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDisplayListener.kt
index e9b637230ede..ca479f5e6b5f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDisplayListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDisplayListener.kt
@@ -21,6 +21,7 @@ import android.hardware.display.DisplayManager
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.os.Handler
import android.view.DisplayInfo
+import com.android.app.tracing.traceSection
import com.android.systemui.biometrics.BiometricDisplayListener.SensorType.Generic
/**
@@ -42,13 +43,15 @@ class BiometricDisplayListener(
override fun onDisplayAdded(displayId: Int) {}
override fun onDisplayRemoved(displayId: Int) {}
override fun onDisplayChanged(displayId: Int) {
- val rotationChanged = didRotationChange()
+ traceSection({ "BiometricDisplayListener($sensorType)#onDisplayChanged" }) {
+ val rotationChanged = didRotationChange()
- when (sensorType) {
- is SensorType.SideFingerprint -> onChanged()
- else -> {
- if (rotationChanged) {
- onChanged()
+ when (sensorType) {
+ is SensorType.SideFingerprint -> onChanged()
+ else -> {
+ if (rotationChanged) {
+ onChanged()
+ }
}
}
}
@@ -82,8 +85,8 @@ class BiometricDisplayListener(
* biometric prompt (and this object will likely change as multi-mode auth is added).
*/
sealed class SensorType {
- object Generic : SensorType()
- object UnderDisplayFingerprint : SensorType()
+ data object Generic : SensorType()
+ data object UnderDisplayFingerprint : SensorType()
data class SideFingerprint(
val properties: FingerprintSensorPropertiesInternal
) : SensorType()
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt
index aaccbc1d2f9e..792a7efeb109 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt
@@ -24,6 +24,7 @@ import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CHANGED
import android.os.Handler
import android.util.Size
import android.view.DisplayInfo
+import com.android.app.tracing.traceSection
import com.android.internal.util.ArrayUtils
import com.android.systemui.biometrics.shared.model.DisplayRotation
import com.android.systemui.biometrics.shared.model.toDisplayRotation
@@ -130,12 +131,17 @@ constructor(
override fun onDisplayAdded(displayId: Int) {}
override fun onDisplayChanged(displayId: Int) {
- val displayInfo = getDisplayInfo()
- trySendWithFailureLogging(
- displayInfo,
- TAG,
- "Error sending displayInfo to $displayInfo"
- )
+ traceSection(
+ "DisplayStateRepository" +
+ ".currentRotationDisplayListener#onDisplayChanged"
+ ) {
+ val displayInfo = getDisplayInfo()
+ trySendWithFailureLogging(
+ displayInfo,
+ TAG,
+ "Error sending displayInfo to $displayInfo"
+ )
+ }
}
}
displayManager.registerDisplayListener(
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
index 1c362e993509..3287ed4d4991 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
@@ -25,6 +25,7 @@ import com.android.systemui.communal.data.db.CommunalWidgetItem
import com.android.systemui.communal.shared.CommunalWidgetHost
import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
import com.android.systemui.communal.widgets.CommunalAppWidgetHost
+import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
@@ -54,7 +55,7 @@ interface CommunalWidgetRepository {
fun addWidget(
provider: ComponentName,
priority: Int,
- configureWidget: suspend (id: Int) -> Boolean
+ configurator: WidgetConfigurator? = null
) {}
/** Delete a widget by id from app widget service and the database. */
@@ -121,41 +122,48 @@ constructor(
override fun addWidget(
provider: ComponentName,
priority: Int,
- configureWidget: suspend (id: Int) -> Boolean
+ configurator: WidgetConfigurator?
) {
applicationScope.launch(bgDispatcher) {
val id = communalWidgetHost.allocateIdAndBindWidget(provider)
- if (id != null) {
- val configured =
- if (communalWidgetHost.requiresConfiguration(id)) {
- logger.i("Widget ${provider.flattenToString()} requires configuration.")
- try {
- configureWidget.invoke(id)
- } catch (ex: Exception) {
- // Cleanup the app widget id if an error happens during configuration.
- logger.e("Error during widget configuration, cleaning up id $id", ex)
- if (ex is CancellationException) {
- appWidgetHost.deleteAppWidgetId(id)
- // Re-throw cancellation to ensure the parent coroutine also gets
- // cancelled.
- throw ex
- } else {
- false
- }
+ if (id == null) {
+ logger.e("Failed to allocate widget id to ${provider.flattenToString()}")
+ return@launch
+ }
+ val info = communalWidgetHost.getAppWidgetInfo(id)
+ val configured =
+ if (
+ configurator != null &&
+ info != null &&
+ CommunalWidgetHost.requiresConfiguration(info)
+ ) {
+ logger.i("Widget ${provider.flattenToString()} requires configuration.")
+ try {
+ configurator.configureWidget(id)
+ } catch (ex: Exception) {
+ // Cleanup the app widget id if an error happens during configuration.
+ logger.e("Error during widget configuration, cleaning up id $id", ex)
+ if (ex is CancellationException) {
+ appWidgetHost.deleteAppWidgetId(id)
+ // Re-throw cancellation to ensure the parent coroutine also gets
+ // cancelled.
+ throw ex
+ } else {
+ false
}
- } else {
- logger.i("Skipping configuration for ${provider.flattenToString()}")
- true
}
- if (configured) {
- communalWidgetDao.addWidget(
- widgetId = id,
- provider = provider,
- priority = priority,
- )
} else {
- appWidgetHost.deleteAppWidgetId(id)
+ logger.i("Skipping configuration for ${provider.flattenToString()}")
+ true
}
+ if (configured) {
+ communalWidgetDao.addWidget(
+ widgetId = id,
+ provider = provider,
+ priority = priority,
+ )
+ } else {
+ appWidgetHost.deleteAppWidgetId(id)
}
logger.i("Added widget ${provider.flattenToString()} at position $priority.")
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 71523b9e750f..b6180cb03416 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -31,6 +31,7 @@ import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
import com.android.systemui.communal.widgets.CommunalAppWidgetHost
import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
+import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -148,17 +149,12 @@ constructor(
/** Dismiss the CTA tile from the hub in view mode. */
suspend fun dismissCtaTile() = communalPrefsRepository.setCtaDismissedForCurrentUser()
- /**
- * Add a widget at the specified position.
- *
- * @param configureWidget The callback to trigger if widget configuration is needed. Should
- * return whether configuration was successful.
- */
+ /** Add a widget at the specified position. */
fun addWidget(
componentName: ComponentName,
priority: Int,
- configureWidget: suspend (id: Int) -> Boolean
- ) = widgetRepository.addWidget(componentName, priority, configureWidget)
+ configurator: WidgetConfigurator?,
+ ) = widgetRepository.addWidget(componentName, priority, configurator)
/** Delete a widget by id. */
fun deleteWidget(id: Int) = widgetRepository.deleteWidget(id)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
index 0d52afd4fff5..acd6cb09e241 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
@@ -17,6 +17,7 @@
package com.android.systemui.communal.domain.model
import android.appwidget.AppWidgetProviderInfo
+import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE
import android.widget.RemoteViews
import com.android.systemui.communal.shared.model.CommunalContentSize
import com.android.systemui.communal.widgets.CommunalAppWidgetHost
@@ -41,7 +42,7 @@ sealed interface CommunalContentModel {
val createdTimestampMillis: Long
}
- class Widget(
+ data class Widget(
val appWidgetId: Int,
val providerInfo: AppWidgetProviderInfo,
val appWidgetHost: CommunalAppWidgetHost,
@@ -49,6 +50,12 @@ sealed interface CommunalContentModel {
override val key = KEY.widget(appWidgetId)
// Widget size is always half.
override val size = CommunalContentSize.HALF
+
+ /** Whether this widget can be reconfigured after it has already been added. */
+ val reconfigurable: Boolean
+ get() =
+ (providerInfo.widgetFeatures and WIDGET_FEATURE_RECONFIGURABLE != 0) &&
+ providerInfo.configure != null
}
/** A placeholder item representing a new widget being added */
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt
index 7fe37ccf0dc8..965c1e873279 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt
@@ -17,6 +17,7 @@
package com.android.systemui.communal.shared
import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProviderInfo
import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL
import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE
import android.content.ComponentName
@@ -24,6 +25,7 @@ import com.android.systemui.communal.widgets.CommunalAppWidgetHost
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
import com.android.systemui.log.dagger.CommunalLog
+import com.android.systemui.util.kotlin.getOrNull
import java.util.Optional
import javax.inject.Inject
@@ -40,6 +42,17 @@ constructor(
) {
companion object {
private const val TAG = "CommunalWidgetHost"
+
+ /** Returns whether a particular widget requires configuration when it is first added. */
+ fun requiresConfiguration(widgetInfo: AppWidgetProviderInfo): Boolean {
+ val featureFlags: Int = widgetInfo.widgetFeatures
+ // A widget's configuration is optional only if it's configuration is marked as optional
+ // AND it can be reconfigured later.
+ val configurationOptional =
+ (featureFlags and WIDGET_FEATURE_CONFIGURATION_OPTIONAL != 0 &&
+ featureFlags and WIDGET_FEATURE_RECONFIGURABLE != 0)
+ return widgetInfo.configure != null && !configurationOptional
+ }
}
private val logger = Logger(logBuffer, TAG)
@@ -66,22 +79,7 @@ constructor(
return false
}
- /**
- * Returns whether a particular widget requires configuration when it is first added.
- *
- * Must be called after the widget id has been bound.
- */
- fun requiresConfiguration(widgetId: Int): Boolean {
- if (appWidgetManager.isPresent) {
- val widgetInfo = appWidgetManager.get().getAppWidgetInfo(widgetId)
- val featureFlags: Int = widgetInfo.widgetFeatures
- // A widget's configuration is optional only if it's configuration is marked as optional
- // AND it can be reconfigured later.
- val configurationOptional =
- (featureFlags and WIDGET_FEATURE_CONFIGURATION_OPTIONAL != 0 &&
- featureFlags and WIDGET_FEATURE_RECONFIGURABLE != 0)
- return widgetInfo.configure != null && !configurationOptional
- }
- return false
+ fun getAppWidgetInfo(widgetId: Int): AppWidgetProviderInfo? {
+ return appWidgetManager.getOrNull()?.getAppWidgetInfo(widgetId)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt
index 75f9d809cc69..c5dac775c8a8 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt
@@ -39,7 +39,7 @@ import java.util.concurrent.Executor
import javax.inject.Inject
import javax.inject.Named
-/** Controller for managing the smartspace view on the dream */
+/** Controller for managing the smartspace view on the glanceable hub */
@SysUISingleton
class CommunalSmartspaceController
@Inject
@@ -125,7 +125,7 @@ constructor(
smartspaceManager.createSmartspaceSession(
SmartspaceConfig.Builder(context, UI_SURFACE_GLANCEABLE_HUB).build()
)
- Log.d(TAG, "Starting smartspace session for dream")
+ Log.d(TAG, "Starting smartspace session for communal")
newSession.addOnTargetsAvailableListener(uiExecutor, sessionListener)
this.session = newSession
@@ -153,7 +153,7 @@ constructor(
plugin?.registerSmartspaceEventNotifier(null)
plugin?.onTargetsAvailable(emptyList())
- Log.d(TAG, "Ending smartspace session for dream")
+ Log.d(TAG, "Ending smartspace session for communal")
}
fun addListener(listener: SmartspaceTargetListener) {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index 73bb0b0ec8a3..f1b16c555a6b 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -22,6 +22,7 @@ import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
+import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.media.controls.ui.MediaHost
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -64,16 +65,12 @@ abstract class BaseCommunalViewModel(
/**
* Called when a widget is added via drag and drop from the widget picker into the communal hub.
*/
- open fun onAddWidget(componentName: ComponentName, priority: Int) {
- communalInteractor.addWidget(componentName, priority, ::configureWidget)
- }
-
- /**
- * Called when a widget needs to be configured, with the id of the widget. The return value
- * should represent whether configuring the widget was successful.
- */
- protected open suspend fun configureWidget(widgetId: Int): Boolean {
- return true
+ open fun onAddWidget(
+ componentName: ComponentName,
+ priority: Int,
+ configurator: WidgetConfigurator? = null
+ ) {
+ communalInteractor.addWidget(componentName, priority, configurator)
}
/** A list of all the communal content to be displayed in the communal hub. */
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index 317dd4040e7c..c69fa6f74c94 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -16,27 +16,17 @@
package com.android.systemui.communal.ui.viewmodel
-import android.app.Activity
-import android.app.Activity.RESULT_CANCELED
-import android.app.Activity.RESULT_OK
-import android.app.ActivityOptions
-import android.content.ActivityNotFoundException
-import android.content.ComponentName
import android.widget.RemoteViews
import com.android.internal.logging.UiEventLogger
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.shared.log.CommunalUiEvent
-import com.android.systemui.communal.widgets.CommunalAppWidgetHost
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.media.controls.ui.MediaHost
import com.android.systemui.media.dagger.MediaModule
-import com.android.systemui.util.nullableAtomicReference
import javax.inject.Inject
import javax.inject.Named
-import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
@@ -48,26 +38,9 @@ class CommunalEditModeViewModel
@Inject
constructor(
private val communalInteractor: CommunalInteractor,
- private val appWidgetHost: CommunalAppWidgetHost,
@Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost,
private val uiEventLogger: UiEventLogger,
) : BaseCommunalViewModel(communalInteractor, mediaHost) {
-
- private companion object {
- private const val KEY_SPLASH_SCREEN_STYLE = "android.activity.splashScreenStyle"
- private const val SPLASH_SCREEN_STYLE_EMPTY = 0
- }
-
- private val _widgetsToConfigure = MutableSharedFlow<Int>()
-
- /**
- * Flow emitting ids of widgets which need to be configured. The consumer of this flow should
- * trigger [startConfigurationActivity] to initiate configuration.
- */
- val widgetsToConfigure: Flow<Int> = _widgetsToConfigure
-
- private var pendingConfiguration: CompletableDeferred<Int>? by nullableAtomicReference()
-
override val isEditMode = true
// Only widgets are editable. The CTA tile comes last in the list and remains visible.
@@ -92,57 +65,6 @@ constructor(
return RemoteViews.InteractionHandler { _, _, _ -> false }
}
- override fun onAddWidget(componentName: ComponentName, priority: Int) {
- if (pendingConfiguration != null) {
- throw IllegalStateException(
- "Cannot add $componentName widget while widget configuration is pending"
- )
- }
- super.onAddWidget(componentName, priority)
- }
-
- fun startConfigurationActivity(activity: Activity, widgetId: Int, requestCode: Int) {
- val options =
- ActivityOptions.makeBasic().apply {
- setPendingIntentBackgroundActivityStartMode(
- ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
- )
- }
- val bundle = options.toBundle()
- bundle.putInt(KEY_SPLASH_SCREEN_STYLE, SPLASH_SCREEN_STYLE_EMPTY)
- try {
- appWidgetHost.startAppWidgetConfigureActivityForResult(
- activity,
- widgetId,
- 0,
- // Use the widget id as the request code.
- requestCode,
- bundle
- )
- } catch (e: ActivityNotFoundException) {
- setConfigurationResult(RESULT_CANCELED)
- }
- }
-
- override suspend fun configureWidget(widgetId: Int): Boolean {
- if (pendingConfiguration != null) {
- throw IllegalStateException(
- "Attempting to configure $widgetId while another configuration is already active"
- )
- }
- pendingConfiguration = CompletableDeferred()
- _widgetsToConfigure.emit(widgetId)
- val resultCode = pendingConfiguration?.await() ?: RESULT_CANCELED
- pendingConfiguration = null
- return resultCode == RESULT_OK
- }
-
- /** Sets the result of widget configuration. */
- fun setConfigurationResult(resultCode: Int) {
- pendingConfiguration?.complete(resultCode)
- ?: throw IllegalStateException("No widget pending configuration")
- }
-
override fun onReorderWidgetStart() {
// Clear selection status
setSelectedIndex(null)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
index 380ed61a556d..c7a14f9eefe1 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -27,15 +27,11 @@ import android.view.WindowInsets
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
import com.android.internal.logging.UiEventLogger
import com.android.systemui.communal.shared.log.CommunalUiEvent
import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
import com.android.systemui.compose.ComposeFacade.setCommunalEditWidgetActivityContent
import javax.inject.Inject
-import kotlinx.coroutines.launch
/** An Activity for editing the widgets that appear in hub mode. */
class EditWidgetsActivity
@@ -44,15 +40,17 @@ constructor(
private val communalViewModel: CommunalEditModeViewModel,
private var windowManagerService: IWindowManager? = null,
private val uiEventLogger: UiEventLogger,
+ private val widgetConfiguratorFactory: WidgetConfigurationController.Factory
) : ComponentActivity() {
companion object {
private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag"
private const val EXTRA_FILTER_STRATEGY = "filter_strategy"
private const val FILTER_STRATEGY_GLANCEABLE_HUB = 1
- private const val REQUEST_CODE_CONFIGURE_WIDGET = 1
private const val TAG = "EditWidgetsActivity"
}
+ private val widgetConfigurator by lazy { widgetConfiguratorFactory.create(this) }
+
private val addWidgetActivityLauncher: ActivityResultLauncher<Intent> =
registerForActivityResult(StartActivityForResult()) { result ->
when (result.resultCode) {
@@ -71,7 +69,7 @@ constructor(
Intent.EXTRA_COMPONENT_NAME,
ComponentName::class.java
)
- ?.let { communalViewModel.onAddWidget(it, 0) }
+ ?.let { communalViewModel.onAddWidget(it, 0, widgetConfigurator) }
?: run { Log.w(TAG, "No AppWidgetProviderInfo found in result.") }
}
}
@@ -92,22 +90,10 @@ constructor(
windowInsetsController?.hide(WindowInsets.Type.systemBars())
window.setDecorFitsSystemWindows(false)
- lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.STARTED) {
- // Start the configuration activity when new widgets are added.
- communalViewModel.widgetsToConfigure.collect { widgetId ->
- communalViewModel.startConfigurationActivity(
- activity = this@EditWidgetsActivity,
- widgetId = widgetId,
- requestCode = REQUEST_CODE_CONFIGURE_WIDGET
- )
- }
- }
- }
-
setCommunalEditWidgetActivityContent(
activity = this,
viewModel = communalViewModel,
+ widgetConfigurator = widgetConfigurator,
onOpenWidgetPicker = {
val intent =
Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) }
@@ -145,8 +131,8 @@ constructor(
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
- if (requestCode == REQUEST_CODE_CONFIGURE_WIDGET) {
- communalViewModel.setConfigurationResult(resultCode)
+ if (requestCode == WidgetConfigurationController.REQUEST_CODE) {
+ widgetConfigurator.setConfigurationResult(resultCode)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetConfigurationController.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetConfigurationController.kt
new file mode 100644
index 000000000000..3e68479e717a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetConfigurationController.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.widgets
+
+import android.app.Activity
+import android.app.ActivityOptions
+import android.content.ActivityNotFoundException
+import android.window.SplashScreen
+import androidx.activity.ComponentActivity
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.nullableAtomicReference
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/**
+ * Handles starting widget configuration activities and receiving the response to determine if
+ * configuration was successful.
+ */
+class WidgetConfigurationController
+@AssistedInject
+constructor(
+ @Assisted private val activity: ComponentActivity,
+ private val appWidgetHost: CommunalAppWidgetHost,
+ @Background private val bgDispatcher: CoroutineDispatcher
+) : WidgetConfigurator {
+ @AssistedFactory
+ fun interface Factory {
+ fun create(activity: ComponentActivity): WidgetConfigurationController
+ }
+
+ private var result: CompletableDeferred<Boolean>? by nullableAtomicReference()
+
+ override suspend fun configureWidget(appWidgetId: Int): Boolean =
+ withContext(bgDispatcher) {
+ if (result != null) {
+ throw IllegalStateException("There is already a pending configuration")
+ }
+ result = CompletableDeferred()
+ val options =
+ ActivityOptions.makeBasic().apply {
+ pendingIntentBackgroundActivityStartMode =
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+ splashScreenStyle = SplashScreen.SPLASH_SCREEN_STYLE_SOLID_COLOR
+ }
+
+ try {
+ appWidgetHost.startAppWidgetConfigureActivityForResult(
+ activity,
+ appWidgetId,
+ 0,
+ REQUEST_CODE,
+ options.toBundle()
+ )
+ } catch (e: ActivityNotFoundException) {
+ setConfigurationResult(Activity.RESULT_CANCELED)
+ }
+ val value = result?.await() ?: false
+ result = null
+ return@withContext value
+ }
+
+ fun setConfigurationResult(resultCode: Int) {
+ result?.complete(resultCode == Activity.RESULT_OK)
+ }
+
+ companion object {
+ const val REQUEST_CODE = 100
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetConfigurator.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetConfigurator.kt
new file mode 100644
index 000000000000..916faa1b88ea
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetConfigurator.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.widgets
+
+/** Configurator which can be used to request a certain widget be reconfigured. */
+fun interface WidgetConfigurator {
+ /** Launch configuration for a widget, and return the result */
+ suspend fun configureWidget(appWidgetId: Int): Boolean
+}
diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
index acbdecc5d514..641064becf24 100644
--- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
+++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
@@ -25,6 +25,7 @@ import androidx.lifecycle.LifecycleOwner
import com.android.systemui.bouncer.ui.BouncerDialogFactory
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
+import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.scene.shared.model.Scene
@@ -64,6 +65,7 @@ interface BaseComposeFacade {
fun setCommunalEditWidgetActivityContent(
activity: ComponentActivity,
viewModel: BaseCommunalViewModel,
+ widgetConfigurator: WidgetConfigurator,
onOpenWidgetPicker: () -> Unit,
onEditDone: () -> Unit,
)
diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepository.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepository.kt
index 5bb6eece9098..074b9ff3ca1f 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepository.kt
@@ -17,23 +17,36 @@
package com.android.systemui.controls.panels
import android.content.ComponentName
+import android.os.UserHandle
import com.android.systemui.controls.ui.ControlsUiController
import com.android.systemui.controls.ui.SelectedItem
import com.android.systemui.flags.Flags
+import kotlinx.coroutines.flow.Flow
/** Stores user-selected preferred component. */
interface SelectedComponentRepository {
+ /** Returns current set preferred component for the specified user. */
+ fun selectedComponentFlow(userHandle: UserHandle): Flow<SelectedComponent?>
+
/**
- * Returns currently set preferred component, or null when nothing is set. Consider using
- * [ControlsUiController.getPreferredSelectedItem] to get domain specific data
+ * Returns the current set preferred component for the specified user, or null when nothing is
+ * set. If no user is specified, the current user's preference is used. This method by default
+ * operates in the context of the current user unless another user is explicitly specified.
+ * Consider using [ControlsUiController.getPreferredSelectedItem] to get domain specific data.
*/
- fun getSelectedComponent(): SelectedComponent?
+ fun getSelectedComponent(userHandle: UserHandle = UserHandle.CURRENT): SelectedComponent?
- /** Sets preferred component. Use [getSelectedComponent] to get current one */
+ /**
+ * Sets the preferred component for the current user. Use [getSelectedComponent] to retrieve the
+ * currently set preferred component. This method applies to the current user's settings.
+ */
fun setSelectedComponent(selectedComponent: SelectedComponent)
- /** Clears current preferred component. [getSelectedComponent] will return null afterwards */
+ /**
+ * Clears the current user's preferred component. After this operation, [getSelectedComponent]
+ * will return null for the current user.
+ */
fun removeSelectedComponent()
/**
diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepositoryImpl.kt
index c9edd4ac7426..0baa81a12e4f 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepositoryImpl.kt
@@ -19,13 +19,24 @@ package com.android.systemui.controls.panels
import android.content.ComponentName
import android.content.Context
import android.content.SharedPreferences
+import android.os.UserHandle
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.launch
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
@SysUISingleton
class SelectedComponentRepositoryImpl
@Inject
@@ -33,6 +44,8 @@ constructor(
private val userFileManager: UserFileManager,
private val userTracker: UserTracker,
private val featureFlags: FeatureFlags,
+ @Background private val bgDispatcher: CoroutineDispatcher,
+ @Application private val applicationScope: CoroutineScope
) : SelectedComponentRepository {
private companion object {
@@ -42,16 +55,42 @@ constructor(
const val SHOULD_ADD_DEFAULT_PANEL = "should_add_default_panel"
}
- private val sharedPreferences: SharedPreferences
- get() =
- userFileManager.getSharedPreferences(
- fileName = DeviceControlsControllerImpl.PREFS_CONTROLS_FILE,
- mode = Context.MODE_PRIVATE,
- userId = userTracker.userId
- )
+ private fun getSharedPreferencesForUser(userId: Int): SharedPreferences {
+ return userFileManager.getSharedPreferences(
+ fileName = DeviceControlsControllerImpl.PREFS_CONTROLS_FILE,
+ mode = Context.MODE_PRIVATE,
+ userId = userId
+ )
+ }
+
+ override fun selectedComponentFlow(
+ userHandle: UserHandle
+ ): Flow<SelectedComponentRepository.SelectedComponent?> {
+ return conflatedCallbackFlow {
+ val sharedPreferencesByUserId = getSharedPreferencesForUser(userHandle.identifier)
+ val listener =
+ SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
+ applicationScope.launch(bgDispatcher) {
+ if (key == PREF_COMPONENT) {
+ trySend(getSelectedComponent(userHandle))
+ }
+ }
+ }
+ sharedPreferencesByUserId.registerOnSharedPreferenceChangeListener(listener)
+ send(getSelectedComponent(userHandle))
+ awaitClose {
+ sharedPreferencesByUserId.unregisterOnSharedPreferenceChangeListener(listener)
+ }
+ }
+ .flowOn(bgDispatcher)
+ }
- override fun getSelectedComponent(): SelectedComponentRepository.SelectedComponent? {
- with(sharedPreferences) {
+ override fun getSelectedComponent(
+ userHandle: UserHandle
+ ): SelectedComponentRepository.SelectedComponent? {
+ val userId =
+ if (userHandle == UserHandle.CURRENT) userTracker.userId else userHandle.identifier
+ with(getSharedPreferencesForUser(userId)) {
val componentString = getString(PREF_COMPONENT, null) ?: return null
return SelectedComponentRepository.SelectedComponent(
name = getString(PREF_STRUCTURE_OR_APP_NAME, "")!!,
@@ -64,7 +103,7 @@ constructor(
override fun setSelectedComponent(
selectedComponent: SelectedComponentRepository.SelectedComponent
) {
- sharedPreferences
+ getSharedPreferencesForUser(userTracker.userId)
.edit()
.putString(PREF_COMPONENT, selectedComponent.componentName?.flattenToString())
.putString(PREF_STRUCTURE_OR_APP_NAME, selectedComponent.name)
@@ -73,7 +112,7 @@ constructor(
}
override fun removeSelectedComponent() {
- sharedPreferences
+ getSharedPreferencesForUser(userTracker.userId)
.edit()
.remove(PREF_COMPONENT)
.remove(PREF_STRUCTURE_OR_APP_NAME)
@@ -82,9 +121,12 @@ constructor(
}
override fun shouldAddDefaultComponent(): Boolean =
- sharedPreferences.getBoolean(SHOULD_ADD_DEFAULT_PANEL, true)
+ getSharedPreferencesForUser(userTracker.userId).getBoolean(SHOULD_ADD_DEFAULT_PANEL, true)
override fun setShouldAddDefaultComponent(shouldAdd: Boolean) {
- sharedPreferences.edit().putBoolean(SHOULD_ADD_DEFAULT_PANEL, shouldAdd).apply()
+ getSharedPreferencesForUser(userTracker.userId)
+ .edit()
+ .putBoolean(SHOULD_ADD_DEFAULT_PANEL, shouldAdd)
+ .apply()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
index 2680328d5d7e..09853578d3f1 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
@@ -61,6 +61,7 @@ constructor(
deviceEntryFaceAuthRepository: DeviceEntryFaceAuthRepository,
trustRepository: TrustRepository,
flags: SceneContainerFlags,
+ deviceUnlockedInteractor: DeviceUnlockedInteractor,
) {
val enteringDeviceFromBiometricUnlock: Flow<BiometricUnlockSource> =
repository.enteringDeviceFromBiometricUnlock
@@ -74,19 +75,7 @@ constructor(
* of this flow will always be `true`, even if the lockscreen is showing and still needs to be
* dismissed by the user to proceed.
*/
- val isUnlocked: StateFlow<Boolean> =
- combine(
- repository.isUnlocked,
- authenticationInteractor.authenticationMethod,
- ) { isUnlocked, authenticationMethod ->
- (!authenticationMethod.isSecure || isUnlocked) &&
- authenticationMethod != AuthenticationMethodModel.Sim
- }
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.Eagerly,
- initialValue = false,
- )
+ val isUnlocked: StateFlow<Boolean> = deviceUnlockedInteractor.isDeviceUnlocked
/**
* Whether the device has been entered (i.e. the lockscreen has been dismissed, by any method).
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt
new file mode 100644
index 000000000000..b0495fb8e819
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.deviceentry.domain.interactor
+
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+
+@SysUISingleton
+class DeviceUnlockedInteractor
+@Inject
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ authenticationInteractor: AuthenticationInteractor,
+ deviceEntryRepository: DeviceEntryRepository,
+) {
+
+ /**
+ * Whether the device is unlocked.
+ *
+ * A device that is not yet unlocked requires unlocking by completing an authentication
+ * challenge according to the current authentication method, unless in cases when the current
+ * authentication method is not "secure" (for example, None and Swipe); in such cases, the value
+ * of this flow will always be `true`, even if the lockscreen is showing and still needs to be
+ * dismissed by the user to proceed.
+ */
+ val isDeviceUnlocked: StateFlow<Boolean> =
+ combine(
+ deviceEntryRepository.isUnlocked,
+ authenticationInteractor.authenticationMethod,
+ ) { isUnlocked, authenticationMethod ->
+ (!authenticationMethod.isSecure || isUnlocked) &&
+ authenticationMethod != AuthenticationMethodModel.Sim
+ }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.Eagerly,
+ initialValue = false,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapterDelegate.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapterDelegate.java
index 92f17f9db0f4..e098929c1b1d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapterDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapterDelegate.java
@@ -119,6 +119,8 @@ class TileAdapterDelegate extends AccessibilityDelegateCompat {
info.removeAction(listOfActions.get(i));
}
}
+ // We really don't want it to be clickable in this case.
+ info.setClickable(false);
return;
}
@@ -126,6 +128,7 @@ class TileAdapterDelegate extends AccessibilityDelegateCompat {
new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
AccessibilityNodeInfo.ACTION_CLICK, clickActionString);
info.addAction(action);
+ info.setClickable(true);
}
private void maybeAddActionMoveToPosition(
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index 0abde4d5c3f4..b3d2e0918db6 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -18,6 +18,7 @@ package com.android.systemui.scene.domain.interactor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.scene.data.repository.SceneContainerRepository
import com.android.systemui.scene.shared.logger.SceneLogger
@@ -50,6 +51,7 @@ constructor(
private val repository: SceneContainerRepository,
private val powerInteractor: PowerInteractor,
private val logger: SceneLogger,
+ private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
) {
/**
@@ -222,6 +224,11 @@ constructor(
loggingReason: String,
log: (from: SceneKey, to: SceneKey, loggingReason: String) -> Unit,
) {
+ check(scene.key != SceneKey.Gone || deviceUnlockedInteractor.isDeviceUnlocked.value) {
+ "Cannot change to the Gone scene while the device is locked. Logging reason for scene" +
+ " change was: $loggingReason"
+ }
+
val currentSceneKey = desiredScene.value.key
if (currentSceneKey == scene.key) {
return
diff --git a/packages/SystemUI/src/com/android/systemui/settings/DisplayTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/DisplayTrackerImpl.kt
index 68cc483fbe80..2ef27a8df117 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/DisplayTrackerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/DisplayTrackerImpl.kt
@@ -23,6 +23,7 @@ import android.view.Display
import androidx.annotation.GuardedBy
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
+import com.android.app.tracing.traceSection
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.util.Assert
import java.lang.ref.WeakReference
@@ -46,18 +47,30 @@ internal constructor(
val displayChangedListener: DisplayManager.DisplayListener =
object : DisplayManager.DisplayListener {
override fun onDisplayAdded(displayId: Int) {
- val list = synchronized(displayCallbacks) { displayCallbacks.toList() }
- onDisplayAdded(displayId, list)
+ traceSection(
+ "DisplayTrackerImpl.displayChangedDisplayListener#onDisplayAdded",
+ ) {
+ val list = synchronized(displayCallbacks) { displayCallbacks.toList() }
+ onDisplayAdded(displayId, list)
+ }
}
override fun onDisplayRemoved(displayId: Int) {
- val list = synchronized(displayCallbacks) { displayCallbacks.toList() }
- onDisplayRemoved(displayId, list)
+ traceSection(
+ "DisplayTrackerImpl.displayChangedDisplayListener#onDisplayRemoved",
+ ) {
+ val list = synchronized(displayCallbacks) { displayCallbacks.toList() }
+ onDisplayRemoved(displayId, list)
+ }
}
override fun onDisplayChanged(displayId: Int) {
- val list = synchronized(displayCallbacks) { displayCallbacks.toList() }
- onDisplayChanged(displayId, list)
+ traceSection(
+ "DisplayTrackerImpl.displayChangedDisplayListener#onDisplayChanged",
+ ) {
+ val list = synchronized(displayCallbacks) { displayCallbacks.toList() }
+ onDisplayChanged(displayId, list)
+ }
}
}
@@ -69,8 +82,12 @@ internal constructor(
override fun onDisplayRemoved(displayId: Int) {}
override fun onDisplayChanged(displayId: Int) {
- val list = synchronized(brightnessCallbacks) { brightnessCallbacks.toList() }
- onDisplayChanged(displayId, list)
+ traceSection(
+ "DisplayTrackerImpl.displayBrightnessChangedDisplayListener#onDisplayChanged",
+ ) {
+ val list = synchronized(brightnessCallbacks) { brightnessCallbacks.toList() }
+ onDisplayChanged(displayId, list)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
index fa2748c1dc77..6e2beb45f3f2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
@@ -61,7 +61,8 @@ internal constructor(
controller.setNotifStats(notifStats)
if (NotificationIconContainerRefactor.isEnabled || FooterViewRefactor.isEnabled) {
renderListInteractor.setRenderedList(entries)
- } else {
+ }
+ if (!NotificationIconContainerRefactor.isEnabled) {
notificationIconAreaController.updateNotificationIcons(entries)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
index 695f21569f3c..ab54bdad66c1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
@@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.domain.interactor
import android.graphics.drawable.Icon
import android.util.ArrayMap
+import com.android.app.tracing.traceSection
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntry
@@ -44,10 +45,12 @@ constructor(
* Sets the current list of rendered notification entries as displayed in the notification list.
*/
fun setRenderedList(entries: List<ListEntry>) {
- repository.activeNotifications.update { existingModels ->
- buildActiveNotificationsStore(existingModels, sectionStyleProvider) {
- entries.forEach(::addListEntry)
- setRankingsMap(entries)
+ traceSection("RenderNotificationListInteractor.setRenderedList") {
+ repository.activeNotifications.update { existingModels ->
+ buildActiveNotificationsStore(existingModels, sectionStyleProvider) {
+ entries.forEach(::addListEntry)
+ setRankingsMap(entries)
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
index e57c0e7f1044..9bfc4ce49cc7 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
@@ -476,10 +476,15 @@ public class ImageWallpaper extends WallpaperService {
@Override
public void onDisplayChanged(int displayId) {
- // changes the display in the color extractor
- // the new display dimensions will be used in the next color computation
- if (displayId == getDisplayContext().getDisplayId()) {
- getDisplaySizeAndUpdateColorExtractor();
+ Trace.beginSection("ImageWallpaper.CanvasEngine#onDisplayChanged");
+ try {
+ // changes the display in the color extractor
+ // the new display dimensions will be used in the next color computation
+ if (displayId == getDisplayContext().getDisplayId()) {
+ getDisplaySizeAndUpdateColorExtractor();
+ }
+ } finally {
+ Trace.endSection();
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
index a7677cca9f29..002862e949ba 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
@@ -16,27 +16,55 @@
package com.android.systemui.controls.panels
-class FakeSelectedComponentRepository : SelectedComponentRepository {
+import android.os.UserHandle
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
- private var selectedComponent: SelectedComponentRepository.SelectedComponent? = null
+class FakeSelectedComponentRepository : SelectedComponentRepository {
private var shouldAddDefaultPanel: Boolean = true
+ private val _selectedComponentFlows =
+ mutableMapOf<UserHandle, MutableStateFlow<SelectedComponentRepository.SelectedComponent?>>()
+ private var currentUserHandle: UserHandle = UserHandle.of(0)
- override fun getSelectedComponent(): SelectedComponentRepository.SelectedComponent? =
- selectedComponent
+ override fun selectedComponentFlow(
+ userHandle: UserHandle
+ ): Flow<SelectedComponentRepository.SelectedComponent?> {
+ // Return an existing flow for the user or create a new one
+ return _selectedComponentFlows.getOrPut(getUserHandle(userHandle)) {
+ MutableStateFlow(null)
+ }
+ }
+
+ override fun getSelectedComponent(
+ userHandle: UserHandle
+ ): SelectedComponentRepository.SelectedComponent? {
+ return _selectedComponentFlows[getUserHandle(userHandle)]?.value
+ }
override fun setSelectedComponent(
selectedComponent: SelectedComponentRepository.SelectedComponent
) {
- this.selectedComponent = selectedComponent
+ val flow = _selectedComponentFlows.getOrPut(currentUserHandle) { MutableStateFlow(null) }
+ flow.value = selectedComponent
}
override fun removeSelectedComponent() {
- selectedComponent = null
+ _selectedComponentFlows[currentUserHandle]?.value = null
}
-
override fun shouldAddDefaultComponent(): Boolean = shouldAddDefaultPanel
override fun setShouldAddDefaultComponent(shouldAdd: Boolean) {
shouldAddDefaultPanel = shouldAdd
}
+
+ fun setCurrentUserHandle(userHandle: UserHandle) {
+ currentUserHandle = userHandle
+ }
+ private fun getUserHandle(userHandle: UserHandle): UserHandle {
+ return if (userHandle == UserHandle.CURRENT) {
+ currentUserHandle
+ } else {
+ userHandle
+ }
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt
index 6230ea7ecd31..b463adf40a91 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt
@@ -18,28 +18,43 @@ package com.android.systemui.controls.panels
import android.content.ComponentName
import android.content.SharedPreferences
+import android.os.UserHandle
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl
+import com.android.systemui.testKosmos
import com.android.systemui.util.FakeSharedPreferences
-import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
+import java.io.File
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
+@ExperimentalCoroutinesApi
@RunWith(AndroidTestingRunner::class)
@SmallTest
class SelectedComponentRepositoryTest : SysuiTestCase() {
private companion object {
+ const val PREF_COMPONENT = "controls_component"
+ const val PREF_STRUCTURE_OR_APP_NAME = "controls_structure"
+ const val PREF_IS_PANEL = "controls_is_panel"
+ val PRIMARY_USER: UserHandle = UserHandle.of(0)
+ val SECONDARY_USER: UserHandle = UserHandle.of(12)
val COMPONENT_A =
SelectedComponentRepository.SelectedComponent(
name = "a",
@@ -53,24 +68,40 @@ class SelectedComponentRepositoryTest : SysuiTestCase() {
isPanel = false,
)
}
+ private lateinit var primaryUserSharedPref: FakeSharedPreferences
+ private lateinit var secondaryUserSharedPref: FakeSharedPreferences
@Mock private lateinit var userTracker: UserTracker
- @Mock private lateinit var userFileManager: UserFileManager
+ private lateinit var userFileManager: UserFileManager
private val featureFlags = FakeFeatureFlags()
- private val sharedPreferences: SharedPreferences = FakeSharedPreferences()
-
// under test
private lateinit var repository: SelectedComponentRepository
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- whenever(userFileManager.getSharedPreferences(any(), any(), any()))
- .thenReturn(sharedPreferences)
+ private val kosmos = testKosmos()
- repository = SelectedComponentRepositoryImpl(userFileManager, userTracker, featureFlags)
- }
+ @Before
+ fun setUp() =
+ with(kosmos) {
+ primaryUserSharedPref = FakeSharedPreferences()
+ secondaryUserSharedPref = FakeSharedPreferences()
+ MockitoAnnotations.initMocks(this@SelectedComponentRepositoryTest)
+ userFileManager =
+ FakeUserFileManager(
+ mapOf(
+ PRIMARY_USER.identifier to primaryUserSharedPref,
+ SECONDARY_USER.identifier to secondaryUserSharedPref
+ )
+ )
+ repository =
+ SelectedComponentRepositoryImpl(
+ userFileManager,
+ userTracker,
+ featureFlags,
+ bgDispatcher = testDispatcher,
+ applicationScope = applicationCoroutineScope
+ )
+ }
@Test
fun testUnsetIsNull() {
@@ -115,18 +146,10 @@ class SelectedComponentRepositoryTest : SysuiTestCase() {
@Test
fun testGetPreferredStructure_differentUserId() {
- sharedPreferences.savePanel(COMPONENT_A)
- whenever(
- userFileManager.getSharedPreferences(
- DeviceControlsControllerImpl.PREFS_CONTROLS_FILE,
- 0,
- 1,
- )
- )
- .thenReturn(FakeSharedPreferences().also { it.savePanel(COMPONENT_B) })
-
+ primaryUserSharedPref.savePanel(COMPONENT_A)
+ secondaryUserSharedPref.savePanel(COMPONENT_B)
val previousPreferredStructure = repository.getSelectedComponent()
- whenever(userTracker.userId).thenReturn(1)
+ whenever(userTracker.userId).thenReturn(SECONDARY_USER.identifier)
val currentPreferredStructure = repository.getSelectedComponent()
assertThat(previousPreferredStructure).isEqualTo(COMPONENT_A)
@@ -134,11 +157,90 @@ class SelectedComponentRepositoryTest : SysuiTestCase() {
assertThat(currentPreferredStructure).isEqualTo(COMPONENT_B)
}
+ @Test
+ fun testEmitValueFromGetSelectedComponent() =
+ with(kosmos) {
+ testScope.runTest {
+ primaryUserSharedPref.savePanel(COMPONENT_A)
+ val emittedValue by collectLastValue(repository.selectedComponentFlow(PRIMARY_USER))
+ assertThat(emittedValue).isEqualTo(COMPONENT_A)
+ }
+ }
+
+ @Test
+ fun testEmitNullWhenRemoveSelectedComponentIsCalled() =
+ with(kosmos) {
+ testScope.runTest {
+ primaryUserSharedPref.savePanel(COMPONENT_A)
+ primaryUserSharedPref.removePanel()
+ val emittedValue by collectLastValue(repository.selectedComponentFlow(PRIMARY_USER))
+ assertThat(emittedValue).isEqualTo(null)
+ }
+ }
+
+ @Test
+ fun testChangeEmitValueChangeWhenANewComponentIsSelected() =
+ with(kosmos) {
+ testScope.runTest {
+ primaryUserSharedPref.savePanel(COMPONENT_A)
+ val emittedValue by collectLastValue(repository.selectedComponentFlow(PRIMARY_USER))
+ advanceUntilIdle()
+ assertThat(emittedValue).isEqualTo(COMPONENT_A)
+ primaryUserSharedPref.savePanel(COMPONENT_B)
+ advanceUntilIdle()
+ assertThat(emittedValue).isEqualTo(COMPONENT_B)
+ }
+ }
+
+ @Test
+ fun testDifferentUsersWithDifferentComponentSelected() =
+ with(kosmos) {
+ testScope.runTest {
+ primaryUserSharedPref.savePanel(COMPONENT_A)
+ secondaryUserSharedPref.savePanel(COMPONENT_B)
+ val primaryUserValue by
+ collectLastValue(repository.selectedComponentFlow(PRIMARY_USER))
+ val secondaryUserValue by
+ collectLastValue(repository.selectedComponentFlow(SECONDARY_USER))
+ assertThat(primaryUserValue).isEqualTo(COMPONENT_A)
+ assertThat(secondaryUserValue).isEqualTo(COMPONENT_B)
+ }
+ }
+
private fun SharedPreferences.savePanel(panel: SelectedComponentRepository.SelectedComponent) {
edit()
- .putString("controls_component", panel.componentName?.flattenToString())
- .putString("controls_structure", panel.name)
- .putBoolean("controls_is_panel", panel.isPanel)
+ .putString(PREF_COMPONENT, panel.componentName?.flattenToString())
+ .putString(PREF_STRUCTURE_OR_APP_NAME, panel.name)
+ .putBoolean(PREF_IS_PANEL, panel.isPanel)
+ .commit()
+ }
+
+ private fun SharedPreferences.removePanel() {
+ edit()
+ .remove(PREF_COMPONENT)
+ .remove(PREF_STRUCTURE_OR_APP_NAME)
+ .remove(PREF_IS_PANEL)
.commit()
}
+
+ private class FakeUserFileManager(private val sharedPrefs: Map<Int, SharedPreferences>) :
+ UserFileManager {
+ override fun getFile(fileName: String, userId: Int): File {
+ throw UnsupportedOperationException()
+ }
+
+ override fun getSharedPreferences(
+ fileName: String,
+ mode: Int,
+ userId: Int
+ ): SharedPreferences {
+ if (fileName != DeviceControlsControllerImpl.PREFS_CONTROLS_FILE) {
+ throw IllegalArgumentException(
+ "Preference files must be " +
+ "$DeviceControlsControllerImpl.PREFS_CONTROLS_FILE"
+ )
+ }
+ return sharedPrefs.getValue(userId)
+ }
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java
index 6cad985c7b57..6e2f5db2eeda 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java
@@ -31,8 +31,8 @@ import android.view.accessibility.AccessibilityNodeInfo;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.test.filters.SmallTest;
-import com.android.systemui.res.R;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.res.R;
import org.junit.Before;
import org.junit.Test;
@@ -136,6 +136,7 @@ public class TileAdapterDelegateTest extends SysuiTestCase {
AccessibilityNodeInfoCompat.AccessibilityActionCompat action =
getActionForId(mInfo, AccessibilityNodeInfo.ACTION_CLICK);
assertThat(action.getLabel().toString()).contains(expectedString);
+ assertThat(mInfo.isClickable()).isTrue();
}
@Test
@@ -152,10 +153,11 @@ public class TileAdapterDelegateTest extends SysuiTestCase {
AccessibilityNodeInfoCompat.AccessibilityActionCompat action =
getActionForId(mInfo, AccessibilityNodeInfo.ACTION_CLICK);
assertThat(action.getLabel().toString()).contains(expectedString);
+ assertThat(mInfo.isClickable()).isTrue();
}
@Test
- public void testNoClickAction() {
+ public void testNoClickActionAndNotClickable() {
mView.setTag(mHolder);
when(mHolder.canTakeAccessibleAction()).thenReturn(true);
when(mHolder.canAdd()).thenReturn(false);
@@ -167,6 +169,7 @@ public class TileAdapterDelegateTest extends SysuiTestCase {
AccessibilityNodeInfoCompat.AccessibilityActionCompat action =
getActionForId(mInfo, AccessibilityNodeInfo.ACTION_CLICK);
assertThat(action).isNull();
+ assertThat(mInfo.isClickable()).isFalse();
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index a894f877fe3c..11da2376328a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -188,7 +188,8 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase {
mTestScope.getBackgroundScope(),
mKosmos.getFakeSceneContainerConfig()),
powerInteractor,
- mock(SceneLogger.class));
+ mock(SceneLogger.class),
+ mKosmos.getDeviceUnlockedInteractor());
FakeConfigurationRepository configurationRepository = new FakeConfigurationRepository();
FakeSceneContainerFlags sceneContainerFlags = new FakeSceneContainerFlags();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
index cbd4d2bfe377..8f46a37bf540 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
@@ -220,7 +220,8 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase {
mTestScope.getBackgroundScope(),
mKosmos.getFakeSceneContainerConfig()),
powerInteractor,
- mock(SceneLogger.class));
+ mock(SceneLogger.class),
+ mKosmos.getDeviceUnlockedInteractor());
FakeSceneContainerFlags sceneContainerFlags = new FakeSceneContainerFlags();
KeyguardInteractor keyguardInteractor = new KeyguardInteractor(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
index 9b4a100a1d64..a1daff14eea6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
@@ -81,7 +81,7 @@ class StackCoordinatorTest : SysuiTestCase() {
}
@Test
- @DisableFlags(NotificationIconContainerRefactor.FLAG_NAME, FooterViewRefactor.FLAG_NAME)
+ @DisableFlags(NotificationIconContainerRefactor.FLAG_NAME)
fun testUpdateNotificationIcons() {
afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
verify(notificationIconAreaController).updateNotificationIcons(eq(listOf(entry)))
@@ -89,7 +89,14 @@ class StackCoordinatorTest : SysuiTestCase() {
@Test
@EnableFlags(NotificationIconContainerRefactor.FLAG_NAME)
- fun testSetRenderedListOnInteractor() {
+ fun testSetRenderedListOnInteractor_iconContainerFlagOn() {
+ afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
+ verify(renderListInteractor).setRenderedList(eq(listOf(entry)))
+ }
+
+ @Test
+ @EnableFlags(FooterViewRefactor.FLAG_NAME)
+ fun testSetRenderedListOnInteractor_footerFlagOn() {
afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
verify(renderListInteractor).setRenderedList(eq(listOf(entry)))
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 1ed045ff6546..589f7c23ea13 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -419,7 +419,8 @@ public class BubblesTest extends SysuiTestCase {
mTestScope.getBackgroundScope(),
mKosmos.getFakeSceneContainerConfig()),
powerInteractor,
- mock(SceneLogger.class));
+ mock(SceneLogger.class),
+ mKosmos.getDeviceUnlockedInteractor());
FakeSceneContainerFlags sceneContainerFlags = new FakeSceneContainerFlags();
KeyguardInteractor keyguardInteractor = new KeyguardInteractor(
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
index d0c2d4b82fb3..e25e8c099c21 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
@@ -1,11 +1,11 @@
package com.android.systemui.communal.data.repository
+import android.appwidget.AppWidgetProviderInfo
import android.content.ComponentName
import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
+import com.android.systemui.communal.widgets.WidgetConfigurator
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
@@ -14,8 +14,6 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) :
CommunalWidgetRepository {
private val _communalWidgets = MutableStateFlow<List<CommunalWidgetContentModel>>(emptyList())
override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = _communalWidgets
- private val _widgetAdded = MutableSharedFlow<Int>()
- val widgetAdded: Flow<Int> = _widgetAdded
private var nextWidgetId = 1
@@ -26,16 +24,22 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) :
override fun addWidget(
provider: ComponentName,
priority: Int,
- configureWidget: suspend (id: Int) -> Boolean
+ configurator: WidgetConfigurator?
) {
coroutineScope.launch {
val id = nextWidgetId++
- if (configureWidget.invoke(id)) {
- _widgetAdded.emit(id)
+ val providerInfo = AppWidgetProviderInfo().apply { this.provider = provider }
+ val configured = configurator?.configureWidget(id) ?: true
+ if (configured) {
+ onConfigured(id, providerInfo, priority)
}
}
}
+ private fun onConfigured(id: Int, providerInfo: AppWidgetProviderInfo, priority: Int) {
+ _communalWidgets.value += listOf(CommunalWidgetContentModel(id, providerInfo, priority))
+ }
+
private var isHostActive = false
override fun updateAppWidgetHostActive(active: Boolean) {
isHostActive = active
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/widgets/FakeWidgetConfigurator.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/widgets/FakeWidgetConfigurator.kt
new file mode 100644
index 000000000000..662303e27ba9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/widgets/FakeWidgetConfigurator.kt
@@ -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.systemui.communal.widgets
+
+import kotlinx.coroutines.CompletableDeferred
+
+class FakeWidgetConfigurator(private val immediateResult: Boolean? = null) : WidgetConfigurator {
+ private val result: CompletableDeferred<Boolean> =
+ immediateResult?.let { CompletableDeferred(it) } ?: CompletableDeferred()
+
+ override suspend fun configureWidget(appWidgetId: Int): Boolean = result.await()
+
+ fun setResult(success: Boolean) {
+ result.complete(success)
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/widgets/WidgetConfiguratorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/widgets/WidgetConfiguratorKosmos.kt
new file mode 100644
index 000000000000..7bb86afd1d08
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/widgets/WidgetConfiguratorKosmos.kt
@@ -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.systemui.communal.widgets
+
+import com.android.systemui.kosmos.Kosmos
+
+/** A fake configurator which always successfully configures */
+val Kosmos.widgetConfiguratorSuccess: WidgetConfigurator by
+ Kosmos.Fixture { FakeWidgetConfigurator(true) }
+
+/** A fake configurator which always fails to configures */
+val Kosmos.widgetConfiguratorFail: WidgetConfigurator by
+ Kosmos.Fixture { FakeWidgetConfigurator(false) }
+
+/** A fake configurator whose result can be set programmatically in a test */
+val Kosmos.fakeWidgetConfigurator by Kosmos.Fixture { FakeWidgetConfigurator() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt
index b600b50b8d2d..8dcdd3a9425c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt
@@ -38,5 +38,6 @@ val Kosmos.deviceEntryInteractor by
deviceEntryFaceAuthRepository = deviceEntryFaceAuthRepository,
trustRepository = trustRepository,
flags = sceneContainerFlags,
+ deviceUnlockedInteractor = deviceUnlockedInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt
new file mode 100644
index 000000000000..df1cdc2f72cb
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.deviceentry.domain.interactor
+
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.deviceentry.data.repository.deviceEntryRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.applicationCoroutineScope
+
+val Kosmos.deviceUnlockedInteractor by Fixture {
+ DeviceUnlockedInteractor(
+ applicationScope = applicationCoroutineScope,
+ authenticationInteractor = authenticationInteractor,
+ deviceEntryRepository = deviceEntryRepository,
+ )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
index 24670b12193a..cc0449d7e7bb 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
package com.android.systemui.kosmos
import android.content.applicationContext
@@ -24,6 +26,8 @@ import com.android.systemui.classifier.falsingCollector
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.common.ui.domain.interactor.configurationInteractor
import com.android.systemui.communal.data.repository.fakeCommunalRepository
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.jank.interactionJankMonitor
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
@@ -36,6 +40,7 @@ import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
import com.android.systemui.statusbar.phone.screenOffAnimationController
import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
import com.android.systemui.util.time.systemClock
+import kotlinx.coroutines.ExperimentalCoroutinesApi
/** Helper for using [Kosmos] from Java. */
@Deprecated("Please convert your test to Kotlin and use [Kosmos] directly.")
@@ -65,6 +70,8 @@ class KosmosJavaAdapter(
val sceneInteractor by lazy { kosmos.sceneInteractor }
val falsingCollector by lazy { kosmos.falsingCollector }
val powerInteractor by lazy { kosmos.powerInteractor }
+ val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor }
+ val deviceUnlockedInteractor by lazy { kosmos.deviceUnlockedInteractor }
init {
kosmos.applicationContext = testCase.context
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt
index 998987602234..fc023758fdf6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt
@@ -16,6 +16,7 @@
package com.android.systemui.scene.domain.interactor
+import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.power.domain.interactor.powerInteractor
@@ -29,5 +30,6 @@ val Kosmos.sceneInteractor by
repository = sceneContainerRepository,
powerInteractor = powerInteractor,
logger = sceneLogger,
+ deviceUnlockedInteractor = deviceUnlockedInteractor,
)
}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt
index 82ea362e8049..bb91f9b8cf0b 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt
@@ -20,6 +20,7 @@ import android.content.Context
import android.hardware.display.DisplayManager
import android.os.Handler
import android.os.RemoteException
+import android.os.Trace
import com.android.systemui.unfold.util.CallbackController
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@@ -86,14 +87,19 @@ constructor(
private inner class RotationDisplayListener : DisplayManager.DisplayListener {
override fun onDisplayChanged(displayId: Int) {
- val display = context.display ?: return
-
- if (displayId == display.displayId) {
- val currentRotation = display.rotation
- if (lastRotation == null || lastRotation != currentRotation) {
- listeners.forEach { it.onRotationChanged(currentRotation) }
- lastRotation = currentRotation
+ Trace.beginSection("RotationChangeProvider.RotationDisplayListener#onDisplayChanged")
+ try {
+ val display = context.display ?: return
+
+ if (displayId == display.displayId) {
+ val currentRotation = display.rotation
+ if (lastRotation == null || lastRotation != currentRotation) {
+ listeners.forEach { it.onRotationChanged(currentRotation) }
+ lastRotation = currentRotation
+ }
}
+ } finally {
+ Trace.endSection()
}
}
diff --git a/packages/WallpaperBackup/AndroidManifest.xml b/packages/WallpaperBackup/AndroidManifest.xml
index c548101080b4..3ce97cdb4c07 100644
--- a/packages/WallpaperBackup/AndroidManifest.xml
+++ b/packages/WallpaperBackup/AndroidManifest.xml
@@ -17,11 +17,19 @@
*/
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.wallpaperbackup"
- android:sharedUserId="android.uid.system" >
+ package="com.android.wallpaperbackup" >
+
+ <uses-permission android:name="android.permission.READ_WALLPAPER_INTERNAL" />
+ <uses-permission android:name="android.permission.SET_WALLPAPER_COMPONENT" />
+ <uses-permission android:name="android.permission.SET_WALLPAPER" />
+
+ <queries>
+ <intent>
+ <action android:name="android.service.wallpaper.WallpaperService" />
+ </intent>
+ </queries>
<application android:allowClearUserData="false"
- android:process="system"
android:killAfterRestore="false"
android:allowBackup="true"
android:backupInForeground="true"
diff --git a/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java b/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java
index 98421a9e1d3e..f31eb44f23f5 100644
--- a/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java
+++ b/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java
@@ -18,11 +18,13 @@ package com.android.wallpaperbackup;
import static android.app.WallpaperManager.FLAG_LOCK;
import static android.app.WallpaperManager.FLAG_SYSTEM;
+import static android.app.WallpaperManager.ORIENTATION_UNKNOWN;
import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_INELIGIBLE;
import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_NO_METADATA;
import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_NO_WALLPAPER;
import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_QUOTA_EXCEEDED;
+import static com.android.window.flags.Flags.multiCrop;
import android.app.AppGlobals;
import android.app.WallpaperManager;
@@ -43,7 +45,9 @@ import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings;
+import android.util.Pair;
import android.util.Slog;
+import android.util.SparseArray;
import android.util.Xml;
import com.android.internal.annotations.VisibleForTesting;
@@ -55,6 +59,7 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.util.List;
/**
* Backs up and restores wallpaper and metadata related to it.
@@ -432,6 +437,27 @@ public class WallpaperBackupAgent extends BackupAgent {
private void restoreFromStage(File stage, File info, String hintTag, int which)
throws IOException {
if (stage.exists()) {
+ if (multiCrop()) {
+ SparseArray<Rect> cropHints = parseCropHints(info, hintTag);
+ if (cropHints != null) {
+ Slog.i(TAG, "Got restored wallpaper; applying which=" + which
+ + "; cropHints = " + cropHints);
+ try (FileInputStream in = new FileInputStream(stage)) {
+ mWallpaperManager.setStreamWithCrops(in, cropHints, true, which);
+ }
+ // And log the success
+ if ((which & FLAG_SYSTEM) > 0) {
+ mEventLogger.onSystemImageWallpaperRestored();
+ }
+ if ((which & FLAG_LOCK) > 0) {
+ mEventLogger.onLockImageWallpaperRestored();
+ }
+ } else {
+ logRestoreError(which, ERROR_NO_METADATA);
+ }
+ return;
+ }
+
// Parse the restored info file to find the crop hint. Note that this currently
// relies on a priori knowledge of the wallpaper info file schema.
Rect cropHint = parseCropHint(info, hintTag);
@@ -501,6 +527,47 @@ public class WallpaperBackupAgent extends BackupAgent {
return cropHint;
}
+ private SparseArray<Rect> parseCropHints(File wallpaperInfo, String sectionTag) {
+ SparseArray<Rect> cropHints = new SparseArray<>();
+ try (FileInputStream stream = new FileInputStream(wallpaperInfo)) {
+ XmlPullParser parser = Xml.resolvePullParser(stream);
+ int type;
+ do {
+ type = parser.next();
+ if (type != XmlPullParser.START_TAG) continue;
+ String tag = parser.getName();
+ if (!sectionTag.equals(tag)) continue;
+ for (Pair<Integer, String> pair: List.of(
+ new Pair<>(WallpaperManager.PORTRAIT, "Portrait"),
+ new Pair<>(WallpaperManager.LANDSCAPE, "Landscape"),
+ new Pair<>(WallpaperManager.SQUARE_PORTRAIT, "SquarePortrait"),
+ new Pair<>(WallpaperManager.SQUARE_LANDSCAPE, "SquareLandscape"))) {
+ Rect cropHint = new Rect(
+ getAttributeInt(parser, "cropLeft" + pair.second, 0),
+ getAttributeInt(parser, "cropTop" + pair.second, 0),
+ getAttributeInt(parser, "cropRight" + pair.second, 0),
+ getAttributeInt(parser, "cropBottom" + pair.second, 0));
+ if (!cropHint.isEmpty()) cropHints.put(pair.first, cropHint);
+ }
+ if (cropHints.size() == 0) {
+ // migration case: the crops per screen orientation are not specified.
+ // use the old attributes to restore the crop for one screen orientation.
+ Rect cropHint = new Rect(
+ getAttributeInt(parser, "cropLeft", 0),
+ getAttributeInt(parser, "cropTop", 0),
+ getAttributeInt(parser, "cropRight", 0),
+ getAttributeInt(parser, "cropBottom", 0));
+ if (!cropHint.isEmpty()) cropHints.put(ORIENTATION_UNKNOWN, cropHint);
+ }
+ } while (type != XmlPullParser.END_DOCUMENT);
+ } catch (Exception e) {
+ // Whoops; can't process the info file at all. Report failure.
+ Slog.w(TAG, "Failed to parse restored crops: " + e.getMessage());
+ return null;
+ }
+ return cropHints;
+ }
+
private ComponentName parseWallpaperComponent(File wallpaperInfo, String sectionTag) {
ComponentName name = null;
try (FileInputStream stream = new FileInputStream(wallpaperInfo)) {
diff --git a/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java b/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java
index fb521e11c083..053ed779a27a 100644
--- a/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java
+++ b/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java
@@ -31,6 +31,7 @@ import static com.android.wallpaperbackup.WallpaperEventLogger.WALLPAPER_IMG_LOC
import static com.android.wallpaperbackup.WallpaperEventLogger.WALLPAPER_IMG_SYSTEM;
import static com.android.wallpaperbackup.WallpaperEventLogger.WALLPAPER_LIVE_LOCK;
import static com.android.wallpaperbackup.WallpaperEventLogger.WALLPAPER_LIVE_SYSTEM;
+import static com.android.window.flags.Flags.multiCrop;
import static com.google.common.truth.Truth.assertThat;
@@ -60,6 +61,7 @@ import android.os.FileUtils;
import android.os.ParcelFileDescriptor;
import android.os.UserHandle;
import android.service.wallpaper.WallpaperService;
+import android.util.SparseArray;
import android.util.Xml;
import androidx.test.InstrumentationRegistry;
@@ -711,8 +713,13 @@ public class WallpaperBackupAgentTest {
@Test
public void testOnRestore_throwsException_logsErrors() throws Exception {
- when(mWallpaperManager.setStream(any(), any(), anyBoolean(), anyInt())).thenThrow(
- new RuntimeException());
+ if (!multiCrop()) {
+ when(mWallpaperManager.setStream(any(), any(), anyBoolean(), anyInt()))
+ .thenThrow(new RuntimeException());
+ } else {
+ when(mWallpaperManager.setStreamWithCrops(any(), any(SparseArray.class), anyBoolean(),
+ anyInt())).thenThrow(new RuntimeException());
+ }
mockStagedWallpaperFile(SYSTEM_WALLPAPER_STAGE);
mockStagedWallpaperFile(WALLPAPER_INFO_STAGE);
mWallpaperBackupAgent.onCreate(USER_HANDLE, BackupAnnotations.BackupDestination.CLOUD,
diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
index 2fbc3cd24d65..055970819e28 100644
--- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
+++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
@@ -140,8 +140,8 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
// Widget-related data handled as part of this restore operation
private byte[] mWidgetData;
- // Number of apps restored in this pass
- private int mCount;
+ // Number of apps attempted to restore in this pass
+ private int mRestoreAttemptedAppsCount;
// When did we start?
private long mStartRealtime;
@@ -574,7 +574,8 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
Slog.v(TAG, "No more packages; finishing restore");
}
int millis = (int) (SystemClock.elapsedRealtime() - mStartRealtime);
- EventLog.writeEvent(EventLogTags.RESTORE_SUCCESS, mCount, millis);
+ EventLog.writeEvent(
+ EventLogTags.RESTORE_SUCCESS, mRestoreAttemptedAppsCount, millis);
nextState = UnifiedRestoreState.FINAL;
return;
}
@@ -582,7 +583,8 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
if (DEBUG) {
Slog.i(TAG, "Next restore package: " + mRestoreDescription);
}
- sendOnRestorePackage(pkgName);
+ mRestoreAttemptedAppsCount++;
+ sendOnRestorePackage(mRestoreAttemptedAppsCount, pkgName);
Metadata metaInfo = mPmAgent.getRestoredMetadata(pkgName);
if (metaInfo == null) {
@@ -810,7 +812,6 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
// And then finally start the restore on this agent
try {
initiateOneRestore(mCurrentPackage, metaInfo.versionCode);
- ++mCount;
} catch (Exception e) {
Slog.e(TAG, "Error when attempting restore: " + e.toString());
Bundle monitoringExtras = addRestoreOperationTypeToEvent(/* extras= */ null);
@@ -1331,13 +1332,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
}
// Tell the observer we're done
- if (mObserver != null) {
- try {
- mObserver.restoreFinished(mStatus);
- } catch (RemoteException e) {
- Slog.d(TAG, "Restore observer died at restoreFinished");
- }
- }
+ sendEndRestore();
// Clear any ongoing session timeout.
backupManagerService.getBackupHandler().removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
@@ -1651,10 +1646,10 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
}
}
- void sendOnRestorePackage(String name) {
+ void sendOnRestorePackage(int index, String name) {
if (mObserver != null) {
try {
- mObserver.onUpdate(mCount, name);
+ mObserver.onUpdate(index, name);
} catch (RemoteException e) {
Slog.d(TAG, "Restore observer died in onUpdate");
mObserver = null;
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
index 53c0184c6e29..e5a8c4fa22b7 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
@@ -101,7 +101,7 @@ class CompanionDeviceShellCommand extends ShellCommand {
String deviceProfile = getNextArg();
final MacAddress macAddress = MacAddress.fromString(address);
mService.createNewAssociation(userId, packageName, macAddress,
- null, deviceProfile, false);
+ /* displayName= */ deviceProfile, deviceProfile, false);
}
break;
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index ef61498e16af..b6e114087f30 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -56,6 +56,7 @@ import android.os.Process;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.UserHandle;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.ExceptionUtils;
import android.util.Slog;
@@ -112,7 +113,7 @@ public class VirtualDeviceManagerService extends SystemService {
Context.DEVICE_ID_DEFAULT + 1);
@GuardedBy("mVirtualDeviceManagerLock")
- private List<AssociationInfo> mActiveAssociations = new ArrayList<>();
+ private ArrayMap<String, AssociationInfo> mActiveAssociations = new ArrayMap<>();
private final CompanionDeviceManager.OnAssociationsChangedListener mCdmAssociationListener =
new CompanionDeviceManager.OnAssociationsChangedListener() {
@@ -343,34 +344,29 @@ public class VirtualDeviceManagerService extends SystemService {
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
void onCdmAssociationsChanged(List<AssociationInfo> associations) {
- List<AssociationInfo> vdmAssociations = new ArrayList<>();
- Set<Integer> activeAssociationIds = new HashSet<>();
+ ArrayMap<String, AssociationInfo> vdmAssociations = new ArrayMap<>();
for (int i = 0; i < associations.size(); ++i) {
AssociationInfo association = associations.get(i);
- if (VIRTUAL_DEVICE_COMPANION_DEVICE_PROFILES.contains(association.getDeviceProfile())) {
- vdmAssociations.add(association);
- activeAssociationIds.add(association.getId());
+ if (VIRTUAL_DEVICE_COMPANION_DEVICE_PROFILES.contains(association.getDeviceProfile())
+ && !association.isRevoked()) {
+ String persistentId =
+ VirtualDeviceImpl.createPersistentDeviceId(association.getId());
+ vdmAssociations.put(persistentId, association);
}
}
Set<VirtualDeviceImpl> virtualDevicesToRemove = new HashSet<>();
- Set<String> removedPersistentDeviceIds = new HashSet<>();
+ Set<String> removedPersistentDeviceIds;
synchronized (mVirtualDeviceManagerLock) {
- for (int i = 0; i < mActiveAssociations.size(); ++i) {
- AssociationInfo associationInfo = mActiveAssociations.get(i);
- if (!activeAssociationIds.contains(associationInfo.getId())) {
- removedPersistentDeviceIds.add(
- VirtualDeviceImpl.createPersistentDeviceId(associationInfo.getId()));
- }
- }
+ removedPersistentDeviceIds = mActiveAssociations.keySet();
+ removedPersistentDeviceIds.removeAll(vdmAssociations.keySet());
+ mActiveAssociations = vdmAssociations;
for (int i = 0; i < mVirtualDevices.size(); i++) {
VirtualDeviceImpl virtualDevice = mVirtualDevices.valueAt(i);
- if (!activeAssociationIds.contains(virtualDevice.getAssociationId())) {
+ if (removedPersistentDeviceIds.contains(virtualDevice.getPersistentDeviceId())) {
virtualDevicesToRemove.add(virtualDevice);
}
}
-
- mActiveAssociations = vdmAssociations;
}
for (VirtualDeviceImpl virtualDevice : virtualDevicesToRemove) {
@@ -577,6 +573,16 @@ public class VirtualDeviceManagerService extends SystemService {
return Context.DEVICE_ID_DEFAULT;
}
+ @Override // Binder call
+ public @Nullable CharSequence getDisplayNameForPersistentDeviceId(
+ @NonNull String persistentDeviceId) {
+ final AssociationInfo associationInfo;
+ synchronized (mVirtualDeviceManagerLock) {
+ associationInfo = mActiveAssociations.get(persistentDeviceId);
+ }
+ return associationInfo == null ? null : associationInfo.getDisplayName();
+ }
+
// Binder call
@Override
public boolean isValidVirtualDeviceId(int deviceId) {
@@ -885,15 +891,9 @@ public class VirtualDeviceManagerService extends SystemService {
@Override
public @NonNull Set<String> getAllPersistentDeviceIds() {
- Set<String> persistentIds = new ArraySet<>();
synchronized (mVirtualDeviceManagerLock) {
- for (int i = 0; i < mActiveAssociations.size(); ++i) {
- AssociationInfo associationInfo = mActiveAssociations.get(i);
- persistentIds.add(
- VirtualDeviceImpl.createPersistentDeviceId(associationInfo.getId()));
- }
+ return Set.copyOf(mActiveAssociations.keySet());
}
- return persistentIds;
}
@Override
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
index 0f964bb75944..73f3999f40ce 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
@@ -143,7 +143,11 @@ public class FaceService extends SystemService {
return proto.getBytes();
}
- @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+ @android.annotation.EnforcePermission(
+ anyOf = {
+ android.Manifest.permission.USE_BIOMETRIC_INTERNAL,
+ android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION
+ })
@Override // Binder call
public List<FaceSensorPropertiesInternal> getSensorPropertiesInternal(
String opPackageName) {
@@ -285,6 +289,29 @@ public class FaceService extends SystemService {
restricted, statsClient, isKeyguard);
}
+ @android.annotation.EnforcePermission(
+ android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION)
+ @Override // Binder call
+ public long authenticateInBackground(final IBinder token, final long operationId,
+ final IFaceServiceReceiver receiver, final FaceAuthenticateOptions options) {
+ // TODO(b/152413782): If the sensor supports face detect and the device is encrypted or
+ // lockdown, something wrong happened. See similar path in FingerprintService.
+
+ super.authenticateInBackground_enforcePermission();
+
+ final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
+ if (provider == null) {
+ Slog.w(TAG, "Null provider for authenticate");
+ return -1;
+ }
+ options.setSensorId(provider.first);
+
+ return provider.second.scheduleAuthenticate(token, operationId,
+ 0 /* cookie */, new ClientMonitorCallbackConverter(receiver), options,
+ false /* restricted */, BiometricsProtoEnums.CLIENT_UNKNOWN /* statsClient */,
+ true /* allowBackgroundAuthentication */);
+ }
+
@android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
@Override // Binder call
public long detectFace(final IBinder token,
@@ -548,7 +575,11 @@ public class FaceService extends SystemService {
return provider.getEnrolledFaces(sensorId, userId);
}
- @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+ @android.annotation.EnforcePermission(
+ anyOf = {
+ android.Manifest.permission.USE_BIOMETRIC_INTERNAL,
+ android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION
+ })
@Override // Binder call
public boolean hasEnrolledFaces(int sensorId, int userId, String opPackageName) {
super.hasEnrolledFaces_enforcePermission();
diff --git a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionManagerInternal.java b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionManagerInternal.java
index 1f59b57d2da9..c260f10b61a6 100644
--- a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionManagerInternal.java
+++ b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionManagerInternal.java
@@ -17,6 +17,7 @@
package com.android.server.grammaticalinflection;
import android.annotation.Nullable;
+import android.content.res.Configuration;
/**
* System-server internal interface to the {@link android.app.GrammaticalInflectionManager}.
@@ -37,5 +38,14 @@ public abstract class GrammaticalInflectionManagerInternal {
* at the time this is called, to be referenced later when the app is installed.
*/
public abstract void stageAndApplyRestoredPayload(byte[] payload, int userId);
+
+ /**
+ * Get the current system grammatical gender of privileged application.
+ *
+ * @return the value of grammatical gender
+ *
+ * @see Configuration#getGrammaticalGender
+ */
+ public abstract @Configuration.GrammaticalGender int getSystemGrammaticalGender(int userId);
}
diff --git a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java
index 68848a2ad426..6eb7e9559b8d 100644
--- a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java
+++ b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java
@@ -19,9 +19,12 @@ package com.android.server.grammaticalinflection;
import static android.app.Flags.systemTermsOfAddressEnabled;
import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED;
+import static com.android.server.grammaticalinflection.GrammaticalInflectionUtils.checkSystemGrammaticalGenderPermission;
+
import android.annotation.Nullable;
import android.app.GrammaticalInflectionManager;
import android.app.IGrammaticalInflectionManager;
+import android.content.AttributionSource;
import android.content.Context;
import android.content.pm.PackageManagerInternal;
import android.os.Binder;
@@ -30,6 +33,7 @@ import android.os.Process;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.os.SystemProperties;
+import android.permission.PermissionManager;
import android.util.AtomicFile;
import android.util.Log;
import android.util.SparseIntArray;
@@ -75,6 +79,8 @@ public class GrammaticalInflectionService extends SystemService {
private PackageManagerInternal mPackageManagerInternal;
private GrammaticalInflectionService.GrammaticalInflectionBinderService mBinderService;
+ private PermissionManager mPermissionManager;
+ private Context mContext;
/**
* Initializes the system service.
@@ -88,11 +94,12 @@ public class GrammaticalInflectionService extends SystemService {
*/
public GrammaticalInflectionService(Context context) {
super(context);
+ mContext = context;
mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
- mBackupHelper = new GrammaticalInflectionBackupHelper(
- this, context.getPackageManager());
+ mBackupHelper = new GrammaticalInflectionBackupHelper(this, context.getPackageManager());
mBinderService = new GrammaticalInflectionBinderService();
+ mPermissionManager = context.getSystemService(PermissionManager.class);
}
@Override
@@ -112,7 +119,7 @@ public class GrammaticalInflectionService extends SystemService {
}
@Override
- public void setSystemWideGrammaticalGender(int userId, int grammaticalGender) {
+ public void setSystemWideGrammaticalGender(int grammaticalGender, int userId) {
checkCallerIsSystem();
checkSystemTermsOfAddressIsEnabled();
GrammaticalInflectionService.this.setSystemWideGrammaticalGender(grammaticalGender,
@@ -120,16 +127,17 @@ public class GrammaticalInflectionService extends SystemService {
}
@Override
- public int getSystemGrammaticalGender(int userId) {
+ public int getSystemGrammaticalGender(AttributionSource attributionSource, int userId) {
checkSystemTermsOfAddressIsEnabled();
- return GrammaticalInflectionService.this.getSystemGrammaticalGender(userId);
+ return GrammaticalInflectionService.this.getSystemGrammaticalGender(attributionSource,
+ userId);
}
@Override
public void onShellCommand(FileDescriptor in, FileDescriptor out,
FileDescriptor err, String[] args, ShellCallback callback,
ResultReceiver resultReceiver) {
- (new GrammaticalInflectionShellCommand(mBinderService))
+ (new GrammaticalInflectionShellCommand(mBinderService, mContext.getAttributionSource()))
.exec(this, in, out, err, args, callback, resultReceiver);
}
};
@@ -148,6 +156,13 @@ public class GrammaticalInflectionService extends SystemService {
public void stageAndApplyRestoredPayload(byte[] payload, int userId) {
mBackupHelper.stageAndApplyRestoredPayload(payload, userId);
}
+
+ @Override
+ public int getSystemGrammaticalGender(int userId) {
+ checkCallerIsSystem();
+ return GrammaticalInflectionService.this.getSystemGrammaticalGender(
+ mContext.getAttributionSource(), userId);
+ }
}
protected int getApplicationGrammaticalGender(String appPackageName, int userId) {
@@ -211,9 +226,24 @@ public class GrammaticalInflectionService extends SystemService {
}
}
- // TODO(b/298591009): Add a new AppOp value for the apps that want to access the grammatical
- // gender.
- public int getSystemGrammaticalGender(int userId) {
+ public int getSystemGrammaticalGender(AttributionSource attributionSource, int userId) {
+ String packageName = attributionSource.getPackageName();
+ if (packageName == null) {
+ Log.d(TAG, "Package name is null.");
+ return GRAMMATICAL_GENDER_NOT_SPECIFIED;
+ }
+
+ int callingUid = Binder.getCallingUid();
+ if (mPackageManagerInternal.getPackageUid(packageName, 0, userId) != callingUid) {
+ Log.d(TAG,
+ "Package " + packageName + " does not belong to the calling uid " + callingUid);
+ return GRAMMATICAL_GENDER_NOT_SPECIFIED;
+ }
+
+ if (!checkSystemGrammaticalGenderPermission(mPermissionManager, attributionSource)) {
+ return GRAMMATICAL_GENDER_NOT_SPECIFIED;
+ }
+
synchronized (mLock) {
final File file = getGrammaticalGenderFile(userId);
if (!file.exists()) {
diff --git a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionShellCommand.java b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionShellCommand.java
index d22372860ead..cdda69278b2c 100644
--- a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionShellCommand.java
+++ b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionShellCommand.java
@@ -21,6 +21,7 @@ import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED
import android.app.ActivityManager;
import android.app.GrammaticalInflectionManager;
import android.app.IGrammaticalInflectionManager;
+import android.content.AttributionSource;
import android.content.res.Configuration;
import android.os.RemoteException;
import android.os.ShellCommand;
@@ -44,9 +45,12 @@ class GrammaticalInflectionShellCommand extends ShellCommand {
}
private final IGrammaticalInflectionManager mBinderService;
+ private AttributionSource mAttributionSource;
- GrammaticalInflectionShellCommand(IGrammaticalInflectionManager grammaticalInflectionManager) {
+ GrammaticalInflectionShellCommand(IGrammaticalInflectionManager grammaticalInflectionManager,
+ AttributionSource attributionSource) {
mBinderService = grammaticalInflectionManager;
+ mAttributionSource = attributionSource;
}
@Override
@@ -115,7 +119,7 @@ class GrammaticalInflectionShellCommand extends ShellCommand {
} while (true);
try {
- mBinderService.setSystemWideGrammaticalGender(userId, grammaticalGender);
+ mBinderService.setSystemWideGrammaticalGender(grammaticalGender, userId);
} catch (RemoteException e) {
getOutPrintWriter().println("Remote Exception: " + e);
}
@@ -141,7 +145,8 @@ class GrammaticalInflectionShellCommand extends ShellCommand {
} while (true);
try {
- int grammaticalGender = mBinderService.getSystemGrammaticalGender(userId);
+ int grammaticalGender = mBinderService.getSystemGrammaticalGender(mAttributionSource,
+ userId);
getOutPrintWriter().println(GRAMMATICAL_GENDER_MAP.get(grammaticalGender));
} catch (RemoteException e) {
getOutPrintWriter().println("Remote Exception: " + e);
diff --git a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionUtils.java b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionUtils.java
new file mode 100644
index 000000000000..f056561f20e0
--- /dev/null
+++ b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionUtils.java
@@ -0,0 +1,46 @@
+/**
+ * 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.grammaticalinflection;
+
+import static android.Manifest.permission.READ_SYSTEM_GRAMMATICAL_GENDER;
+
+import android.annotation.NonNull;
+import android.content.AttributionSource;
+import android.permission.PermissionManager;
+import android.util.Log;
+
+/**
+ * Utility methods for system grammatical gender.
+ */
+public class GrammaticalInflectionUtils {
+
+ private static final String TAG = "GrammaticalInflectionUtils";
+
+ public static boolean checkSystemGrammaticalGenderPermission(
+ @NonNull PermissionManager permissionManager,
+ @NonNull AttributionSource attributionSource) {
+ int permissionCheckResult = permissionManager.checkPermissionForDataDelivery(
+ READ_SYSTEM_GRAMMATICAL_GENDER,
+ attributionSource, /* message= */ null);
+ if (permissionCheckResult != PermissionManager.PERMISSION_GRANTED) {
+ Log.v(TAG, "AttributionSource: " + attributionSource
+ + " does not have READ_SYSTEM_GRAMMATICAL_GENDER permission.");
+ return false;
+ }
+ 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 2ae040a69583..308d441fb871 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -69,6 +69,7 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_OF
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_ON;
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR;
import static android.app.Flags.lifetimeExtensionRefactor;
+import static android.app.NotificationManager.zenModeFromInterruptionFilter;
import static android.app.StatusBarManager.ACTION_KEYGUARD_PRIVATE_NOTIFICATIONS_CHANGED;
import static android.app.StatusBarManager.EXTRA_KM_PRIVATE_NOTIFS_ALLOWED;
import static android.content.Context.BIND_ALLOW_WHITELIST_MANAGEMENT;
@@ -5311,18 +5312,41 @@ public class NotificationManagerService extends SystemService {
@Override
public void requestInterruptionFilterFromListener(INotificationListener token,
int interruptionFilter) throws RemoteException {
- final int callingUid = Binder.getCallingUid();
- final boolean isSystemOrSystemUi = isCallerSystemOrSystemUi();
- final long identity = Binder.clearCallingIdentity();
- try {
+ if (android.app.Flags.modesApi()) {
+ final int callingUid = Binder.getCallingUid();
+ ManagedServiceInfo info;
synchronized (mNotificationLock) {
- final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
- mZenModeHelper.requestFromListener(info.component, interruptionFilter,
- callingUid, isSystemOrSystemUi);
- updateInterruptionFilterLocked();
+ info = mListeners.checkServiceTokenLocked(token);
+ }
+
+ final int zenMode = zenModeFromInterruptionFilter(interruptionFilter, -1);
+ if (zenMode == -1) return;
+ if (!canManageGlobalZenPolicy(info.component.getPackageName(), callingUid)) {
+ mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(
+ info.component.getPackageName(), callingUid, zenMode);
+ } else {
+ int origin = computeZenOrigin(/* fromUser= */ false);
+ Binder.withCleanCallingIdentity(() -> {
+ mZenModeHelper.setManualZenMode(zenMode, /* conditionId= */ null, origin,
+ "listener:" + info.component.flattenToShortString(),
+ /* caller= */ info.component.getPackageName(),
+ callingUid);
+ });
+ }
+ } else {
+ final int callingUid = Binder.getCallingUid();
+ final boolean isSystemOrSystemUi = isCallerSystemOrSystemUi();
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mNotificationLock) {
+ final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
+ mZenModeHelper.requestFromListener(info.component, interruptionFilter,
+ callingUid, isSystemOrSystemUi);
+ updateInterruptionFilterLocked();
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
}
- } finally {
- Binder.restoreCallingIdentity(identity);
}
}
@@ -5358,10 +5382,10 @@ public class NotificationManagerService extends SystemService {
@Override
public void setZenMode(int mode, Uri conditionId, String reason, boolean fromUser) {
enforceSystemOrSystemUI("INotificationManager.setZenMode");
- final int callingUid = Binder.getCallingUid();
- final long identity = Binder.clearCallingIdentity();
enforceUserOriginOnlyFromSystem(fromUser, "setZenMode");
+ final int callingUid = Binder.getCallingUid();
+ final long identity = Binder.clearCallingIdentity();
try {
mZenModeHelper.setManualZenMode(mode, conditionId, computeZenOrigin(fromUser),
reason, /* caller= */ null, callingUid);
@@ -5554,7 +5578,7 @@ public class NotificationManagerService extends SystemService {
@Override
public void setInterruptionFilter(String pkg, int filter, boolean fromUser) {
enforcePolicyAccess(pkg, "setInterruptionFilter");
- final int zen = NotificationManager.zenModeFromInterruptionFilter(filter, -1);
+ final int zen = zenModeFromInterruptionFilter(filter, -1);
if (zen == -1) throw new IllegalArgumentException("Invalid filter: " + filter);
final int callingUid = Binder.getCallingUid();
enforceUserOriginOnlyFromSystem(fromUser, "setInterruptionFilter");
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 93ffd974bb80..153af13b61b4 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -359,6 +359,7 @@ public class ZenModeHelper {
return NotificationManager.zenModeToInterruptionFilter(mZenMode);
}
+ // TODO: b/310620812 - Remove when MODES_API is inlined (no more callers).
public void requestFromListener(ComponentName name, int filter, int callingUid,
boolean fromSystemOrSystemUi) {
final int newZen = NotificationManager.zenModeFromInterruptionFilter(filter, -1);
@@ -2051,7 +2052,10 @@ public class ZenModeHelper {
/* optional LoggedZenMode zen_mode = 4 */ ROOT_CONFIG,
/* optional string id = 5 */ "", // empty for root config
/* optional int32 uid = 6 */ Process.SYSTEM_UID, // system owns root config
- /* optional DNDPolicyProto policy = 7 */ config.toZenPolicy().toProto()));
+ /* optional DNDPolicyProto policy = 7 */ config.toZenPolicy().toProto(),
+ /* optional int32 rule_modified_fields = 8 */ 0,
+ /* optional int32 policy_modified_fields = 9 */ 0,
+ /* optional int32 device_effects_modified_fields = 10 */ 0));
if (config.manualRule != null) {
ruleToProtoLocked(user, config.manualRule, true, events);
}
@@ -2093,7 +2097,11 @@ public class ZenModeHelper {
/* optional android.stats.dnd.ZenMode zen_mode = 4 */ rule.zenMode,
/* optional string id = 5 */ id,
/* optional int32 uid = 6 */ getPackageUid(pkg, user),
- /* optional DNDPolicyProto policy = 7 */ policyProto));
+ /* optional DNDPolicyProto policy = 7 */ policyProto,
+ /* optional int32 rule_modified_fields = 8 */ rule.userModifiedFields,
+ /* optional int32 policy_modified_fields = 9 */ rule.zenPolicyUserModifiedFields,
+ /* optional int32 device_effects_modified_fields = 10 */
+ rule.zenDeviceEffectsUserModifiedFields));
}
private int getPackageUid(String pkg, int user) {
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 54055904d090..c94111c31ef4 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1023,15 +1023,17 @@ public class UserManagerService extends IUserManager.Stub {
if (isAutoLockForPrivateSpaceEnabled()) {
int mainUserId = getMainUserIdUnchecked();
+ if (mainUserId != UserHandle.USER_NULL) {
+ mContext.getContentResolver().registerContentObserverAsUser(
+ Settings.Secure.getUriFor(
+ Settings.Secure.PRIVATE_SPACE_AUTO_LOCK), false,
+ mPrivateSpaceAutoLockSettingsObserver, UserHandle.of(mainUserId));
- mContext.getContentResolver().registerContentObserverAsUser(Settings.Secure.getUriFor(
- Settings.Secure.PRIVATE_SPACE_AUTO_LOCK), false,
- mPrivateSpaceAutoLockSettingsObserver, UserHandle.of(mainUserId));
-
- setOrUpdateAutoLockPreferenceForPrivateProfile(
- Settings.Secure.getIntForUser(mContext.getContentResolver(),
- Settings.Secure.PRIVATE_SPACE_AUTO_LOCK,
- Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_NEVER, mainUserId));
+ setOrUpdateAutoLockPreferenceForPrivateProfile(
+ Settings.Secure.getIntForUser(mContext.getContentResolver(),
+ Settings.Secure.PRIVATE_SPACE_AUTO_LOCK,
+ Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_NEVER, mainUserId));
+ }
}
markEphemeralUsersForRemoval();
diff --git a/services/core/java/com/android/server/utils/AnrTimer.java b/services/core/java/com/android/server/utils/AnrTimer.java
index c15ac37565ff..743005a2d844 100644
--- a/services/core/java/com/android/server/utils/AnrTimer.java
+++ b/services/core/java/com/android/server/utils/AnrTimer.java
@@ -96,17 +96,11 @@ public class AnrTimer<V> implements AutoCloseable {
private static boolean DEBUG = false;
/**
- * The trace tag.
+ * The trace tag is the same usd by ActivityManager.
*/
private static final long TRACE_TAG = Trace.TRACE_TAG_ACTIVITY_MANAGER;
/**
- * Enable tracing from the time a timer expires until it is accepted or discarded. This is
- * used to diagnose long latencies in the client.
- */
- private static final boolean ENABLE_TRACING = false;
-
- /**
* Return true if the feature is enabled. By default, the value is take from the Flags class
* but it can be changed for local testing.
*/
@@ -320,24 +314,21 @@ public class AnrTimer<V> implements AutoCloseable {
}
/**
- * Start a trace on the timer. The trace is laid down in the AnrTimerTrack.
+ * Generate a trace point with full timer information. The meaning of milliseconds depends on
+ * the caller.
*/
- private void traceBegin(int timerId, int pid, int uid, String what) {
- if (ENABLE_TRACING) {
- final String label = formatSimple("%s(%d,%d,%s)", what, pid, uid, mLabel);
- final int cookie = timerId;
- Trace.asyncTraceForTrackBegin(TRACE_TAG, TRACK, label, cookie);
- }
+ private void trace(String op, int timerId, int pid, int uid, long milliseconds) {
+ final String label =
+ formatSimple("%s(%d,%d,%d,%s,%d)", op, timerId, pid, uid, mLabel, milliseconds);
+ Trace.instantForTrack(TRACE_TAG, TRACK, label);
}
/**
- * End a trace on the timer.
+ * Generate a trace point with just the timer ID.
*/
- private void traceEnd(int timerId) {
- if (ENABLE_TRACING) {
- final int cookie = timerId;
- Trace.asyncTraceForTrackEnd(TRACE_TAG, TRACK, cookie);
- }
+ private void trace(String op, int timerId) {
+ final String label = formatSimple("%s(%d)", op, timerId);
+ Trace.instantForTrack(TRACE_TAG, TRACK, label);
}
/**
@@ -492,7 +483,7 @@ public class AnrTimer<V> implements AutoCloseable {
return false;
}
nativeAnrTimerAccept(mNative, timer);
- traceEnd(timer);
+ trace("accept", timer);
return true;
}
}
@@ -511,7 +502,7 @@ public class AnrTimer<V> implements AutoCloseable {
return false;
}
nativeAnrTimerDiscard(mNative, timer);
- traceEnd(timer);
+ trace("discard", timer);
return true;
}
}
@@ -629,13 +620,18 @@ public class AnrTimer<V> implements AutoCloseable {
}
/**
- * The notifier that a timer has fired. The timerId and original pid/uid are supplied. This
- * method is called from native code. This method takes mLock so that a timer cannot expire
- * in the middle of another operation (like start or cancel).
+ * The notifier that a timer has fired. The timerId and original pid/uid are supplied. The
+ * elapsed time is the actual time since the timer was scheduled, which may be different from
+ * the original timeout if the timer was extended or if other delays occurred. This method
+ * takes mLock so that a timer cannot expire in the middle of another operation (like start or
+ * cancel).
+ *
+ * This method is called from native code. The function must return true if the expiration
+ * message is delivered to the upper layers and false if it could not be delivered.
*/
@Keep
- private boolean expire(int timerId, int pid, int uid) {
- traceBegin(timerId, pid, uid, "expired");
+ private boolean expire(int timerId, int pid, int uid, long elapsedMs) {
+ trace("expired", timerId, pid, uid, elapsedMs);
V arg = null;
synchronized (mLock) {
arg = mTimerArgMap.get(timerId);
@@ -815,9 +811,4 @@ public class AnrTimer<V> implements AutoCloseable {
/** Prod the native library to log a few statistics. */
private static native void nativeAnrTimerDump(long service, boolean verbose);
-
- // This is not a native method but it is a native interface, in the sense that it is called from
- // the native layer to report timer expiration. The function must return true if the expiration
- // message is delivered to the upper layers and false if it could not be delivered.
- // private boolean expire(int timerId, int pid, int uid);
}
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
index b773ade09b89..51acc8e01cda 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
@@ -16,21 +16,28 @@
package com.android.server.wallpaper;
+import static android.app.WallpaperManager.getOrientation;
+import static android.app.WallpaperManager.getRotatedOrientation;
import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.server.wallpaper.WallpaperUtils.RECORD_FILE;
import static com.android.server.wallpaper.WallpaperUtils.RECORD_LOCK_FILE;
import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER;
import static com.android.server.wallpaper.WallpaperUtils.getWallpaperDir;
+import static com.android.window.flags.Flags.multiCrop;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageDecoder;
+import android.graphics.Point;
import android.graphics.Rect;
import android.os.FileUtils;
import android.os.SELinux;
+import android.text.TextUtils;
import android.util.Slog;
+import android.util.SparseArray;
import android.view.DisplayInfo;
+import android.view.View;
import com.android.server.utils.TimingsTraceAndSlog;
@@ -39,28 +46,334 @@ import libcore.io.IoUtils;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
/**
* Helper file for wallpaper cropping
- * Meant to have a single instance, only used by the WallpaperManagerService
+ * Meant to have a single instance, only used internally by system_server
+ * @hide
*/
-class WallpaperCropper {
+public class WallpaperCropper {
private static final String TAG = WallpaperCropper.class.getSimpleName();
private static final boolean DEBUG = false;
private static final boolean DEBUG_CROP = true;
+ /**
+ * Maximum acceptable parallax.
+ * A value of 1 means "the additional width for parallax is at most 100% of the screen width"
+ */
+ private static final float MAX_PARALLAX = 1f;
+
+ /**
+ * We define three ways to adjust a crop. These modes are used depending on the situation:
+ * - When going from unfolded to folded, we want to remove content
+ * - When going from folded to unfolded, we want to add content
+ * - For a screen rotation, we want to keep the same amount of content
+ */
+ private static final int ADD = 1;
+ private static final int REMOVE = 2;
+ private static final int BALANCE = 3;
+
+
private final WallpaperDisplayHelper mWallpaperDisplayHelper;
+ /**
+ * Helpers exposed to the window manager part (WallpaperController)
+ */
+ public interface WallpaperCropUtils {
+
+ /**
+ * Equivalent to {@link #getCrop(Point, Point, SparseArray, boolean)}
+ */
+ Rect getCrop(Point displaySize, Point bitmapSize,
+ SparseArray<Rect> suggestedCrops, boolean rtl);
+ }
+
WallpaperCropper(WallpaperDisplayHelper wallpaperDisplayHelper) {
mWallpaperDisplayHelper = wallpaperDisplayHelper;
}
/**
- * Once a new wallpaper has been written via setWallpaper(...), it needs to be cropped
- * for display.
+ * Given the dimensions of the original wallpaper image, some optional suggested crops
+ * (either defined by the user, or coming from a backup), and whether the device is RTL,
+ * generate a crop for the current display. This is done through the following process:
+ * <ul>
+ * <li> If no suggested crops are provided, center the full image on the display. </li>
+ * <li> If there is a suggested crop the given displaySize, reuse the suggested crop and
+ * adjust it using {@link #getAdjustedCrop}. </li>
+ * <li> If there are suggested crops, but not for the orientation of the given displaySize,
+ * reuse one of the suggested crop for another orientation and adjust if using
+ * {@link #getAdjustedCrop}. </li>
+ * </ul>
+ *
+ * @param displaySize The dimensions of the surface where we want to render the wallpaper
+ * @param bitmapSize The dimensions of the wallpaper bitmap
+ * @param rtl Whether the device is right-to-left
+ * @param suggestedCrops An optional list of user-defined crops for some orientations.
+ * If there is a suggested crop for
*
- * This will generate the crop and write it in the file
+ * @return A Rect indicating how to crop the bitmap for the current display.
+ */
+ public Rect getCrop(Point displaySize, Point bitmapSize,
+ SparseArray<Rect> suggestedCrops, boolean rtl) {
+
+ // Case 1: if no crops are provided, center align the full image
+ if (suggestedCrops == null || suggestedCrops.size() == 0) {
+ Rect crop = new Rect(0, 0, displaySize.x, displaySize.y);
+ float scale = Math.min(
+ ((float) bitmapSize.x) / displaySize.x,
+ ((float) bitmapSize.y) / displaySize.y);
+ crop.scale(scale);
+ crop.offset((bitmapSize.x - crop.width()) / 2,
+ (bitmapSize.y - crop.height()) / 2);
+ return crop;
+ }
+ int orientation = getOrientation(displaySize);
+
+ // Case 2: if the orientation exists in the suggested crops, adjust the suggested crop
+ Rect suggestedCrop = suggestedCrops.get(orientation);
+ if (suggestedCrop != null) {
+ if (suggestedCrop.left < 0 || suggestedCrop.top < 0
+ || suggestedCrop.right > bitmapSize.x || suggestedCrop.bottom > bitmapSize.y) {
+ Slog.w(TAG, "invalid suggested crop: " + suggestedCrop);
+ Rect fullImage = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
+ return getAdjustedCrop(fullImage, bitmapSize, displaySize, true, rtl, ADD);
+ } else {
+ return getAdjustedCrop(suggestedCrop, bitmapSize, displaySize, true, rtl, ADD);
+ }
+ }
+
+ // Case 3: if we have the 90° rotated orientation in the suggested crops, reuse it and
+ // trying to preserve the zoom level and the center of the image
+ SparseArray<Point> defaultDisplaySizes = mWallpaperDisplayHelper.getDefaultDisplaySizes();
+ int rotatedOrientation = getRotatedOrientation(orientation);
+ suggestedCrop = suggestedCrops.get(rotatedOrientation);
+ Point suggestedDisplaySize = defaultDisplaySizes.get(rotatedOrientation);
+ if (suggestedCrop != null) {
+ // only keep the visible part (without parallax)
+ Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl);
+ return getAdjustedCrop(adjustedCrop, bitmapSize, displaySize, false, rtl, BALANCE);
+ }
+
+ // Case 4: if the device is a foldable, if we're looking for a folded orientation and have
+ // the suggested crop of the relative unfolded orientation, reuse it by removing content.
+ int unfoldedOrientation = mWallpaperDisplayHelper.getUnfoldedOrientation(orientation);
+ suggestedCrop = suggestedCrops.get(unfoldedOrientation);
+ suggestedDisplaySize = defaultDisplaySizes.get(unfoldedOrientation);
+ if (suggestedCrop != null) {
+ // only keep the visible part (without parallax)
+ Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl);
+ return getAdjustedCrop(adjustedCrop, bitmapSize, displaySize, false, rtl, REMOVE);
+ }
+
+ // Case 5: if the device is a foldable, if we're looking for an unfolded orientation and
+ // have the suggested crop of the relative folded orientation, reuse it by adding content.
+ int foldedOrientation = mWallpaperDisplayHelper.getFoldedOrientation(orientation);
+ suggestedCrop = suggestedCrops.get(foldedOrientation);
+ suggestedDisplaySize = defaultDisplaySizes.get(foldedOrientation);
+ if (suggestedCrop != null) {
+ // only keep the visible part (without parallax)
+ Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl);
+ return getAdjustedCrop(adjustedCrop, bitmapSize, displaySize, false, rtl, ADD);
+ }
+
+ // Case 6: for a foldable device, try to combine case 3 + case 4 or 5:
+ // rotate, then fold or unfold
+ Point rotatedDisplaySize = defaultDisplaySizes.get(rotatedOrientation);
+ if (rotatedDisplaySize != null) {
+ int rotatedFolded = mWallpaperDisplayHelper.getFoldedOrientation(rotatedOrientation);
+ int rotateUnfolded = mWallpaperDisplayHelper.getUnfoldedOrientation(rotatedOrientation);
+ for (int suggestedOrientation : new int[]{rotatedFolded, rotateUnfolded}) {
+ suggestedCrop = suggestedCrops.get(suggestedOrientation);
+ if (suggestedCrop != null) {
+ Rect rotatedCrop = getCrop(rotatedDisplaySize, bitmapSize, suggestedCrops, rtl);
+ SparseArray<Rect> rotatedCropMap = new SparseArray<>();
+ rotatedCropMap.put(rotatedOrientation, rotatedCrop);
+ return getCrop(displaySize, bitmapSize, rotatedCropMap, rtl);
+ }
+ }
+ }
+
+ // Case 7: could not properly reuse the suggested crops. Fall back to case 1.
+ Slog.w(TAG, "Could not find a proper default crop for display: " + displaySize
+ + ", bitmap size: " + bitmapSize + ", suggested crops: " + suggestedCrops
+ + ", orientation: " + orientation + ", rtl: " + rtl
+ + ", defaultDisplaySizes: " + defaultDisplaySizes);
+ return getCrop(displaySize, bitmapSize, new SparseArray<>(), rtl);
+ }
+
+ /**
+ * Given a crop, a displaySize for the orientation of that crop, compute the visible part of the
+ * crop. This removes any additional width used for parallax. No-op if displaySize == null.
+ */
+ private static Rect noParallax(Rect crop, Point displaySize, Point bitmapSize, boolean rtl) {
+ if (displaySize == null) return crop;
+ Rect adjustedCrop = getAdjustedCrop(crop, bitmapSize, displaySize, true, rtl, ADD);
+ // only keep the visible part (without parallax)
+ float suggestedDisplayRatio = 1f * displaySize.x / displaySize.y;
+ int widthToRemove = (int) (adjustedCrop.width()
+ - (((float) adjustedCrop.height()) * suggestedDisplayRatio) + 0.5f);
+ if (rtl) {
+ adjustedCrop.left += widthToRemove;
+ } else {
+ adjustedCrop.right -= widthToRemove;
+ }
+ return adjustedCrop;
+ }
+
+ /**
+ * Adjust a given crop:
+ * <ul>
+ * <li>If parallax = true, make sure we have a parallax of at most {@link #MAX_PARALLAX},
+ * by removing content from the right (or left if RTL) if necessary.
+ * </li>
+ * <li>If parallax = false, make sure we do not have additional width for parallax. If we
+ * have additional width for parallax, remove half of the additional width on both sides.
+ * </li>
+ * <li>Make sure the crop fills the screen, i.e. that the width/height ratio of the crop
+ * is at least the width/height ratio of the screen. If it is less, add width to the crop
+ * (if possible on both sides) to fill the screen. If not enough width available, remove
+ * height to the crop.
+ * </li>
+ * </ul>
+ */
+ private static Rect getAdjustedCrop(Rect crop, Point bitmapSize, Point screenSize,
+ boolean parallax, boolean rtl, int mode) {
+ Rect adjustedCrop = new Rect(crop);
+ float cropRatio = ((float) crop.width()) / crop.height();
+ float screenRatio = ((float) screenSize.x) / screenSize.y;
+ if (cropRatio >= screenRatio) {
+ if (!parallax) {
+ // rotate everything 90 degrees clockwise, compute the result, and rotate back
+ int newLeft = bitmapSize.y - crop.bottom;
+ int newRight = newLeft + crop.height();
+ int newTop = crop.left;
+ int newBottom = newTop + crop.width();
+ Rect rotatedCrop = new Rect(newLeft, newTop, newRight, newBottom);
+ Point rotatedBitmap = new Point(bitmapSize.y, bitmapSize.x);
+ Point rotatedScreen = new Point(screenSize.y, screenSize.x);
+ Rect rect = getAdjustedCrop(rotatedCrop, rotatedBitmap, rotatedScreen, false, rtl,
+ mode);
+ int resultLeft = rect.top;
+ int resultRight = resultLeft + rect.height();
+ int resultTop = rotatedBitmap.x - rect.right;
+ int resultBottom = resultTop + rect.width();
+ return new Rect(resultLeft, resultTop, resultRight, resultBottom);
+ }
+ float additionalWidthForParallax = cropRatio / screenRatio - 1f;
+ if (additionalWidthForParallax > MAX_PARALLAX) {
+ int widthToRemove = (int) Math.ceil(
+ (additionalWidthForParallax - MAX_PARALLAX) * screenRatio * crop.height());
+ if (rtl) {
+ adjustedCrop.left += widthToRemove;
+ } else {
+ adjustedCrop.right -= widthToRemove;
+ }
+ }
+ } else {
+ int widthToAdd = mode == REMOVE ? 0
+ : mode == ADD ? (int) (0.5 + crop.height() * screenRatio - crop.width())
+ : (int) (0.5 + crop.height() - crop.width());
+ int availableWidth = bitmapSize.x - crop.width();
+ if (availableWidth >= widthToAdd) {
+ int widthToAddLeft = widthToAdd / 2;
+ int widthToAddRight = widthToAdd / 2 + widthToAdd % 2;
+
+ if (crop.left < widthToAddLeft) {
+ widthToAddRight += (widthToAddLeft - crop.left);
+ widthToAddLeft = crop.left;
+ } else if (bitmapSize.x - crop.right < widthToAddRight) {
+ widthToAddLeft += (widthToAddRight - (bitmapSize.x - crop.right));
+ widthToAddRight = bitmapSize.x - crop.right;
+ }
+ adjustedCrop.left -= widthToAddLeft;
+ adjustedCrop.right += widthToAddRight;
+ } else {
+ adjustedCrop.left = 0;
+ adjustedCrop.right = bitmapSize.x;
+ }
+ int heightToRemove = (int) (crop.height() - (adjustedCrop.width() / screenRatio));
+ adjustedCrop.top += heightToRemove / 2 + heightToRemove % 2;
+ adjustedCrop.bottom -= heightToRemove / 2;
+ }
+ return adjustedCrop;
+ }
+
+ /**
+ * To find the smallest sub-image that contains all the given crops.
+ * This is used in {@link #generateCrop(WallpaperData)}
+ * to determine how the file from {@link WallpaperData#getCropFile()} needs to be cropped.
+ *
+ * @param crops a list of rectangles
+ * @return the smallest rectangle that contains them all.
+ */
+ public static Rect getTotalCrop(SparseArray<Rect> crops) {
+ int left = Integer.MAX_VALUE, top = Integer.MAX_VALUE;
+ int right = Integer.MIN_VALUE, bottom = Integer.MIN_VALUE;
+ for (int i = 0; i < crops.size(); i++) {
+ Rect rect = crops.valueAt(i);
+ left = Math.min(left, rect.left);
+ top = Math.min(top, rect.top);
+ right = Math.max(right, rect.right);
+ bottom = Math.max(bottom, rect.bottom);
+ }
+ return new Rect(left, top, right, bottom);
+ }
+
+ /**
+ * The crops stored in {@link WallpaperData#mCropHints} are relative to the original image.
+ * This computes the crops relative to the sub-image that will actually be rendered on a window.
+ */
+ SparseArray<Rect> getRelativeCropHints(WallpaperData wallpaper) {
+ SparseArray<Rect> result = new SparseArray<>();
+ for (int i = 0; i < wallpaper.mCropHints.size(); i++) {
+ Rect adjustedRect = new Rect(wallpaper.mCropHints.valueAt(i));
+ adjustedRect.offset(-wallpaper.cropHint.left, -wallpaper.cropHint.top);
+ adjustedRect.scale(1f / wallpaper.mSampleSize);
+ result.put(wallpaper.mCropHints.keyAt(i), adjustedRect);
+ }
+ return result;
+ }
+
+ /**
+ * Inverse operation of {@link #getRelativeCropHints}
+ */
+ static List<Rect> getOriginalCropHints(
+ WallpaperData wallpaper, List<Rect> relativeCropHints) {
+ List<Rect> result = new ArrayList<>();
+ for (Rect crop : relativeCropHints) {
+ Rect originalRect = new Rect(crop);
+ originalRect.scale(wallpaper.mSampleSize);
+ originalRect.offset(wallpaper.cropHint.left, wallpaper.cropHint.right);
+ result.add(originalRect);
+ }
+ return result;
+ }
+
+ /**
+ * Given some suggested crops, find cropHints for all orientations of the default display.
+ */
+ SparseArray<Rect> getDefaultCrops(SparseArray<Rect> suggestedCrops, Point bitmapSize) {
+ SparseArray<Rect> result = new SparseArray<>();
+ // add missing cropHints for all orientation of the default display
+ SparseArray<Point> defaultDisplaySizes = mWallpaperDisplayHelper.getDefaultDisplaySizes();
+ boolean rtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
+ == View.LAYOUT_DIRECTION_RTL;
+ for (int i = 0; i < defaultDisplaySizes.size(); i++) {
+ int orientation = defaultDisplaySizes.keyAt(i);
+ Point displaySize = defaultDisplaySizes.valueAt(i);
+ Rect newCrop = getCrop(displaySize, bitmapSize, suggestedCrops, rtl);
+ result.put(orientation, newCrop);
+ }
+ return result;
+ }
+
+ /**
+ * Once a new wallpaper has been written via setWallpaper(...), it needs to be cropped
+ * for display. This will generate the crop and write it in the file.
*/
void generateCrop(WallpaperData wallpaper) {
TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG);
@@ -75,27 +388,47 @@ class WallpaperCropper {
// Only generate crop for default display.
final WallpaperDisplayHelper.DisplayData wpData =
mWallpaperDisplayHelper.getDisplayDataOrCreate(DEFAULT_DISPLAY);
- final Rect cropHint = new Rect(wallpaper.cropHint);
final DisplayInfo displayInfo = mWallpaperDisplayHelper.getDisplayInfo(DEFAULT_DISPLAY);
- if (DEBUG) {
- Slog.v(TAG, "Generating crop for new wallpaper(s): 0x"
- + Integer.toHexString(wallpaper.mWhich)
- + " to " + wallpaper.getCropFile().getName()
- + " crop=(" + cropHint.width() + 'x' + cropHint.height()
- + ") dim=(" + wpData.mWidth + 'x' + wpData.mHeight + ')');
- }
-
// Analyse the source; needed in multiple cases
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(wallpaper.getWallpaperFile().getAbsolutePath(), options);
if (options.outWidth <= 0 || options.outHeight <= 0) {
Slog.w(TAG, "Invalid wallpaper data");
- success = false;
} else {
boolean needCrop = false;
boolean needScale;
+ boolean multiCrop = multiCrop() && wallpaper.mSupportsMultiCrop;
+
+ Point bitmapSize = new Point(options.outWidth, options.outHeight);
+
+ final Rect cropHint;
+ if (multiCrop) {
+ SparseArray<Rect> defaultDisplayCrops =
+ getDefaultCrops(wallpaper.mCropHints, bitmapSize);
+ // adapt the entries in wallpaper.mCropHints for the actual display
+ SparseArray<Rect> updatedCropHints = new SparseArray<>();
+ for (int i = 0; i < wallpaper.mCropHints.size(); i++) {
+ Rect defaultCrop = defaultDisplayCrops.valueAt(i);
+ if (defaultCrop != null) {
+ updatedCropHints.put(defaultDisplayCrops.keyAt(i), defaultCrop);
+ }
+ }
+ wallpaper.mCropHints = updatedCropHints;
+ cropHint = getTotalCrop(defaultDisplayCrops);
+ wallpaper.cropHint.set(cropHint);
+ } else {
+ cropHint = new Rect(wallpaper.cropHint);
+ }
+
+ if (DEBUG) {
+ Slog.v(TAG, "Generating crop for new wallpaper(s): 0x"
+ + Integer.toHexString(wallpaper.mWhich)
+ + " to " + wallpaper.getCropFile().getName()
+ + " crop=(" + cropHint.width() + 'x' + cropHint.height()
+ + ") dim=(" + wpData.mWidth + 'x' + wpData.mHeight + ')');
+ }
// Empty crop means use the full image
if (cropHint.isEmpty()) {
@@ -128,7 +461,7 @@ class WallpaperCropper {
|| cropHint.width() > GLHelper.getMaxTextureSize();
//make sure screen aspect ratio is preserved if width is scaled under screen size
- if (needScale) {
+ if (needScale && !multiCrop) {
final float scaleByHeight = (float) wpData.mHeight / (float) cropHint.height();
final int newWidth = (int) (cropHint.width() * scaleByHeight);
if (newWidth < displayInfo.logicalWidth) {
@@ -171,7 +504,7 @@ class WallpaperCropper {
BufferedOutputStream bos = null;
try {
// This actually downsamples only by powers of two, but that's okay; we do
- // a proper scaling blit later. This is to minimize transient RAM use.
+ // a proper scaling a bit later. This is to minimize transient RAM use.
// We calculate the largest power-of-two under the actual ratio rather than
// just let the decode take care of it because we also want to remap where the
// cropHint rectangle lies in the decoded [super]rect.
@@ -185,19 +518,31 @@ class WallpaperCropper {
final Rect estimateCrop = new Rect(cropHint);
estimateCrop.scale(1f / options.inSampleSize);
- final float hRatio = (float) wpData.mHeight / estimateCrop.height();
+ float hRatio = (float) wpData.mHeight / estimateCrop.height();
+ if (multiCrop) {
+ // make sure the crop height is at most the display largest dimension
+ hRatio = (float) mWallpaperDisplayHelper.getDefaultDisplayLargestDimension()
+ / estimateCrop.height();
+ hRatio = Math.min(hRatio, 1f);
+ }
final int destHeight = (int) (estimateCrop.height() * hRatio);
final int destWidth = (int) (estimateCrop.width() * hRatio);
// We estimated an invalid crop, try to adjust the cropHint to get a valid one.
if (destWidth > GLHelper.getMaxTextureSize()) {
- int newHeight = (int) (wpData.mHeight / hRatio);
- int newWidth = (int) (wpData.mWidth / hRatio);
-
if (DEBUG) {
- Slog.v(TAG, "Invalid crop dimensions, trying to adjust.");
+ Slog.w(TAG, "Invalid crop dimensions, trying to adjust.");
+ }
+ if (multiCrop) {
+ // clear custom crop guidelines, fallback to system default
+ wallpaper.mCropHints.clear();
+ generateCropInternal(wallpaper);
+ return;
}
+ int newHeight = (int) (wpData.mHeight / hRatio);
+ int newWidth = (int) (wpData.mWidth / hRatio);
+
estimateCrop.set(cropHint);
estimateCrop.left += (cropHint.width() - newWidth) / 2;
estimateCrop.top += (cropHint.height() - newHeight) / 2;
@@ -210,8 +555,8 @@ class WallpaperCropper {
// We've got the safe cropHint; now we want to scale it properly to
// the desired rectangle.
// That's a height-biased operation: make it fit the hinted height.
- final int safeHeight = (int) (estimateCrop.height() * hRatio);
- final int safeWidth = (int) (estimateCrop.width() * hRatio);
+ final int safeHeight = (int) (estimateCrop.height() * hRatio + 0.5f);
+ final int safeWidth = (int) (estimateCrop.width() * hRatio + 0.5f);
if (DEBUG_CROP) {
Slog.v(TAG, "Decode parameters:");
@@ -248,6 +593,12 @@ class WallpaperCropper {
// We are safe to create final crop with safe dimensions now.
final Bitmap finalCrop = Bitmap.createScaledBitmap(cropped,
safeWidth, safeHeight, true);
+
+ if (multiCrop) {
+ wallpaper.mSampleSize =
+ ((float) cropHint.height()) / finalCrop.getHeight();
+ }
+
if (DEBUG) {
Slog.v(TAG, "Final extract:");
Slog.v(TAG, " dims: w=" + wpData.mWidth
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperData.java b/services/core/java/com/android/server/wallpaper/WallpaperData.java
index 5c867017f4e0..02594d2d8d22 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperData.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperData.java
@@ -17,6 +17,7 @@
package com.android.server.wallpaper;
import static android.app.WallpaperManager.FLAG_LOCK;
+import static android.app.WallpaperManager.ORIENTATION_UNKNOWN;
import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER;
import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_CROP;
@@ -26,6 +27,7 @@ import static com.android.server.wallpaper.WallpaperUtils.getWallpaperDir;
import android.app.IWallpaperManagerCallback;
import android.app.WallpaperColors;
+import android.app.WallpaperManager.ScreenOrientation;
import android.app.WallpaperManager.SetWallpaperFlags;
import android.content.ComponentName;
import android.graphics.Rect;
@@ -126,10 +128,16 @@ class WallpaperData {
RemoteCallbackList<IWallpaperManagerCallback> callbacks = new RemoteCallbackList<>();
/**
- * The crop hint supplied for displaying a subset of the source image
+ * Defines which part of the {@link #getWallpaperFile()} image is in the {@link #getCropFile()}.
*/
final Rect cropHint = new Rect(0, 0, 0, 0);
+ /**
+ * How much the crop is sub-sampled. A value > 1 means that the image quality was reduced.
+ * This is the ratio between the cropHint height and the actual {@link #getCropFile()} height.
+ */
+ float mSampleSize = 1f;
+
// Describes the context of a call to WallpaperManagerService#bindWallpaperComponentLocked
enum BindSource {
UNKNOWN,
@@ -156,6 +164,23 @@ class WallpaperData {
private final SparseArray<File> mWallpaperFiles = new SparseArray<>();
private final SparseArray<File> mCropFiles = new SparseArray<>();
+ /**
+ * Mapping of {@link ScreenOrientation} -> crop hint. The crop hints are relative to the
+ * original image stored in {@link #getWallpaperFile()}.
+ * Only used when multi crop flag is enabled.
+ */
+ SparseArray<Rect> mCropHints = new SparseArray<>();
+
+ /**
+ * cropHints will be ignored if this flag is false
+ */
+ boolean mSupportsMultiCrop;
+
+ /**
+ * The phone orientation when the wallpaper was set. Only relevant for image wallpapers
+ */
+ int mOrientationWhenSet = ORIENTATION_UNKNOWN;
+
WallpaperData(int userId, @SetWallpaperFlags int wallpaperType) {
this.userId = userId;
this.mWhich = wallpaperType;
@@ -176,6 +201,10 @@ class WallpaperData {
this.mWhich = source.mWhich;
this.wallpaperId = source.wallpaperId;
this.cropHint.set(source.cropHint);
+ if (source.mCropHints != null) {
+ this.mCropHints = source.mCropHints.clone();
+ }
+ this.mSupportsMultiCrop = source.mSupportsMultiCrop;
this.allowBackup = source.allowBackup;
this.primaryColors = source.primaryColors;
this.mWallpaperDimAmount = source.mWallpaperDimAmount;
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
index de98df55c3ea..88e9672cd0a1 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
@@ -18,6 +18,7 @@ package com.android.server.wallpaper;
import static android.app.WallpaperManager.FLAG_LOCK;
import static android.app.WallpaperManager.FLAG_SYSTEM;
+import static android.app.WallpaperManager.ORIENTATION_UNKNOWN;
import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.server.wallpaper.WallpaperDisplayHelper.DisplayData;
@@ -26,9 +27,11 @@ import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_CROP;
import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_INFO;
import static com.android.server.wallpaper.WallpaperUtils.getWallpaperDir;
import static com.android.server.wallpaper.WallpaperUtils.makeWallpaperIdLocked;
+import static com.android.window.flags.Flags.multiCrop;
import android.annotation.Nullable;
import android.app.WallpaperColors;
+import android.app.WallpaperManager;
import android.app.WallpaperManager.SetWallpaperFlags;
import android.app.backup.WallpaperBackupHelper;
import android.content.ComponentName;
@@ -36,7 +39,9 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Color;
+import android.graphics.Rect;
import android.os.FileUtils;
+import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import android.util.Xml;
@@ -60,14 +65,16 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
/**
* Helper for the wallpaper loading / saving / xml parsing
* Only meant to be used lock held by WallpaperManagerService
* Only meant to be instantiated once by WallpaperManagerService
+ * @hide
*/
-class WallpaperDataParser {
+public class WallpaperDataParser {
private static final String TAG = WallpaperDataParser.class.getSimpleName();
private static final boolean DEBUG = false;
@@ -132,6 +139,7 @@ class WallpaperDataParser {
*/
public WallpaperLoadingResult loadSettingsLocked(int userId, boolean keepDimensionHints,
boolean migrateFromOld, @SetWallpaperFlags int which) {
+ // TODO(b/270726737) remove the "keepDimensionHints" arg when removing the multi crop flag
JournaledFile journal = makeJournaledFile(userId);
FileInputStream stream = null;
File file = journal.chooseForRead();
@@ -174,7 +182,9 @@ class WallpaperDataParser {
WallpaperData wallpaperToParse =
"wp".equals(tag) ? wallpaper : lockWallpaper;
- parseWallpaperAttributes(parser, wallpaperToParse, keepDimensionHints);
+ if (!multiCrop()) {
+ parseWallpaperAttributes(parser, wallpaperToParse, keepDimensionHints);
+ }
String comp = parser.getAttributeValue(null, "component");
wallpaperToParse.nextWallpaperComponent = comp != null
@@ -186,6 +196,10 @@ class WallpaperDataParser {
wallpaperToParse.nextWallpaperComponent = mImageWallpaper;
}
+ if (multiCrop()) {
+ parseWallpaperAttributes(parser, wallpaperToParse, keepDimensionHints);
+ }
+
if (DEBUG) {
Slog.v(TAG, "mWidth:" + wpdData.mWidth);
Slog.v(TAG, "mHeight:" + wpdData.mHeight);
@@ -300,20 +314,48 @@ class WallpaperDataParser {
wallpaper.wallpaperId = makeWallpaperIdLocked();
}
- final DisplayData wpData = mWallpaperDisplayHelper.getDisplayDataOrCreate(DEFAULT_DISPLAY);
-
- if (!keepDimensionHints) {
- wpData.mWidth = parser.getAttributeInt(null, "width");
- wpData.mHeight = parser.getAttributeInt(null, "height");
+ Rect totalCropHint = new Rect(
+ getAttributeInt(parser, "totalCropLeft", 0),
+ getAttributeInt(parser, "totalCropTop", 0),
+ getAttributeInt(parser, "totalCropRight", 0),
+ getAttributeInt(parser, "totalCropBottom", 0));
+ wallpaper.mSupportsMultiCrop = multiCrop() && (
+ parser.getAttributeBoolean(null, "supportsMultiCrop", false)
+ || mImageWallpaper.equals(wallpaper.wallpaperComponent));
+ if (wallpaper.mSupportsMultiCrop) {
+ wallpaper.mCropHints = new SparseArray<>();
+ for (Pair<Integer, String> pair: screenDimensionPairs()) {
+ Rect cropHint = new Rect(
+ parser.getAttributeInt(null, "cropLeft" + pair.second, 0),
+ parser.getAttributeInt(null, "cropTop" + pair.second, 0),
+ parser.getAttributeInt(null, "cropRight" + pair.second, 0),
+ parser.getAttributeInt(null, "cropBottom" + pair.second, 0));
+ if (!cropHint.isEmpty()) wallpaper.mCropHints.put(pair.first, cropHint);
+ }
+ if (wallpaper.mCropHints.size() == 0) {
+ // migration case: the crops per screen orientation are not specified.
+ // use the old attributes to find the crop for one screen orientation.
+ Integer orientation = totalCropHint.width() < totalCropHint.height()
+ ? WallpaperManager.PORTRAIT : WallpaperManager.LANDSCAPE;
+ if (!totalCropHint.isEmpty()) wallpaper.mCropHints.put(orientation, totalCropHint);
+ } else {
+ wallpaper.cropHint.set(totalCropHint);
+ }
+ } else {
+ wallpaper.cropHint.set(totalCropHint);
+ }
+ final DisplayData wpData = mWallpaperDisplayHelper
+ .getDisplayDataOrCreate(DEFAULT_DISPLAY);
+ if (!keepDimensionHints && !multiCrop()) {
+ wpData.mWidth = parser.getAttributeInt(null, "width", 0);
+ wpData.mHeight = parser.getAttributeInt(null, "height", 0);
+ }
+ if (!multiCrop()) {
+ wpData.mPadding.left = getAttributeInt(parser, "paddingLeft", 0);
+ wpData.mPadding.top = getAttributeInt(parser, "paddingTop", 0);
+ wpData.mPadding.right = getAttributeInt(parser, "paddingRight", 0);
+ wpData.mPadding.bottom = getAttributeInt(parser, "paddingBottom", 0);
}
- wallpaper.cropHint.left = getAttributeInt(parser, "cropLeft", 0);
- wallpaper.cropHint.top = getAttributeInt(parser, "cropTop", 0);
- wallpaper.cropHint.right = getAttributeInt(parser, "cropRight", 0);
- wallpaper.cropHint.bottom = getAttributeInt(parser, "cropBottom", 0);
- wpData.mPadding.left = getAttributeInt(parser, "paddingLeft", 0);
- wpData.mPadding.top = getAttributeInt(parser, "paddingTop", 0);
- wpData.mPadding.right = getAttributeInt(parser, "paddingRight", 0);
- wpData.mPadding.bottom = getAttributeInt(parser, "paddingBottom", 0);
wallpaper.mWallpaperDimAmount = getAttributeFloat(parser, "dimAmount", 0f);
BindSource bindSource;
try {
@@ -365,11 +407,11 @@ class WallpaperDataParser {
wallpaper.allowBackup = parser.getAttributeBoolean(null, "backup", false);
}
- private int getAttributeInt(TypedXmlPullParser parser, String name, int defValue) {
+ private static int getAttributeInt(TypedXmlPullParser parser, String name, int defValue) {
return parser.getAttributeInt(null, name, defValue);
}
- private float getAttributeFloat(TypedXmlPullParser parser, String name, float defValue) {
+ private static float getAttributeFloat(TypedXmlPullParser parser, String name, float defValue) {
return parser.getAttributeFloat(null, name, defValue);
}
@@ -412,28 +454,66 @@ class WallpaperDataParser {
if (DEBUG) {
Slog.v(TAG, "writeWallpaperAttributes id=" + wallpaper.wallpaperId);
}
- final DisplayData wpdData = mWallpaperDisplayHelper.getDisplayDataOrCreate(DEFAULT_DISPLAY);
out.startTag(null, tag);
out.attributeInt(null, "id", wallpaper.wallpaperId);
- out.attributeInt(null, "width", wpdData.mWidth);
- out.attributeInt(null, "height", wpdData.mHeight);
- out.attributeInt(null, "cropLeft", wallpaper.cropHint.left);
- out.attributeInt(null, "cropTop", wallpaper.cropHint.top);
- out.attributeInt(null, "cropRight", wallpaper.cropHint.right);
- out.attributeInt(null, "cropBottom", wallpaper.cropHint.bottom);
+ out.attributeBoolean(null, "supportsMultiCrop", wallpaper.mSupportsMultiCrop);
- if (wpdData.mPadding.left != 0) {
- out.attributeInt(null, "paddingLeft", wpdData.mPadding.left);
- }
- if (wpdData.mPadding.top != 0) {
- out.attributeInt(null, "paddingTop", wpdData.mPadding.top);
- }
- if (wpdData.mPadding.right != 0) {
- out.attributeInt(null, "paddingRight", wpdData.mPadding.right);
- }
- if (wpdData.mPadding.bottom != 0) {
- out.attributeInt(null, "paddingBottom", wpdData.mPadding.bottom);
+ if (multiCrop() && wallpaper.mSupportsMultiCrop) {
+ if (wallpaper.mCropHints == null) {
+ Slog.e(TAG, "cropHints should not be null when saved");
+ wallpaper.mCropHints = new SparseArray<>();
+ }
+ for (Pair<Integer, String> pair : screenDimensionPairs()) {
+ Rect cropHint = wallpaper.mCropHints.get(pair.first);
+ if (cropHint == null) continue;
+ out.attributeInt(null, "cropLeft" + pair.second, cropHint.left);
+ out.attributeInt(null, "cropTop" + pair.second, cropHint.top);
+ out.attributeInt(null, "cropRight" + pair.second, cropHint.right);
+ out.attributeInt(null, "cropBottom" + pair.second, cropHint.bottom);
+
+ // to support back compatibility in B&R, save the crops for one orientation in the
+ // legacy "cropLeft", "cropTop", "cropRight", "cropBottom" entries
+ int orientationToPutInLegacyCrop = wallpaper.mOrientationWhenSet;
+ if (mWallpaperDisplayHelper.isFoldable()) {
+ int unfoldedOrientation = mWallpaperDisplayHelper
+ .getUnfoldedOrientation(orientationToPutInLegacyCrop);
+ if (unfoldedOrientation != ORIENTATION_UNKNOWN) {
+ orientationToPutInLegacyCrop = unfoldedOrientation;
+ }
+ }
+ if (pair.first == orientationToPutInLegacyCrop) {
+ out.attributeInt(null, "cropLeft", cropHint.left);
+ out.attributeInt(null, "cropTop", cropHint.top);
+ out.attributeInt(null, "cropRight", cropHint.right);
+ out.attributeInt(null, "cropBottom", cropHint.bottom);
+ }
+ }
+ out.attributeInt(null, "totalCropLeft", wallpaper.cropHint.left);
+ out.attributeInt(null, "totalCropTop", wallpaper.cropHint.top);
+ out.attributeInt(null, "totalCropRight", wallpaper.cropHint.right);
+ out.attributeInt(null, "totalCropBottom", wallpaper.cropHint.bottom);
+ } else if (!multiCrop()) {
+ final DisplayData wpdData =
+ mWallpaperDisplayHelper.getDisplayDataOrCreate(DEFAULT_DISPLAY);
+ out.attributeInt(null, "width", wpdData.mWidth);
+ out.attributeInt(null, "height", wpdData.mHeight);
+ out.attributeInt(null, "cropLeft", wallpaper.cropHint.left);
+ out.attributeInt(null, "cropTop", wallpaper.cropHint.top);
+ out.attributeInt(null, "cropRight", wallpaper.cropHint.right);
+ out.attributeInt(null, "cropBottom", wallpaper.cropHint.bottom);
+ if (wpdData.mPadding.left != 0) {
+ out.attributeInt(null, "paddingLeft", wpdData.mPadding.left);
+ }
+ if (wpdData.mPadding.top != 0) {
+ out.attributeInt(null, "paddingTop", wpdData.mPadding.top);
+ }
+ if (wpdData.mPadding.right != 0) {
+ out.attributeInt(null, "paddingRight", wpdData.mPadding.right);
+ }
+ if (wpdData.mPadding.bottom != 0) {
+ out.attributeInt(null, "paddingBottom", wpdData.mPadding.bottom);
+ }
}
out.attributeFloat(null, "dimAmount", wallpaper.mWallpaperDimAmount);
@@ -564,4 +644,12 @@ class WallpaperDataParser {
}
return false;
}
+
+ private static List<Pair<Integer, String>> screenDimensionPairs() {
+ return List.of(
+ new Pair<>(WallpaperManager.PORTRAIT, "Portrait"),
+ new Pair<>(WallpaperManager.LANDSCAPE, "Landscape"),
+ new Pair<>(WallpaperManager.SQUARE_PORTRAIT, "SquarePortrait"),
+ new Pair<>(WallpaperManager.SQUARE_LANDSCAPE, "SquareLandscape"));
+ }
}
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java
index f48178c5b9f7..19fd9a90518d 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java
@@ -16,19 +16,31 @@
package com.android.server.wallpaper;
+import static android.app.WallpaperManager.ORIENTATION_UNKNOWN;
+import static android.app.WallpaperManager.getRotatedOrientation;
import static android.view.Display.DEFAULT_DISPLAY;
+import static com.android.window.flags.Flags.multiCrop;
+
+import android.app.WallpaperManager;
+import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.os.Binder;
import android.os.Debug;
+import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import android.view.Display;
import android.view.DisplayInfo;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
import com.android.server.wm.WindowManagerInternal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
import java.util.function.Consumer;
/**
@@ -50,12 +62,55 @@ class WallpaperDisplayHelper {
private final SparseArray<DisplayData> mDisplayDatas = new SparseArray<>();
private final DisplayManager mDisplayManager;
private final WindowManagerInternal mWindowManagerInternal;
+ private final SparseArray<Point> mDefaultDisplaySizes = new SparseArray<>();
+
+ // related orientations pairs for foldable (folded orientation, unfolded orientation)
+ private final List<Pair<Integer, Integer>> mFoldableOrientationPairs = new ArrayList<>();
+
+ private boolean mIsFoldable;
WallpaperDisplayHelper(
DisplayManager displayManager,
- WindowManagerInternal windowManagerInternal) {
+ WindowManager windowManager,
+ WindowManagerInternal windowManagerInternal,
+ boolean isFoldable) {
mDisplayManager = displayManager;
mWindowManagerInternal = windowManagerInternal;
+ mIsFoldable = isFoldable;
+ if (!multiCrop()) return;
+ Set<WindowMetrics> metrics = windowManager.getPossibleMaximumWindowMetrics(DEFAULT_DISPLAY);
+ boolean populateOrientationPairs = isFoldable && metrics.size() == 2;
+ float surface = 0;
+ int firstOrientation = -1;
+ for (WindowMetrics metric: metrics) {
+ Rect bounds = metric.getBounds();
+ Point displaySize = new Point(bounds.width(), bounds.height());
+ Point reversedDisplaySize = new Point(displaySize.y, displaySize.x);
+ for (Point point : List.of(displaySize, reversedDisplaySize)) {
+ int orientation = WallpaperManager.getOrientation(point);
+ // don't add an entry if there is already a larger display of the same orientation
+ Point display = mDefaultDisplaySizes.get(orientation);
+ if (display == null || display.x * display.y < point.x * point.y) {
+ mDefaultDisplaySizes.put(orientation, point);
+ }
+ }
+ if (populateOrientationPairs) {
+ int orientation = WallpaperManager.getOrientation(displaySize);
+ float newSurface = displaySize.x * displaySize.y * metric.getDensity();
+ if (surface <= 0) {
+ surface = newSurface;
+ firstOrientation = orientation;
+ } else {
+ Pair<Integer, Integer> pair = (newSurface > surface)
+ ? new Pair<>(firstOrientation, orientation)
+ : new Pair<>(orientation, firstOrientation);
+ Pair<Integer, Integer> rotatedPair = new Pair<>(
+ getRotatedOrientation(pair.first), getRotatedOrientation(pair.second));
+ mFoldableOrientationPairs.add(pair);
+ mFoldableOrientationPairs.add(rotatedPair);
+ }
+ }
+ }
}
DisplayData getDisplayDataOrCreate(int displayId) {
@@ -68,6 +123,12 @@ class WallpaperDisplayHelper {
return wpdData;
}
+ int getDefaultDisplayCurrentOrientation() {
+ Point displaySize = new Point();
+ mDisplayManager.getDisplay(DEFAULT_DISPLAY).getSize(displaySize);
+ return WallpaperManager.getOrientation(displaySize);
+ }
+
void removeDisplayData(int displayId) {
mDisplayDatas.remove(displayId);
}
@@ -133,4 +194,46 @@ class WallpaperDisplayHelper {
boolean isValidDisplay(int displayId) {
return mDisplayManager.getDisplay(displayId) != null;
}
+
+ SparseArray<Point> getDefaultDisplaySizes() {
+ return mDefaultDisplaySizes;
+ }
+
+ /** Return the number of pixel of the largest dimension of the default display */
+ int getDefaultDisplayLargestDimension() {
+ int result = -1;
+ for (int i = 0; i < mDefaultDisplaySizes.size(); i++) {
+ Point size = mDefaultDisplaySizes.valueAt(i);
+ result = Math.max(result, Math.max(size.x, size.y));
+ }
+ return result;
+ }
+
+ boolean isFoldable() {
+ return mIsFoldable;
+ }
+
+ /**
+ * If a given orientation corresponds to an unfolded orientation on foldable, return the
+ * corresponding folded orientation. Otherwise, return UNKNOWN. Always return UNKNOWN if the
+ * device is not a foldable.
+ */
+ int getFoldedOrientation(int orientation) {
+ for (Pair<Integer, Integer> pair : mFoldableOrientationPairs) {
+ if (pair.second.equals(orientation)) return pair.first;
+ }
+ return ORIENTATION_UNKNOWN;
+ }
+
+ /**
+ * If a given orientation corresponds to a folded orientation on foldable, return the
+ * corresponding unfolded orientation. Otherwise, return UNKNOWN. Always return UNKNOWN if the
+ * device is not a foldable.
+ */
+ int getUnfoldedOrientation(int orientation) {
+ for (Pair<Integer, Integer> pair : mFoldableOrientationPairs) {
+ if (pair.first.equals(orientation)) return pair.second;
+ }
+ return ORIENTATION_UNKNOWN;
+ }
}
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 26584315f15c..8c27bb80d337 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -22,6 +22,7 @@ import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREG
import static android.app.WallpaperManager.COMMAND_REAPPLY;
import static android.app.WallpaperManager.FLAG_LOCK;
import static android.app.WallpaperManager.FLAG_SYSTEM;
+import static android.app.WallpaperManager.ORIENTATION_UNKNOWN;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AUTO;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.ParcelFileDescriptor.MODE_CREATE;
@@ -74,6 +75,7 @@ import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
+import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.hardware.display.DisplayManager;
@@ -103,12 +105,15 @@ import android.service.wallpaper.IWallpaperService;
import android.service.wallpaper.WallpaperService;
import android.system.ErrnoException;
import android.system.Os;
+import android.text.TextUtils;
import android.util.EventLog;
import android.util.IntArray;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.view.Display;
+import android.view.View;
+import android.view.WindowManager;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
@@ -137,6 +142,7 @@ import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -189,8 +195,6 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
}
private final Object mLock = new Object();
- /** True to support different crops for different display dimensions */
- private final boolean mIsMultiCropEnabled;
/** Tracks wallpaper being migrated from system+lock to lock when setting static wp. */
WallpaperDestinationChangeHandler mPendingMigrationViaStatic;
@@ -804,6 +808,12 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
null /* options */);
mWindowManagerInternal.setWallpaperShowWhenLocked(
mToken, (wallpaper.mWhich & FLAG_LOCK) != 0);
+ if (multiCrop() && wallpaper.mSupportsMultiCrop) {
+ mWindowManagerInternal.setWallpaperCropHints(mToken,
+ mWallpaperCropper.getRelativeCropHints(wallpaper));
+ } else {
+ mWindowManagerInternal.setWallpaperCropHints(mToken, new SparseArray<>());
+ }
final DisplayData wpdData =
mWallpaperDisplayHelper.getDisplayDataOrCreate(mDisplayId);
try {
@@ -1479,10 +1489,15 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
mIPackageManager = AppGlobals.getPackageManager();
mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
- DisplayManager dm = mContext.getSystemService(DisplayManager.class);
- dm.registerDisplayListener(mDisplayListener, null /* handler */);
- mWallpaperDisplayHelper = new WallpaperDisplayHelper(dm, mWindowManagerInternal);
+ DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
+ displayManager.registerDisplayListener(mDisplayListener, null /* handler */);
+ WindowManager windowManager = mContext.getSystemService(WindowManager.class);
+ boolean isFoldable = mContext.getResources()
+ .getIntArray(R.array.config_foldedDeviceStates).length > 0;
+ mWallpaperDisplayHelper = new WallpaperDisplayHelper(
+ displayManager, windowManager, mWindowManagerInternal, isFoldable);
mWallpaperCropper = new WallpaperCropper(mWallpaperDisplayHelper);
+ mWindowManagerInternal.setWallpaperCropUtils(mWallpaperCropper::getCrop);
mActivityManager = mContext.getSystemService(ActivityManager.class);
if (mContext.getResources().getBoolean(
@@ -1522,7 +1537,6 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
mColorsChangedListeners = new SparseArray<>();
mWallpaperDataParser = new WallpaperDataParser(mContext, mWallpaperDisplayHelper,
mWallpaperCropper);
- mIsMultiCropEnabled = multiCrop();
LocalServices.addService(WallpaperManagerInternal.class, new LocalService());
}
@@ -2199,6 +2213,66 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
}
}
+ @Override
+ public List<Rect> getBitmapCrops(List<Point> displaySizes, @SetWallpaperFlags int which,
+ boolean originalBitmap, int userId) {
+ userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
+ Binder.getCallingUid(), userId, false, true, "getBitmapCrop", null);
+ synchronized (mLock) {
+ checkPermission(READ_WALLPAPER_INTERNAL);
+ WallpaperData wallpaper = (which == FLAG_LOCK) ? mLockWallpaperMap.get(userId)
+ : mWallpaperMap.get(userId);
+ if (wallpaper == null || !wallpaper.mSupportsMultiCrop) return null;
+ SparseArray<Rect> relativeSuggestedCrops =
+ mWallpaperCropper.getRelativeCropHints(wallpaper);
+ Point croppedBitmapSize =
+ new Point(wallpaper.cropHint.width(), wallpaper.cropHint.height());
+ SparseArray<Rect> relativeDefaultCrops =
+ mWallpaperCropper.getDefaultCrops(relativeSuggestedCrops, croppedBitmapSize);
+ SparseArray<Rect> adjustedRelativeSuggestedCrops = new SparseArray<>();
+ for (int i = 0; i < relativeDefaultCrops.size(); i++) {
+ int key = relativeDefaultCrops.keyAt(i);
+ if (relativeSuggestedCrops.contains(key)) {
+ adjustedRelativeSuggestedCrops.put(key, relativeDefaultCrops.get(key));
+ }
+ }
+ List<Rect> result = new ArrayList<>();
+ boolean rtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
+ == View.LAYOUT_DIRECTION_RTL;
+ for (Point displaySize : displaySizes) {
+ result.add(mWallpaperCropper.getCrop(
+ displaySize, croppedBitmapSize, adjustedRelativeSuggestedCrops, rtl));
+ }
+ if (originalBitmap) result = WallpaperCropper.getOriginalCropHints(wallpaper, result);
+ return result;
+ }
+ }
+
+ @Override
+ public List<Rect> getFutureBitmapCrops(Point bitmapSize, List<Point> displaySizes,
+ int[] screenOrientations, List<Rect> crops) {
+ SparseArray<Rect> cropMap = getCropMap(screenOrientations, crops, ORIENTATION_UNKNOWN);
+ SparseArray<Rect> defaultCrops = mWallpaperCropper.getDefaultCrops(cropMap, bitmapSize);
+ List<Rect> result = new ArrayList<>();
+ boolean rtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
+ == View.LAYOUT_DIRECTION_RTL;
+ for (Point displaySize : displaySizes) {
+ result.add(mWallpaperCropper.getCrop(displaySize, bitmapSize, defaultCrops, rtl));
+ }
+ return result;
+ }
+
+ @Override
+ public Rect getBitmapCrop(Point bitmapSize, int[] screenOrientations, List<Rect> crops) {
+ if (!multiCrop()) {
+ throw new UnsupportedOperationException(
+ "This method should only be called with the multi crop flag enabled");
+ }
+ SparseArray<Rect> cropMap = getCropMap(screenOrientations, crops, ORIENTATION_UNKNOWN);
+ SparseArray<Rect> defaultCrops = mWallpaperCropper.getDefaultCrops(cropMap, bitmapSize);
+ return WallpaperCropper.getTotalCrop(defaultCrops);
+ }
+
private boolean hasPermission(String permission) {
return mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED;
}
@@ -2755,8 +2829,18 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
@Override
public ParcelFileDescriptor setWallpaper(String name, String callingPackage,
- Rect cropHint, boolean allowBackup, Bundle extras, int which,
- IWallpaperManagerCallback completion, int userId) {
+ int[] screenOrientations, List<Rect> crops, boolean allowBackup,
+ Bundle extras, int which, IWallpaperManagerCallback completion, int userId) {
+
+ if (DEBUG) {
+ Slog.d(TAG, "setWallpaper: name = " + name + ", callingPackage = " + callingPackage
+ + ", screenOrientations = "
+ + (screenOrientations == null ? null
+ : Arrays.stream(screenOrientations).boxed().toList())
+ + ", crops = " + crops
+ + ", allowBackup = " + allowBackup);
+ }
+
userId = ActivityManager.handleIncomingUser(getCallingPid(), getCallingUid(), userId,
false /* all */, true /* full */, "changing wallpaper", null /* pkg */);
checkPermission(android.Manifest.permission.SET_WALLPAPER);
@@ -2771,10 +2855,17 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
return null;
}
+ int currentOrientation = mWallpaperDisplayHelper.getDefaultDisplayCurrentOrientation();
+ SparseArray<Rect> cropMap = !multiCrop() ? null
+ : getCropMap(screenOrientations, crops, currentOrientation);
+ Rect cropHint = multiCrop() || crops == null ? null : crops.get(0);
+ final boolean fromForegroundApp = !multiCrop() ? false
+ : isFromForegroundApp(callingPackage);
+
// "null" means the no-op crop, preserving the full input image
- if (cropHint == null) {
+ if (cropHint == null && !multiCrop()) {
cropHint = new Rect(0, 0, 0, 0);
- } else {
+ } else if (!multiCrop()) {
if (cropHint.width() < 0 || cropHint.height() < 0
|| cropHint.left < 0
|| cropHint.top < 0) {
@@ -2814,10 +2905,14 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
wallpaper.mSystemWasBoth = systemIsBoth;
wallpaper.mWhich = which;
wallpaper.setComplete = completion;
- wallpaper.fromForegroundApp = isFromForegroundApp(callingPackage);
- wallpaper.cropHint.set(cropHint);
+ wallpaper.fromForegroundApp = multiCrop() ? fromForegroundApp
+ : isFromForegroundApp(callingPackage);
+ if (!multiCrop()) wallpaper.cropHint.set(cropHint);
+ if (multiCrop()) wallpaper.mSupportsMultiCrop = true;
+ if (multiCrop()) wallpaper.mCropHints = cropMap;
wallpaper.allowBackup = allowBackup;
wallpaper.mWallpaperDimAmount = getWallpaperDimAmount();
+ wallpaper.mOrientationWhenSet = currentOrientation;
}
return pfd;
} finally {
@@ -2826,11 +2921,47 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
}
}
+ private SparseArray<Rect> getCropMap(int[] screenOrientations, List<Rect> crops,
+ int currentOrientation) {
+ if ((crops == null ^ screenOrientations == null)
+ || (crops != null && crops.size() != screenOrientations.length)) {
+ throw new IllegalArgumentException(
+ "Illegal crops/orientations lists: must both be null, or both the same size");
+ }
+ SparseArray<Rect> cropMap = new SparseArray<>();
+ boolean unknown = false;
+ if (crops != null && crops.size() != 0) {
+ for (int i = 0; i < crops.size(); i++) {
+ Rect crop = crops.get(i);
+ int width = crop.width(), height = crop.height();
+ if (width < 0 || height < 0 || crop.left < 0 || crop.top < 0) {
+ throw new IllegalArgumentException("Invalid crop rect supplied: " + crop);
+ }
+ int orientation = screenOrientations[i];
+ if (orientation == ORIENTATION_UNKNOWN) {
+ if (currentOrientation == ORIENTATION_UNKNOWN) {
+ throw new IllegalArgumentException(
+ "Invalid orientation: " + ORIENTATION_UNKNOWN);
+ }
+ unknown = true;
+ orientation = currentOrientation;
+ }
+ cropMap.put(orientation, crop);
+ }
+ }
+ if (unknown && cropMap.size() > 1) {
+ throw new IllegalArgumentException("Invalid crops supplied: the UNKNOWN screen "
+ + "orientation should only be used in a singleton map (in which case it"
+ + "represents the current orientation of the default display)");
+ }
+ return cropMap;
+ }
+
private void migrateStaticSystemToLockWallpaperLocked(int userId) {
WallpaperData sysWP = mWallpaperMap.get(userId);
if (sysWP == null) {
if (DEBUG) {
- Slog.i(TAG, "No system wallpaper? Not tracking for lock-only");
+ Slog.i(TAG, "No system wallpaper? Not tracking for lock-only");
}
return;
}
@@ -2839,6 +2970,10 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
WallpaperData lockWP = new WallpaperData(userId, FLAG_LOCK);
lockWP.wallpaperId = sysWP.wallpaperId;
lockWP.cropHint.set(sysWP.cropHint);
+ lockWP.mSupportsMultiCrop = sysWP.mSupportsMultiCrop;
+ if (sysWP.mCropHints != null) {
+ lockWP.mCropHints = sysWP.mCropHints.clone();
+ }
lockWP.allowBackup = sysWP.allowBackup;
lockWP.primaryColors = sysWP.primaryColors;
lockWP.mWallpaperDimAmount = sysWP.mWallpaperDimAmount;
@@ -2956,6 +3091,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
final long ident = Binder.clearCallingIdentity();
try {
+ newWallpaper.mSupportsMultiCrop = mImageWallpaper.equals(name);
newWallpaper.imageWallpaperPending = false;
newWallpaper.mWhich = which;
newWallpaper.mSystemWasBoth = systemIsBoth;
@@ -3428,11 +3564,6 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
return (wallpaper != null) ? wallpaper.allowBackup : false;
}
- @Override
- public boolean isMultiCropEnabled() {
- return mIsMultiCropEnabled;
- }
-
private void onDisplayReadyInternal(int displayId) {
synchronized (mLock) {
if (mLastWallpaper == null) {
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index a9f0554b2bec..d68f932400a2 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -34,6 +34,7 @@ import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WALLPAPER;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import static com.android.server.wm.WindowManagerService.H.WALLPAPER_DRAW_PENDING_TIMEOUT;
+import static com.android.window.flags.Flags.multiCrop;
import android.annotation.Nullable;
import android.content.res.Resources;
@@ -48,6 +49,7 @@ import android.os.SystemClock;
import android.util.ArraySet;
import android.util.MathUtils;
import android.util.Slog;
+import android.util.SparseArray;
import android.view.Display;
import android.view.DisplayInfo;
import android.view.SurfaceControl;
@@ -60,6 +62,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.ProtoLogImpl;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.ToBooleanFunction;
+import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -73,6 +76,7 @@ import java.util.function.Consumer;
class WallpaperController {
private static final String TAG = TAG_WITH_CLASS_NAME ? "WallpaperController" : TAG_WM;
private WindowManagerService mService;
+ private WallpaperCropUtils mWallpaperCropUtils = null;
private DisplayContent mDisplayContent;
private final ArrayList<WallpaperWindowToken> mWallpaperTokens = new ArrayList<>();
@@ -240,9 +244,8 @@ class WallpaperController {
mMinWallpaperScale =
resources.getFloat(com.android.internal.R.dimen.config_wallpaperMinScale);
mMaxWallpaperScale = resources.getFloat(R.dimen.config_wallpaperMaxScale);
- mShouldOffsetWallpaperCenter =
- resources.getBoolean(
- com.android.internal.R.bool.config_offsetWallpaperToCenterOfLargestDisplay);
+ mShouldOffsetWallpaperCenter = resources.getBoolean(
+ com.android.internal.R.bool.config_offsetWallpaperToCenterOfLargestDisplay);
}
void resetLargestDisplay(Display display) {
@@ -266,7 +269,7 @@ class WallpaperController {
}
@Nullable private Point findLargestDisplaySize() {
- if (!mShouldOffsetWallpaperCenter) {
+ if (!mShouldOffsetWallpaperCenter || multiCrop()) {
return null;
}
Point largestDisplaySize = new Point();
@@ -284,6 +287,10 @@ class WallpaperController {
return largestDisplaySize;
}
+ void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils) {
+ mWallpaperCropUtils = wallpaperCropUtils;
+ }
+
WindowState getWallpaperTarget() {
return mWallpaperTarget;
}
@@ -357,26 +364,92 @@ class WallpaperController {
boolean updateWallpaperOffset(WindowState wallpaperWin, boolean sync) {
// Size of the display the wallpaper is rendered on.
final Rect lastWallpaperBounds = wallpaperWin.getParentFrame();
- // Full size of the wallpaper (usually larger than bounds above to parallax scroll when
- // swiping through Launcher pages).
- final Rect wallpaperFrame = wallpaperWin.getFrame();
+ int screenWidth = lastWallpaperBounds.width();
+ int screenHeight = lastWallpaperBounds.height();
+ float screenRatio = ((float) screenWidth) / screenHeight;
+ Point screenSize = new Point(screenWidth, screenHeight);
+
WallpaperWindowToken token = wallpaperWin.mToken.asWallpaperToken();
- final int diffWidth = wallpaperFrame.width() - lastWallpaperBounds.width();
- final int diffHeight = wallpaperFrame.height() - lastWallpaperBounds.height();
- if ((wallpaperWin.mAttrs.flags & WindowManager.LayoutParams.FLAG_SCALED) != 0
- && Math.abs(diffWidth) > 1 && Math.abs(diffHeight) > 1) {
- Slog.d(TAG, "Skip wallpaper offset with inconsistent orientation, bounds="
- + lastWallpaperBounds + " frame=" + wallpaperFrame);
- // With FLAG_SCALED, the requested size should at least make the frame match one of
- // side. If both sides contain differences, the client side may not have updated the
- // latest size according to the current orientation. So skip calculating the offset to
- // avoid the wallpaper not filling the screen.
- return false;
+ /*
+ * TODO(b/270726737) adapt comments once flag gets removed and multiCrop is always true
+ * Size of the wallpaper. May have more width/height ratio than the screen for parallax.
+ *
+ * If multiCrop is true, we use a map, cropHints, defining which sub-area of the wallpaper
+ * to show for a given screen orientation. In this case, wallpaperFrame represents the
+ * sub-area of WallpaperWin to show for the current screen size.
+ *
+ * If multiCrop is false, don't show a custom sub-area of the wallpaper. Just show the
+ * whole wallpaperWin if possible, and center and zoom if necessary.
+ */
+ final Rect wallpaperFrame;
+
+ /*
+ * The values cropZoom, cropOffsetX and cropOffsetY are only used if multiCrop is true.
+ * Zoom and offsets to be applied in order to show wallpaperFrame on screen.
+ */
+ final float cropZoom;
+ final int cropOffsetX;
+ final int cropOffsetY;
+
+ /*
+ * Difference of width/height between the wallpaper and the screen.
+ * This is the additional room that we have to apply offsets (i.e. parallax).
+ */
+ final int diffWidth;
+ final int diffHeight;
+
+ /*
+ * zoom, offsetX and offsetY are not related to cropping the wallpaper:
+ * - zoom is used to apply an additional zoom (e.g. for launcher animations).
+ * - offsetX, offsetY are used to apply an offset to the wallpaper (e.g. parallax effect).
+ */
+ final float zoom;
+ int offsetX;
+ int offsetY;
+
+ if (multiCrop()) {
+ if (mWallpaperCropUtils == null) {
+ Slog.e(TAG, "Update wallpaper offsets before the system is ready. Aborting");
+ return false;
+ }
+ Point bitmapSize = new Point(
+ wallpaperWin.mRequestedWidth, wallpaperWin.mRequestedHeight);
+ SparseArray<Rect> cropHints = token.getCropHints();
+ wallpaperFrame = mWallpaperCropUtils.getCrop(
+ screenSize, bitmapSize, cropHints, wallpaperWin.isRtl());
+
+ cropZoom = wallpaperFrame.isEmpty() ? 1f
+ : ((float) screenHeight) / wallpaperFrame.height() / wallpaperWin.mVScale;
+
+ // A positive x / y offset shifts the wallpaper to the right / bottom respectively.
+ cropOffsetX = -wallpaperFrame.left
+ + (int) ((cropZoom - 1f) * wallpaperFrame.height() * screenRatio / 2f);
+ cropOffsetY = -wallpaperFrame.top
+ + (int) ((cropZoom - 1f) * wallpaperFrame.height() / 2f);
+
+ diffWidth = (int) (wallpaperFrame.width() * wallpaperWin.mHScale) - screenWidth;
+ diffHeight = (int) (wallpaperFrame.height() * wallpaperWin.mVScale) - screenHeight;
+ } else {
+ wallpaperFrame = wallpaperWin.getFrame();
+ cropZoom = 1f;
+ cropOffsetX = 0;
+ cropOffsetY = 0;
+ diffWidth = wallpaperFrame.width() - screenWidth;
+ diffHeight = wallpaperFrame.height() - screenHeight;
+
+ if ((wallpaperWin.mAttrs.flags & WindowManager.LayoutParams.FLAG_SCALED) != 0
+ && Math.abs(diffWidth) > 1 && Math.abs(diffHeight) > 1) {
+ Slog.d(TAG, "Skip wallpaper offset with inconsistent orientation, bounds="
+ + lastWallpaperBounds + " frame=" + wallpaperFrame);
+ // With FLAG_SCALED, the requested size should at least make the frame match one of
+ // side. If both sides contain differences, the client side may not have updated the
+ // latest size according to the current orientation. So skip calculating the offset
+ // to avoid the wallpaper not filling the screen.
+ return false;
+ }
}
- int newXOffset = 0;
- int newYOffset = 0;
boolean rawChanged = false;
// Set the default wallpaper x-offset to either edge of the screen (depending on RTL), to
// match the behavior of most Launchers
@@ -396,17 +469,17 @@ class WallpaperController {
int displayOffset = getDisplayWidthOffset(availw, lastWallpaperBounds,
wallpaperWin.isRtl());
availw -= displayOffset;
- int offset = availw > 0 ? -(int)(availw * wpx + .5f) : 0;
+ offsetX = availw > 0 ? -(int) (availw * wpx + .5f) : 0;
if (token.mWallpaperDisplayOffsetX != Integer.MIN_VALUE) {
// if device is LTR, then offset wallpaper to the left (the wallpaper is drawn
// always starting from the left of the screen).
- offset += token.mWallpaperDisplayOffsetX;
+ offsetX += token.mWallpaperDisplayOffsetX;
} else if (!wallpaperWin.isRtl()) {
// In RTL the offset is calculated so that the wallpaper ends up right aligned (see
// offset above).
- offset -= displayOffset;
+ offsetX -= displayOffset;
}
- newXOffset = offset;
+ offsetX += cropOffsetX * wallpaperWin.mHScale;
if (wallpaperWin.mWallpaperX != wpx || wallpaperWin.mWallpaperXStep != wpxs) {
wallpaperWin.mWallpaperX = wpx;
@@ -416,11 +489,11 @@ class WallpaperController {
float wpy = token.mWallpaperY >= 0 ? token.mWallpaperY : 0.5f;
float wpys = token.mWallpaperYStep >= 0 ? token.mWallpaperYStep : -1.0f;
- offset = diffHeight > 0 ? -(int) (diffHeight * wpy + .5f) : 0;
+ offsetY = diffHeight > 0 ? -(int) (diffHeight * wpy + .5f) : 0;
if (token.mWallpaperDisplayOffsetY != Integer.MIN_VALUE) {
- offset += token.mWallpaperDisplayOffsetY;
+ offsetY += token.mWallpaperDisplayOffsetY;
}
- newYOffset = offset;
+ offsetY += cropOffsetY * wallpaperWin.mVScale;
if (wallpaperWin.mWallpaperY != wpy || wallpaperWin.mWallpaperYStep != wpys) {
wallpaperWin.mWallpaperY = wpy;
@@ -432,10 +505,10 @@ class WallpaperController {
wallpaperWin.mWallpaperZoomOut = mLastWallpaperZoomOut;
rawChanged = true;
}
-
- boolean changed = wallpaperWin.setWallpaperOffset(newXOffset, newYOffset,
- wallpaperWin.mShouldScaleWallpaper
- ? zoomOutToScale(wallpaperWin.mWallpaperZoomOut) : 1);
+ zoom = wallpaperWin.mShouldScaleWallpaper
+ ? zoomOutToScale(wallpaperWin.mWallpaperZoomOut) : 1f;
+ final float totalZoom = zoom * cropZoom;
+ boolean changed = wallpaperWin.setWallpaperOffset(offsetX, offsetY, totalZoom);
if (rawChanged && (wallpaperWin.mAttrs.privateFlags &
WindowManager.LayoutParams.PRIVATE_FLAG_WANTS_OFFSET_NOTIFICATIONS) != 0) {
@@ -496,7 +569,7 @@ class WallpaperController {
* display).
*/
private int getDisplayWidthOffset(int availWidth, Rect displayFrame, boolean isRtl) {
- if (!mShouldOffsetWallpaperCenter) {
+ if (!mShouldOffsetWallpaperCenter || multiCrop()) {
return 0;
}
if (mLargestDisplaySize == null) {
diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
index 15bd6078dc2d..1bcd882b5d64 100644
--- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java
+++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
@@ -25,9 +25,11 @@ import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import android.annotation.Nullable;
+import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
+import android.util.SparseArray;
import android.view.animation.Animation;
import com.android.internal.protolog.common.ProtoLog;
@@ -49,6 +51,12 @@ class WallpaperWindowToken extends WindowToken {
int mWallpaperDisplayOffsetX = Integer.MIN_VALUE;
int mWallpaperDisplayOffsetY = Integer.MIN_VALUE;
+ /**
+ * Map from {@link android.app.WallpaperManager.ScreenOrientation} to crop rectangles.
+ * Crop rectangles represent the part of the wallpaper displayed for each screen orientation.
+ */
+ private SparseArray<Rect> mCropHints = new SparseArray<>();
+
WallpaperWindowToken(WindowManagerService service, IBinder token, boolean explicit,
DisplayContent dc, boolean ownerCanManageAppTokens) {
this(service, token, explicit, dc, ownerCanManageAppTokens, null /* options */);
@@ -98,6 +106,14 @@ class WallpaperWindowToken extends WindowToken {
return mShowWhenLocked;
}
+ void setCropHints(SparseArray<Rect> cropHints) {
+ mCropHints = cropHints.clone();
+ }
+
+ SparseArray<Rect> getCropHints() {
+ return mCropHints;
+ }
+
void sendWindowWallpaperCommand(
String action, int x, int y, int z, Bundle extras, boolean sync) {
for (int wallpaperNdx = mChildren.size() - 1; wallpaperNdx >= 0; wallpaperNdx--) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index f2a58e54bfbe..d0b9a6ec8775 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -34,6 +34,7 @@ import android.os.IBinder;
import android.os.Message;
import android.util.ArraySet;
import android.util.Pair;
+import android.util.SparseArray;
import android.view.ContentRecordingSession;
import android.view.Display;
import android.view.IInputFilter;
@@ -52,6 +53,7 @@ import android.window.ScreenCapture;
import com.android.internal.policy.KeyInterceptionInfo;
import com.android.server.input.InputManagerService;
import com.android.server.policy.WindowManagerPolicy;
+import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils;
import com.android.server.wm.SensitiveContentPackages.PackageInfo;
import java.lang.annotation.Retention;
@@ -699,6 +701,21 @@ public abstract class WindowManagerInternal {
public abstract void setWallpaperShowWhenLocked(IBinder windowToken, boolean showWhenLocked);
/**
+ * Sets the crop hints of a {@link WallpaperWindowToken}. Only effective for image wallpapers.
+ *
+ * @param windowToken wallpaper token previously added via {@link #addWindowToken}
+ * @param cropHints a map that represents which part of the wallpaper should be shown, for
+ * each type of {@link android.app.WallpaperManager.ScreenOrientation}.
+ */
+ public abstract void setWallpaperCropHints(IBinder windowToken, SparseArray<Rect> cropHints);
+
+ /**
+ * Transmits the {@link WallpaperCropUtils} instance to {@link WallpaperController}.
+ * {@link WallpaperCropUtils} contains the helpers to properly position the wallpaper.
+ */
+ public abstract void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils);
+
+ /**
* Returns {@code true} if a Window owned by {@code uid} has focus.
*/
public abstract boolean isUidFocused(int uid);
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 6c833565119f..f8ac8da710c8 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -153,6 +153,7 @@ import static com.android.server.wm.WindowManagerServiceDumpProto.INPUT_METHOD_W
import static com.android.server.wm.WindowManagerServiceDumpProto.POLICY;
import static com.android.server.wm.WindowManagerServiceDumpProto.ROOT_WINDOW_CONTAINER;
import static com.android.server.wm.WindowManagerServiceDumpProto.WINDOW_FRAMES_VALID;
+import static com.android.window.flags.Flags.multiCrop;
import android.Manifest;
import android.Manifest.permission;
@@ -238,6 +239,7 @@ import android.util.EventLog;
import android.util.MergedConfiguration;
import android.util.Pair;
import android.util.Slog;
+import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import android.util.TimeUtils;
@@ -342,6 +344,7 @@ import com.android.server.policy.WindowManagerPolicy;
import com.android.server.policy.WindowManagerPolicy.ScreenOffListener;
import com.android.server.power.ShutdownThread;
import com.android.server.utils.PriorityDump;
+import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils;
import dalvik.annotation.optimization.NeverCompile;
@@ -8128,6 +8131,25 @@ public class WindowManagerService extends IWindowManager.Stub
}
@Override
+ public void setWallpaperCropHints(IBinder binder, SparseArray<Rect> cropHints) {
+ synchronized (mGlobalLock) {
+ final WindowToken token = mRoot.getWindowToken(binder);
+ if (token == null || token.asWallpaperToken() == null) {
+ ProtoLog.w(WM_ERROR,
+ "setWallpaperCropHints: non-existent wallpaper token: %s", binder);
+ return;
+ }
+ token.asWallpaperToken().setCropHints(cropHints);
+ }
+ }
+
+ @Override
+ public void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils) {
+ mRoot.getDisplayContent(DEFAULT_DISPLAY).mWallpaperController
+ .setWallpaperCropUtils(wallpaperCropUtils);
+ }
+
+ @Override
public boolean isUidFocused(int uid) {
synchronized (mGlobalLock) {
for (int i = mRoot.getChildCount() - 1; i >= 0; i--) {
@@ -9366,7 +9388,8 @@ public class WindowManagerService extends IWindowManager.Stub
final long origId = Binder.clearCallingIdentity();
try {
synchronized (mGlobalLock) {
- if (!mAtmService.isCallerRecents(callingUid)) {
+ if (!mAtmService.isCallerRecents(callingUid)
+ && (!multiCrop() || callingUid != SYSTEM_UID)) {
Slog.e(TAG, "Unable to verify uid for getPossibleDisplayInfo"
+ " on uid " + callingUid);
return new ArrayList<>();
diff --git a/services/core/jni/com_android_server_utils_AnrTimer.cpp b/services/core/jni/com_android_server_utils_AnrTimer.cpp
index 1e48aced0041..8ca5333a65bd 100644
--- a/services/core/jni/com_android_server_utils_AnrTimer.cpp
+++ b/services/core/jni/com_android_server_utils_AnrTimer.cpp
@@ -113,8 +113,10 @@ class AnrTimerService {
static const timer_id_t NOTIMER = 0;
// A notifier is called with a timer ID, the timer's tag, and the client's cookie. The pid
- // and uid that were originally assigned to the timer are passed as well.
- using notifier_t = bool (*)(timer_id_t, int pid, int uid, void* cookie, jweak object);
+ // and uid that were originally assigned to the timer are passed as well. The elapsed time
+ // is the time since the timer was scheduled.
+ using notifier_t = bool (*)(timer_id_t, int pid, int uid, nsecs_t elapsed,
+ void* cookie, jweak object);
enum Status {
Invalid,
@@ -278,6 +280,9 @@ class AnrTimerService::Timer {
// The state of this timer.
Status status;
+ // The time at which the timer was started.
+ nsecs_t started;
+
// The scheduled timeout. This is an absolute time. It may be extended.
nsecs_t scheduled;
@@ -297,6 +302,7 @@ class AnrTimerService::Timer {
timeout(0),
extend(false),
status(Invalid),
+ started(0),
scheduled(0),
extended(false) {
}
@@ -310,6 +316,7 @@ class AnrTimerService::Timer {
timeout(0),
extend(false),
status(Invalid),
+ started(0),
scheduled(0),
extended(false) {
}
@@ -322,7 +329,8 @@ class AnrTimerService::Timer {
timeout(timeout),
extend(extend),
status(Running),
- scheduled(now() + timeout),
+ started(now()),
+ scheduled(started + timeout),
extended(false) {
if (extend && pid != 0) {
initial.fill(pid);
@@ -714,6 +722,7 @@ void AnrTimerService::expire(timer_id_t timerId) {
// Save the timer attributes for the notification
int pid = 0;
int uid = 0;
+ nsecs_t elapsed = 0;
bool expired = false;
{
AutoMutex _l(lock_);
@@ -727,11 +736,14 @@ void AnrTimerService::expire(timer_id_t timerId) {
// accept or discard).
insert(t);
}
+ pid = t.pid;
+ uid = t.uid;
+ elapsed = now() - t.started;
}
// Deliver the notification outside of the lock.
if (expired) {
- if (!notifier_(timerId, pid, uid, notifierCookie_, notifierObject_)) {
+ if (!notifier_(timerId, pid, uid, elapsed, notifierCookie_, notifierObject_)) {
AutoMutex _l(lock_);
// Notification failed, which means the listener will never call accept() or
// discard(). Do not reinsert the timer.
@@ -804,7 +816,7 @@ struct AnrArgs {
static AnrArgs gAnrArgs;
// The cookie is the address of the AnrArgs object to which the notification should be sent.
-static bool anrNotify(AnrTimerService::timer_id_t timerId, int pid, int uid,
+static bool anrNotify(AnrTimerService::timer_id_t timerId, int pid, int uid, nsecs_t elapsed,
void* cookie, jweak jtimer) {
AutoMutex _l(gAnrLock);
AnrArgs* target = reinterpret_cast<AnrArgs* >(cookie);
@@ -816,7 +828,8 @@ static bool anrNotify(AnrTimerService::timer_id_t timerId, int pid, int uid,
jboolean r = false;
jobject timer = env->NewGlobalRef(jtimer);
if (timer != nullptr) {
- r = env->CallBooleanMethod(timer, target->func, timerId, pid, uid);
+ // Convert the elsapsed time from ns (native) to ms (Java)
+ r = env->CallBooleanMethod(timer, target->func, timerId, pid, uid, ns2ms(elapsed));
env->DeleteGlobalRef(timer);
}
target->vm->DetachCurrentThread();
@@ -909,7 +922,7 @@ int register_android_server_utils_AnrTimer(JNIEnv* env)
jclass service = FindClassOrDie(env, className);
gAnrArgs.clazz = MakeGlobalRefOrDie(env, service);
- gAnrArgs.func = env->GetMethodID(gAnrArgs.clazz, "expire", "(III)Z");
+ gAnrArgs.func = env->GetMethodID(gAnrArgs.clazz, "expire", "(IIIJ)Z");
env->GetJavaVM(&gAnrArgs.vm);
nativeSupportEnabled = NATIVE_SUPPORT;
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index 0089d4cafaad..5e5181bdfeeb 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -109,6 +109,7 @@
<uses-permission android:name="android.permission.UPDATE_LOCK_TASK_PACKAGES" />
<uses-permission android:name="android.permission.ACCESS_CONTEXT_HUB" />
<uses-permission android:name="android.permission.USE_BIOMETRIC_INTERNAL" />
+ <uses-permission android:name="android.permission.USE_BACKGROUND_FACE_AUTHENTICATION" />
<uses-permission android:name="android.permission.MANAGE_MEDIA_PROJECTION" />
<uses-permission android:name="android.permission.MANAGE_ROLE_HOLDERS" />
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java
index 3aaac2e9cf1b..c8a5583de0b2 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java
@@ -16,6 +16,7 @@
package com.android.server.biometrics.sensors.face;
+import static android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION;
import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL;
import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG;
import static android.hardware.face.FaceSensorProperties.TYPE_UNKNOWN;
@@ -234,6 +235,26 @@ public class FaceServiceTest {
}
@Test
+ public void testAuthenticateInBackground() throws Exception {
+ FaceAuthenticateOptions faceAuthenticateOptions = new FaceAuthenticateOptions.Builder()
+ .build();
+ initService();
+ mFaceService.mServiceWrapper.registerAuthenticators(List.of());
+ waitForRegistration();
+
+ mContext.getTestablePermissions().setPermission(
+ USE_BIOMETRIC_INTERNAL, PackageManager.PERMISSION_DENIED);
+ mContext.getTestablePermissions().setPermission(
+ USE_BACKGROUND_FACE_AUTHENTICATION, PackageManager.PERMISSION_GRANTED);
+
+ final long operationId = 5;
+ mFaceService.mServiceWrapper.authenticateInBackground(mToken, operationId,
+ mFaceServiceReceiver, faceAuthenticateOptions);
+
+ assertThat(faceAuthenticateOptions.getSensorId()).isEqualTo(ID_DEFAULT);
+ }
+
+ @Test
public void testOptionsForDetect() throws Exception {
FaceAuthenticateOptions faceAuthenticateOptions = new FaceAuthenticateOptions.Builder()
.setOpPackageName(ComponentName.unflattenFromString(OP_PACKAGE_NAME)
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index d71844b00b3b..9ff29d208dc0 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -829,6 +829,40 @@ public class VirtualDeviceManagerServiceTest {
}
@Test
+ public void getDisplayNameForPersistentDeviceId_nonExistentPeristentId_returnsNull() {
+ assertThat(mVdm.getDisplayNameForPersistentDeviceId("nonExistentPersistentId")).isNull();
+ }
+
+ @Test
+ public void getDisplayNameForPersistentDeviceId_defaultDevicePeristentId_returnsNull() {
+ assertThat(mVdm.getDisplayNameForPersistentDeviceId(
+ VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT))
+ .isNull();
+ }
+
+ @Test
+ public void getDisplayNameForPersistentDeviceId_validVirtualDevice_returnsCorrectId() {
+ mVdms.onCdmAssociationsChanged(List.of(mAssociationInfo));
+ CharSequence persistentIdDisplayName =
+ mVdm.getDisplayNameForPersistentDeviceId(mDeviceImpl.getPersistentDeviceId());
+ assertThat(persistentIdDisplayName.toString())
+ .isEqualTo(mAssociationInfo.getDisplayName().toString());
+ }
+
+ @Test
+ public void getDisplayNameForPersistentDeviceId_noVirtualDevice_returnsCorrectId() {
+ CharSequence displayName = "New display name for the new association";
+ mVdms.onCdmAssociationsChanged(List.of(
+ createAssociationInfo(2, AssociationRequest.DEVICE_PROFILE_APP_STREAMING,
+ displayName)));
+
+ CharSequence persistentIdDisplayName =
+ mVdm.getDisplayNameForPersistentDeviceId(
+ VirtualDeviceImpl.createPersistentDeviceId(2));
+ assertThat(persistentIdDisplayName.toString()).isEqualTo(displayName.toString());
+ }
+
+ @Test
public void onAppsOnVirtualDeviceChanged_singleVirtualDevice_listenersNotified() {
ArraySet<Integer> uids = new ArraySet<>(Arrays.asList(UID_1, UID_2));
mLocalService.registerAppsOnVirtualDeviceListener(mAppsOnVirtualDeviceListener);
@@ -1994,8 +2028,14 @@ public class VirtualDeviceManagerServiceTest {
}
private AssociationInfo createAssociationInfo(int associationId, String deviceProfile) {
+ return createAssociationInfo(
+ associationId, deviceProfile, /* displayName= */ deviceProfile);
+ }
+
+ private AssociationInfo createAssociationInfo(int associationId, String deviceProfile,
+ CharSequence displayName) {
return new AssociationInfo(associationId, /* userId= */ 0, /* packageName=*/ null,
- /* tag= */ null, MacAddress.BROADCAST_ADDRESS, /* displayName= */ "", deviceProfile,
+ /* tag= */ null, MacAddress.BROADCAST_ADDRESS, displayName, deviceProfile,
/* associatedDevice= */ null, /* selfManaged= */ true,
/* notifyOnDeviceNearby= */ false, /* revoked= */ false, /* pending= */ false,
/* timeApprovedMs= */0, /* lastTimeConnectedMs= */0, /* systemDataSyncFlags= */ -1);
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 723ac15fb50f..9c2cba8ecf96 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -224,6 +224,7 @@ import android.os.UserHandle;
import android.os.UserManager;
import android.os.WorkSource;
import android.permission.PermissionManager;
+import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.platform.test.rule.DeniedDevices;
@@ -13978,6 +13979,58 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
@Test
@EnableFlags(android.app.Flags.FLAG_MODES_API)
+ public void requestInterruptionFilterFromListener_fromApp_doesNotSetGlobalZen()
+ throws Exception {
+ mService.setCallerIsNormalPackage();
+ mService.mZenModeHelper = mock(ZenModeHelper.class);
+ ManagedServices.ManagedServiceInfo info = mock(ManagedServices.ManagedServiceInfo.class);
+ when(mListeners.checkServiceTokenLocked(any())).thenReturn(info);
+ info.component = new ComponentName("pkg", "cls");
+
+ mBinderService.requestInterruptionFilterFromListener(mock(INotificationListener.class),
+ INTERRUPTION_FILTER_PRIORITY);
+
+ verify(mService.mZenModeHelper).applyGlobalZenModeAsImplicitZenRule(eq("pkg"), eq(mUid),
+ eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS));
+ }
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_MODES_API)
+ public void requestInterruptionFilterFromListener_fromSystem_setsGlobalZen()
+ throws Exception {
+ mService.isSystemUid = true;
+ mService.mZenModeHelper = mock(ZenModeHelper.class);
+ ManagedServices.ManagedServiceInfo info = mock(ManagedServices.ManagedServiceInfo.class);
+ when(mListeners.checkServiceTokenLocked(any())).thenReturn(info);
+ info.component = new ComponentName("pkg", "cls");
+
+ mBinderService.requestInterruptionFilterFromListener(mock(INotificationListener.class),
+ INTERRUPTION_FILTER_PRIORITY);
+
+ verify(mService.mZenModeHelper).setManualZenMode(eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS),
+ eq(null), eq(ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI), anyString(),
+ eq("pkg"), eq(mUid));
+ }
+
+ @Test
+ @DisableFlags(android.app.Flags.FLAG_MODES_API)
+ public void requestInterruptionFilterFromListener_flagOff_callsRequestFromListener()
+ throws Exception {
+ mService.setCallerIsNormalPackage();
+ mService.mZenModeHelper = mock(ZenModeHelper.class);
+ ManagedServices.ManagedServiceInfo info = mock(ManagedServices.ManagedServiceInfo.class);
+ when(mListeners.checkServiceTokenLocked(any())).thenReturn(info);
+ info.component = new ComponentName("pkg", "cls");
+
+ mBinderService.requestInterruptionFilterFromListener(mock(INotificationListener.class),
+ INTERRUPTION_FILTER_PRIORITY);
+
+ verify(mService.mZenModeHelper).requestFromListener(eq(info.component),
+ eq(INTERRUPTION_FILTER_PRIORITY), eq(mUid), /* fromSystemOrSystemUi= */ eq(false));
+ }
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_MODES_API)
@EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
public void updateAutomaticZenRule_implicitRuleWithoutCPS_disallowedFromApp() throws Exception {
setUpRealZenTest();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index edc876aab388..248683836336 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -1185,6 +1185,28 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
+ @EnableFlags(Flags.FLAG_MODES_API)
+ public void testProtoWithAutoRuleWithModifiedFields() throws Exception {
+ setupZenConfig();
+ mZenModeHelper.mConfig.automaticRules = new ArrayMap<>();
+ ZenRule rule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS, CUSTOM_RULE_ID);
+ rule.userModifiedFields = AutomaticZenRule.FIELD_NAME;
+ rule.zenPolicyUserModifiedFields = ZenPolicy.FIELD_PRIORITY_CATEGORY_MEDIA;
+ rule.zenDeviceEffectsUserModifiedFields = ZenDeviceEffects.FIELD_GRAYSCALE;
+ mZenModeHelper.mConfig.automaticRules.put(rule.id, rule);
+
+ List<StatsEvent> events = new ArrayList<>();
+ mZenModeHelper.pullRules(events);
+
+ assertThat(events).hasSize(2); // Global config + 1 automatic rule
+ DNDModeProto ruleProto = StatsEventTestUtils.convertToAtom(events.get(1)).getDndModeRule();
+ assertThat(ruleProto.getRuleModifiedFields()).isEqualTo(rule.userModifiedFields);
+ assertThat(ruleProto.getPolicyModifiedFields()).isEqualTo(rule.zenPolicyUserModifiedFields);
+ assertThat(ruleProto.getDeviceEffectsModifiedFields()).isEqualTo(
+ rule.zenDeviceEffectsUserModifiedFields);
+ }
+
+ @Test
public void ruleUidsCached() throws Exception {
setupZenConfig();
// one enabled automatic rule
diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
index 6216acbfe465..73d386a328f5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
@@ -32,6 +32,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+import static com.android.window.flags.Flags.multiCrop;
import static com.google.common.truth.Truth.assertThat;
@@ -39,6 +40,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -484,6 +486,7 @@ public class WallpaperControllerTests extends WindowTestsBase {
@Test
public void testUpdateWallpaperOffset_resize_shouldCenterEnabled() {
+ assumeFalse(multiCrop());
final DisplayContent dc = new TestDisplayContent.Builder(mAtm, INITIAL_WIDTH,
INITIAL_HEIGHT).build();
dc.mWallpaperController.setShouldOffsetWallpaperCenter(true);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 7ae5a1156d07..114b9c3a68f2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -119,6 +119,7 @@ import android.window.TransitionRequestInfo;
import com.android.internal.policy.AttributeCache;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils;
import com.android.server.wm.DisplayWindowSettings.SettingsProvider.SettingsEntry;
import org.junit.After;
@@ -286,6 +287,18 @@ class WindowTestsBase extends SystemServiceTestsBase {
mAtm.mWindowManager.mLetterboxConfiguration
.setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(false);
+ // Setup WallpaperController crop utils with a simple center-align strategy
+ WallpaperCropUtils cropUtils = (displaySize, bitmapSize, suggestedCrops, rtl) -> {
+ Rect crop = new Rect(0, 0, displaySize.x, displaySize.y);
+ crop.scale(Math.min(
+ ((float) bitmapSize.x) / displaySize.x,
+ ((float) bitmapSize.y) / displaySize.y));
+ crop.offset((bitmapSize.x - crop.width()) / 2, (bitmapSize.y - crop.height()) / 2);
+ return crop;
+ };
+ mDisplayContent.mWallpaperController.setWallpaperCropUtils(cropUtils);
+ mDefaultDisplay.mWallpaperController.setWallpaperCropUtils(cropUtils);
+
checkDeviceSpecificOverridesNotApplied();
}
diff --git a/telecomm/java/android/telecom/Response.java b/telecomm/java/android/telecom/Response.java
deleted file mode 100644
index ce7a7612786b..000000000000
--- a/telecomm/java/android/telecom/Response.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2014 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.telecom;
-
-/**
- * @hide
- */
-public interface Response<IN, OUT> {
-
- /**
- * Provide a set of results.
- *
- * @param request The original request.
- * @param result The results.
- */
- void onResult(IN request, OUT... result);
-
- /**
- * Indicates the inability to provide results.
- *
- * @param request The original request.
- * @param code An integer code indicating the reason for failure.
- * @param msg A message explaining the reason for failure.
- */
- void onError(IN request, int code, String msg);
-}
diff --git a/telephony/java/android/telephony/CarrierInfo.java b/telephony/java/android/telephony/CarrierInfo.java
new file mode 100644
index 000000000000..da77a45b998f
--- /dev/null
+++ b/telephony/java/android/telephony/CarrierInfo.java
@@ -0,0 +1,236 @@
+/*
+ * 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.telephony;
+
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.telephony.Rlog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * CarrierInfo that is used to represent the carrier lock information details.
+ *
+ * @hide
+ */
+public final class CarrierInfo implements Parcelable {
+
+ /**
+ * Used to create a {@link CarrierInfo} from a {@link Parcel}.
+ *
+ * @hide
+ */
+ public static final @android.annotation.NonNull Creator<CarrierInfo> CREATOR =
+ new Creator<CarrierInfo>() {
+ /**
+ * Create a new instance of the Parcelable class, instantiating it
+ * from the given Parcel whose data had previously been written by
+ * {@link Parcelable#writeToParcel Parcelable.writeToParcel()}.
+ *
+ * @param source The Parcel to read the object's data from.
+ * @return Returns a new instance of the Parcelable class.
+ */
+ @Override
+ public CarrierInfo createFromParcel(Parcel source) {
+ return new CarrierInfo(source);
+ }
+
+ /**
+ * Create a new array of the Parcelable class.
+ *
+ * @param size Size of the array.
+ * @return Returns an array of the Parcelable class, with every entry
+ * initialized to null.
+ */
+ @Override
+ public CarrierInfo[] newArray(int size) {
+ return new CarrierInfo[size];
+ }
+
+ };
+ @NonNull
+ private String mMcc;
+ @NonNull
+ private String mMnc;
+ @Nullable
+ private String mSpn;
+ @Nullable
+ private String mGid1;
+ @Nullable
+ private String mGid2;
+ @Nullable
+ private String mImsiPrefix;
+ /** Ehplmn is String combination of MCC,MNC */
+ @Nullable
+ private List<String> mEhplmn;
+ @Nullable
+ private String mIccid;
+ @Nullable
+ private String mImpi;
+
+ /** @hide */
+ @NonNull
+ public String getMcc() {
+ return mMcc;
+ }
+
+ /** @hide */
+ @NonNull
+ public String getMnc() {
+ return mMnc;
+ }
+
+ /** @hide */
+ @Nullable
+ public String getSpn() {
+ return mSpn;
+ }
+
+ /** @hide */
+ @Nullable
+ public String getGid1() {
+ return mGid1;
+ }
+
+ /** @hide */
+ @Nullable
+ public String getGid2() {
+ return mGid2;
+ }
+
+ /** @hide */
+ @Nullable
+ public String getImsiPrefix() {
+ return mImsiPrefix;
+ }
+
+ /** @hide */
+ @Nullable
+ public String getIccid() {
+ return mIccid;
+ }
+
+ /** @hide */
+ @Nullable
+ public String getImpi() {
+ return mImpi;
+ }
+
+ /**
+ * Returns the list of EHPLMN.
+ *
+ * @return List of String that represent Ehplmn.
+ * @hide
+ */
+ @NonNull
+ public List<String> getEhplmn() {
+ return mEhplmn;
+ }
+
+ /** @hide */
+ public CarrierInfo(@NonNull String mcc, @NonNull String mnc, @Nullable String spn,
+ @Nullable String gid1, @Nullable String gid2, @Nullable String imsi,
+ @Nullable String iccid, @Nullable String impi, @Nullable List<String> plmnArrayList) {
+ mMcc = mcc;
+ mMnc = mnc;
+ mSpn = spn;
+ mGid1 = gid1;
+ mGid2 = gid2;
+ mImsiPrefix = imsi;
+ mIccid = iccid;
+ mImpi = impi;
+ mEhplmn = plmnArrayList;
+ }
+
+ /**
+ * Describe the kinds of special objects contained in this Parcelable
+ * instance's marshaled representation. For example, if the object will
+ * include a file descriptor in the output of {@link #writeToParcel(Parcel, int)},
+ * the return value of this method must include the
+ * {@link #CONTENTS_FILE_DESCRIPTOR} bit.
+ *
+ * @return a bitmask indicating the set of special object types marshaled
+ * by this Parcelable object instance.
+ * @hide
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Flatten this object in to a Parcel.
+ *
+ * @param dest The Parcel in which the object should be written.
+ * @param flags Additional flags about how the object should be written.
+ * May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}.
+ * @hide
+ */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeString8(mMcc);
+ dest.writeString8(mMnc);
+ dest.writeString8(mSpn);
+ dest.writeString8(mGid1);
+ dest.writeString8(mGid2);
+ dest.writeString8(mImsiPrefix);
+ dest.writeString8(mIccid);
+ dest.writeString8(mImpi);
+ dest.writeStringList(mEhplmn);
+ }
+
+ /** @hide */
+ public CarrierInfo(Parcel in) {
+ mEhplmn = new ArrayList<String>();
+ mMcc = in.readString8();
+ mMnc = in.readString8();
+ mSpn = in.readString8();
+ mGid1 = in.readString8();
+ mGid2 = in.readString8();
+ mImsiPrefix = in.readString8();
+ mIccid = in.readString8();
+ mImpi = in.readString8();
+ in.readStringList(mEhplmn);
+ }
+
+
+ /** @hide */
+ @android.annotation.NonNull
+ @Override
+ public String toString() {
+ return "CarrierInfo MCC = " + mMcc + " MNC = " + mMnc + " SPN = " + mSpn + " GID1 = "
+ + mGid1 + " GID2 = " + mGid2 + " IMSI = " + getPrintableImsi() + " ICCID = "
+ + SubscriptionInfo.getPrintableId(mIccid) + " IMPI = " + mImpi + " EHPLMN = [ "
+ + getEhplmn_toString() + " ]";
+ }
+
+ private String getEhplmn_toString() {
+ return String.join(" ", mEhplmn);
+ }
+
+ private String getPrintableImsi() {
+ boolean enablePiiLog = Rlog.isLoggable("CarrierInfo", Log.VERBOSE);
+ return ((mImsiPrefix != null && mImsiPrefix.length() > 6) ? mImsiPrefix.substring(0, 6)
+ + Rlog.pii(enablePiiLog, mImsiPrefix.substring(6)) : mImsiPrefix);
+ }
+}
diff --git a/telephony/java/android/telephony/CarrierRestrictionRules.java b/telephony/java/android/telephony/CarrierRestrictionRules.java
index cc768bc00250..2b0d6261886f 100644
--- a/telephony/java/android/telephony/CarrierRestrictionRules.java
+++ b/telephony/java/android/telephony/CarrierRestrictionRules.java
@@ -84,13 +84,75 @@ public final class CarrierRestrictionRules implements Parcelable {
/** The same configuration is applied to all SIM slots independently. */
public static final int MULTISIM_POLICY_NONE = 0;
- /** Any SIM card can be used as far as one SIM card matching the configuration is present. */
+ /**
+ * Indicates that any SIM card can be used as far as one valid card is present in the device.
+ * For the modem, a SIM card is valid when its content (i.e. MCC, MNC, GID, SPN) matches the
+ * carrier restriction configuration.
+ */
public static final int MULTISIM_POLICY_ONE_VALID_SIM_MUST_BE_PRESENT = 1;
+ /**
+ * Indicates that the SIM lock policy applies uniformly to all sim slots.
+ * @hide
+ */
+ public static final int MULTISIM_POLICY_APPLY_TO_ALL_SLOTS = 2;
+
+ /**
+ * The SIM lock configuration applies exclusively to sim slot 1, leaving
+ * all other sim slots unlocked irrespective of the SIM card in slot 1
+ * @hide
+ */
+ public static final int MULTISIM_POLICY_APPLY_TO_ONLY_SLOT_1 = 3;
+
+ /**
+ * Valid sim cards must be present on sim slot1 in order
+ * to use other sim slots.
+ * @hide
+ */
+ public static final int MULTISIM_POLICY_VALID_SIM_MUST_PRESENT_ON_SLOT_1 = 4;
+
+ /**
+ * Valid sim card must be present on slot1 and it must be in full service
+ * in order to use other sim slots.
+ * @hide
+ */
+ public static final int MULTISIM_POLICY_ACTIVE_SERVICE_ON_SLOT_1_TO_UNBLOCK_OTHER_SLOTS = 5;
+
+ /**
+ * Valid sim card be present on any slot and it must be in full service
+ * in order to use other sim slots.
+ * @hide
+ */
+ public static final int MULTISIM_POLICY_ACTIVE_SERVICE_ON_ANY_SLOT_TO_UNBLOCK_OTHER_SLOTS = 6;
+
+ /**
+ * Valid sim cards must be present on all slots. If any SIM cards become
+ * invalid then device would set other SIM cards as invalid as well.
+ * @hide
+ */
+ public static final int MULTISIM_POLICY_ALL_SIMS_MUST_BE_VALID = 7;
+
+ /**
+ * In case there is no match policy listed above.
+ * @hide
+ */
+ public static final int MULTISIM_POLICY_SLOT_POLICY_OTHER = 8;
+
+
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = "MULTISIM_POLICY_",
- value = {MULTISIM_POLICY_NONE, MULTISIM_POLICY_ONE_VALID_SIM_MUST_BE_PRESENT})
+ value = {MULTISIM_POLICY_NONE,
+ MULTISIM_POLICY_ONE_VALID_SIM_MUST_BE_PRESENT,
+ MULTISIM_POLICY_APPLY_TO_ALL_SLOTS,
+ MULTISIM_POLICY_APPLY_TO_ONLY_SLOT_1,
+ MULTISIM_POLICY_VALID_SIM_MUST_PRESENT_ON_SLOT_1,
+ MULTISIM_POLICY_ACTIVE_SERVICE_ON_SLOT_1_TO_UNBLOCK_OTHER_SLOTS,
+ MULTISIM_POLICY_ACTIVE_SERVICE_ON_ANY_SLOT_TO_UNBLOCK_OTHER_SLOTS,
+ MULTISIM_POLICY_ALL_SIMS_MUST_BE_VALID,
+ MULTISIM_POLICY_SLOT_POLICY_OTHER
+ })
public @interface MultiSimPolicy {}
/** @hide */
@@ -104,6 +166,8 @@ public final class CarrierRestrictionRules implements Parcelable {
private List<CarrierIdentifier> mAllowedCarriers;
private List<CarrierIdentifier> mExcludedCarriers;
+ private List<CarrierInfo> mAllowedCarrierInfo;
+ private List<CarrierInfo> mExcludedCarrierInfo;
@CarrierRestrictionDefault
private int mCarrierRestrictionDefault;
@MultiSimPolicy
@@ -114,6 +178,8 @@ public final class CarrierRestrictionRules implements Parcelable {
private CarrierRestrictionRules() {
mAllowedCarriers = new ArrayList<CarrierIdentifier>();
mExcludedCarriers = new ArrayList<CarrierIdentifier>();
+ mAllowedCarrierInfo = new ArrayList<CarrierInfo>();
+ mExcludedCarrierInfo = new ArrayList<CarrierInfo>();
mCarrierRestrictionDefault = CARRIER_RESTRICTION_DEFAULT_NOT_ALLOWED;
mMultiSimPolicy = MULTISIM_POLICY_NONE;
mCarrierRestrictionStatus = TelephonyManager.CARRIER_RESTRICTION_STATUS_UNKNOWN;
@@ -122,12 +188,17 @@ public final class CarrierRestrictionRules implements Parcelable {
private CarrierRestrictionRules(Parcel in) {
mAllowedCarriers = new ArrayList<CarrierIdentifier>();
mExcludedCarriers = new ArrayList<CarrierIdentifier>();
-
+ mAllowedCarrierInfo = new ArrayList<CarrierInfo>();
+ mExcludedCarrierInfo = new ArrayList<CarrierInfo>();
in.readTypedList(mAllowedCarriers, CarrierIdentifier.CREATOR);
in.readTypedList(mExcludedCarriers, CarrierIdentifier.CREATOR);
mCarrierRestrictionDefault = in.readInt();
mMultiSimPolicy = in.readInt();
mCarrierRestrictionStatus = in.readInt();
+ if (Flags.carrierRestrictionRulesEnhancement()) {
+ in.readTypedList(mAllowedCarrierInfo, CarrierInfo.CREATOR);
+ in.readTypedList(mExcludedCarrierInfo, CarrierInfo.CREATOR);
+ }
}
/**
@@ -165,6 +236,25 @@ public final class CarrierRestrictionRules implements Parcelable {
}
/**
+ * Retrieves list of excluded carrierInfos
+ *
+ * @return the list of excluded carrierInfos
+ * @hide
+ */
+ public @NonNull List<CarrierInfo> getExcludedCarriersInfoList() {
+ return mExcludedCarrierInfo;
+ }
+
+ /**
+ * Retrieves list of excluded carrierInfos
+ *
+ * @return the list of excluded carrierInfos
+ * @hide
+ */
+ public @NonNull List<CarrierInfo> getAllowedCarriersInfoList() {
+ return mAllowedCarrierInfo;
+ }
+ /**
* Retrieves the default behavior of carrier restrictions
*/
public @CarrierRestrictionDefault int getDefaultCarrierRestriction() {
@@ -326,6 +416,10 @@ public final class CarrierRestrictionRules implements Parcelable {
out.writeInt(mCarrierRestrictionDefault);
out.writeInt(mMultiSimPolicy);
out.writeInt(mCarrierRestrictionStatus);
+ if (Flags.carrierRestrictionRulesEnhancement()) {
+ out.writeTypedList(mAllowedCarrierInfo);
+ out.writeTypedList(mExcludedCarrierInfo);
+ }
}
/**
@@ -357,7 +451,16 @@ public final class CarrierRestrictionRules implements Parcelable {
public String toString() {
return "CarrierRestrictionRules(allowed:" + mAllowedCarriers + ", excluded:"
+ mExcludedCarriers + ", default:" + mCarrierRestrictionDefault
- + ", multisim policy:" + mMultiSimPolicy + ")";
+ + ", multisim policy:" + mMultiSimPolicy + getCarrierInfoList() + ")";
+ }
+
+ private String getCarrierInfoList() {
+ if (Flags.carrierRestrictionRulesEnhancement()) {
+ return ", allowedCarrierInfoList:" + mAllowedCarrierInfo
+ + ", excludedCarrierInfoList:" + mExcludedCarrierInfo;
+ } else {
+ return "";
+ }
}
/**
@@ -382,6 +485,12 @@ public final class CarrierRestrictionRules implements Parcelable {
mRules.mAllowedCarriers.clear();
mRules.mExcludedCarriers.clear();
mRules.mCarrierRestrictionDefault = CARRIER_RESTRICTION_DEFAULT_ALLOWED;
+ if (Flags.carrierRestrictionRulesEnhancement()) {
+ mRules.mCarrierRestrictionStatus =
+ TelephonyManager.CARRIER_RESTRICTION_STATUS_NOT_RESTRICTED;
+ mRules.mAllowedCarrierInfo.clear();
+ mRules.mExcludedCarrierInfo.clear();
+ }
return this;
}
@@ -439,5 +548,29 @@ public final class CarrierRestrictionRules implements Parcelable {
mRules.mCarrierRestrictionStatus = carrierRestrictionStatus;
return this;
}
+
+ /**
+ * Set list of allowed carrierInfo
+ *
+ * @param allowedCarrierInfo list of allowed CarrierInfo
+ * @hide
+ */
+ public @NonNull Builder setAllowedCarrierInfo(
+ @NonNull List<CarrierInfo> allowedCarrierInfo) {
+ mRules.mAllowedCarrierInfo = new ArrayList<CarrierInfo>(allowedCarrierInfo);
+ return this;
+ }
+
+ /**
+ * Set list of allowed carrierInfo
+ *
+ * @param excludedCarrierInfo list of allowed CarrierInfo
+ * @hide
+ */
+ public @NonNull Builder setExcludedCarrierInfo(
+ @NonNull List<CarrierInfo> excludedCarrierInfo) {
+ mRules.mExcludedCarrierInfo = new ArrayList<CarrierInfo>(excludedCarrierInfo);
+ return this;
+ }
}
}
diff --git a/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt b/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt
index b0ca4d230e12..79d3a10a34cb 100644
--- a/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt
+++ b/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt
@@ -17,7 +17,10 @@
package com.android.server.wm.flicker.rotation
import android.platform.test.annotations.Presubmit
+import android.tools.common.flicker.subject.layers.LayerTraceEntrySubject
import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.common.traces.component.IComponentMatcher
+import android.tools.common.traces.surfaceflinger.Display
import android.tools.device.apphelpers.StandardAppHelper
import android.tools.device.flicker.legacy.FlickerBuilder
import android.tools.device.flicker.legacy.LegacyFlickerTest
@@ -57,9 +60,8 @@ abstract class RotationTransition(flicker: LegacyFlickerTest) : BaseTest(flicker
@Test
open fun appLayerRotates_StartingPos() {
flicker.assertLayersStart {
- this.entry.displays.map { display ->
- this.visibleRegion(testApp).coversExactly(display.layerStackSpace)
- }
+ val display = getDisplay(testApp)
+ this.visibleRegion(testApp).coversAtLeast(display.layerStackSpace)
}
}
@@ -68,12 +70,20 @@ abstract class RotationTransition(flicker: LegacyFlickerTest) : BaseTest(flicker
@Test
open fun appLayerRotates_EndingPos() {
flicker.assertLayersEnd {
- this.entry.displays.map { display ->
- this.visibleRegion(testApp).coversExactly(display.layerStackSpace)
- }
+ val display = getDisplay(testApp)
+ this.visibleRegion(testApp).coversAtLeast(display.layerStackSpace)
}
}
+ private fun LayerTraceEntrySubject.getDisplay(componentMatcher: IComponentMatcher): Display {
+ val stackId = this.layer {
+ componentMatcher.layerMatchesAnyOf(it) && it.isVisible
+ }?.layer?.stackId ?: -1
+
+ return this.entry.displays.firstOrNull { it.layerStackId == stackId }
+ ?: error("Unable to find visible layer for $componentMatcher")
+ }
+
override fun cujCompleted() {
super.cujCompleted()
appLayerRotates_StartingPos()