diff options
662 files changed, 15452 insertions, 3700 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index c231b30503fa..9e308435c9bc 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -180,6 +180,12 @@ aconfig_declarations { ], } +cc_aconfig_library { + name: "android_location_flags_c_lib", + vendor_available: true, + aconfig_declarations: "android.location.flags-aconfig", +} + java_aconfig_library { name: "android.location.flags-aconfig-java", aconfig_declarations: "android.location.flags-aconfig", @@ -249,6 +255,13 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +java_aconfig_library { + name: "android.app.usage.flags-aconfig-java-host", + aconfig_declarations: "android.app.usage.flags-aconfig", + host_supported: true, + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + // OS aconfig_declarations { name: "android.os.flags-aconfig", diff --git a/Android.bp b/Android.bp index 13b170353dd6..b139b7e50676 100644 --- a/Android.bp +++ b/Android.bp @@ -132,6 +132,7 @@ filegroup { ":libcamera_client_aidl", ":libcamera_client_framework_aidl", ":libupdate_engine_aidl", + ":libupdate_engine_stable-V2-java-source", ":logd_aidl", ":resourcemanager_aidl", ":storaged_aidl", @@ -220,6 +221,7 @@ java_library { "updatable-driver-protos", "ota_metadata_proto_java", "android.hidl.base-V1.0-java", + "android.hidl.manager-V1.2-java", "android.hardware.cas-V1-java", // AIDL "android.hardware.cas-V1.0-java", "android.hardware.cas-V1.1-java", diff --git a/TEST_MAPPING b/TEST_MAPPING index 117faa203325..d59775f4060b 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -132,6 +132,9 @@ }, { "name": "vts_treble_vintf_vendor_test" + }, + { + "name": "CtsStrictJavaPackagesTestCases" } ], "postsubmit-ravenwood": [ diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp index 28b2d4b5e3ee..50c9fd3ec499 100644 --- a/api/StubLibraries.bp +++ b/api/StubLibraries.bp @@ -900,6 +900,15 @@ droidstubs { ], api_levels_sdk_type: "system", extensions_info_file: ":sdk-extensions-info", + dists: [ + // Make the api-versions.xml file for the system API available in the + // sdk build target. + { + targets: ["sdk"], + dest: "api-versions_system.xml", + tag: ".api_versions.xml", + }, + ], } // This module can be built with: diff --git a/core/api/current.txt b/core/api/current.txt index 7ab234ab3e40..c7b921c8f6d5 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -4369,7 +4369,7 @@ package android.app { method public final android.media.session.MediaController getMediaController(); method @NonNull public android.view.MenuInflater getMenuInflater(); method @NonNull public android.window.OnBackInvokedDispatcher getOnBackInvokedDispatcher(); - method public final android.app.Activity getParent(); + method @Deprecated public final android.app.Activity getParent(); method @Nullable public android.content.Intent getParentActivityIntent(); method public android.content.SharedPreferences getPreferences(int); method @Nullable public android.net.Uri getReferrer(); @@ -4387,7 +4387,7 @@ package android.app { method public void invalidateOptionsMenu(); method public boolean isActivityTransitionRunning(); method public boolean isChangingConfigurations(); - method public final boolean isChild(); + method @Deprecated public final boolean isChild(); method public boolean isDestroyed(); method public boolean isFinishing(); method public boolean isImmersive(); @@ -12368,6 +12368,7 @@ package android.content.pm { public final class ModuleInfo implements android.os.Parcelable { method public int describeContents(); + method @FlaggedApi("android.content.pm.provide_info_of_apk_in_apex") @NonNull public java.util.Collection<java.lang.String> getApkInApexPackageNames(); method @Nullable public CharSequence getName(); method @Nullable public String getPackageName(); method public boolean isHidden(); @@ -12378,6 +12379,7 @@ package android.content.pm { public class PackageInfo implements android.os.Parcelable { ctor public PackageInfo(); method public int describeContents(); + method @FlaggedApi("android.content.pm.provide_info_of_apk_in_apex") @Nullable public String getApexPackageName(); method @FlaggedApi("android.content.pm.archiving") public long getArchiveTimeMillis(); method public long getLongVersionCode(); method public void setLongVersionCode(long); @@ -22100,7 +22102,7 @@ package android.media { } @FlaggedApi("android.media.audio.loudness_configurator_api") public class LoudnessCodecConfigurator { - method @FlaggedApi("android.media.audio.loudness_configurator_api") public void addMediaCodec(@NonNull android.media.MediaCodec); + method @FlaggedApi("android.media.audio.loudness_configurator_api") public boolean addMediaCodec(@NonNull android.media.MediaCodec); method @FlaggedApi("android.media.audio.loudness_configurator_api") @NonNull public static android.media.LoudnessCodecConfigurator create(); method @FlaggedApi("android.media.audio.loudness_configurator_api") @NonNull public static android.media.LoudnessCodecConfigurator create(@NonNull java.util.concurrent.Executor, @NonNull android.media.LoudnessCodecConfigurator.OnLoudnessCodecUpdateListener); method @FlaggedApi("android.media.audio.loudness_configurator_api") @NonNull public android.os.Bundle getLoudnessCodecParams(@NonNull android.media.AudioTrack, @NonNull android.media.MediaCodec); @@ -28823,6 +28825,8 @@ package android.nfc { method public boolean isSecureNfcEnabled(); method public boolean isSecureNfcSupported(); method @FlaggedApi("android.nfc.enable_nfc_charging") public boolean isWlcEnabled(); + method @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public void resetDiscoveryTechnology(@NonNull android.app.Activity); + method @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public void setDiscoveryTechnology(@NonNull android.app.Activity, int, int); field public static final String ACTION_ADAPTER_STATE_CHANGED = "android.nfc.action.ADAPTER_STATE_CHANGED"; field public static final String ACTION_NDEF_DISCOVERED = "android.nfc.action.NDEF_DISCOVERED"; field @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public static final String ACTION_PREFERRED_PAYMENT_CHANGED = "android.nfc.action.PREFERRED_PAYMENT_CHANGED"; @@ -28838,6 +28842,13 @@ package android.nfc { field public static final String EXTRA_READER_PRESENCE_CHECK_DELAY = "presence"; field public static final String EXTRA_SECURE_ELEMENT_NAME = "android.nfc.extra.SECURE_ELEMENT_NAME"; field public static final String EXTRA_TAG = "android.nfc.extra.TAG"; + field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_LISTEN_DISABLE = 0; // 0x0 + field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_LISTEN_KEEP = -1; // 0xffffffff + field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_LISTEN_NFC_PASSIVE_A = 1; // 0x1 + field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_LISTEN_NFC_PASSIVE_B = 2; // 0x2 + field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_LISTEN_NFC_PASSIVE_F = 4; // 0x4 + field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_READER_DISABLE = 0; // 0x0 + field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_READER_KEEP = -1; // 0xffffffff field public static final int FLAG_READER_NFC_A = 1; // 0x1 field public static final int FLAG_READER_NFC_B = 2; // 0x2 field public static final int FLAG_READER_NFC_BARCODE = 16; // 0x10 @@ -53956,8 +53967,10 @@ package android.view { field public static final String PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH = "android.window.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH"; field public static final String PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE = "android.window.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE"; field public static final String PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE"; + field @FlaggedApi("com.android.window.flags.app_compat_properties_api") public static final String PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED = "android.window.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED"; field @FlaggedApi("com.android.window.flags.app_compat_properties_api") public static final String PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE"; field public static final String PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE"; + field @FlaggedApi("com.android.window.flags.app_compat_properties_api") public static final String PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES = "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES"; field public static final String PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_BOUNDS_APIS = "android.window.PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_BOUNDS_APIS"; field public static final String PROPERTY_COMPAT_ENABLE_FAKE_FOCUS = "android.window.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS"; field public static final String PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION = "android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION"; diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 0d4169fb9121..494d48899c80 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -95,7 +95,7 @@ package android { field public static final String BYPASS_ROLE_QUALIFICATION = "android.permission.BYPASS_ROLE_QUALIFICATION"; field public static final String CALL_AUDIO_INTERCEPTION = "android.permission.CALL_AUDIO_INTERCEPTION"; field public static final String CAMERA_DISABLE_TRANSMIT_LED = "android.permission.CAMERA_DISABLE_TRANSMIT_LED"; - field public static final String CAMERA_HEADLESS_SYSTEM_USER = "android.permission.CAMERA_HEADLESS_SYSTEM_USER"; + field @FlaggedApi("com.android.internal.camera.flags.camera_hsum_permission") public static final String CAMERA_HEADLESS_SYSTEM_USER = "android.permission.CAMERA_HEADLESS_SYSTEM_USER"; field public static final String CAMERA_OPEN_CLOSE_LISTENER = "android.permission.CAMERA_OPEN_CLOSE_LISTENER"; field public static final String CAPTURE_AUDIO_HOTWORD = "android.permission.CAPTURE_AUDIO_HOTWORD"; field public static final String CAPTURE_CONSENTLESS_BUGREPORT_ON_USERDEBUG_BUILD = "android.permission.CAPTURE_CONSENTLESS_BUGREPORT_ON_USERDEBUG_BUILD"; diff --git a/core/api/test-current.txt b/core/api/test-current.txt index ad8b6852a6f4..572be192fb3e 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1010,6 +1010,7 @@ package android.content.pm { field public static final long OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH = 264304459L; // 0xfc0f74bL field public static final long OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE = 264301586L; // 0xfc0ec12L field public static final long OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS = 263259275L; // 0xfb1048bL + field @FlaggedApi("com.android.window.flags.app_compat_properties_api") public static final long OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED = 273509367L; // 0x104d6bf7L field public static final long OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION = 254631730L; // 0xf2d5f32L field public static final long OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE = 266124927L; // 0xfdcbe7fL field public static final long OVERRIDE_MIN_ASPECT_RATIO = 174042980L; // 0xa5faf64L diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 5674a108baaa..5d4d5e23d6db 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -1174,12 +1174,23 @@ public class Activity extends ContextThemeWrapper return mApplication; } - /** Is this activity embedded inside of another activity? */ + /** + * Whether this is a child {@link Activity} of an {@link ActivityGroup}. + * + * @deprecated {@link ActivityGroup} is deprecated. + */ + @Deprecated public final boolean isChild() { return mParent != null; } - /** Return the parent activity if this view is an embedded child. */ + /** + * Returns the parent {@link Activity} if this is a child {@link Activity} of an + * {@link ActivityGroup}. + * + * @deprecated {@link ActivityGroup} is deprecated. + */ + @Deprecated public final Activity getParent() { return mParent; } diff --git a/core/java/android/app/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java index 343348b89625..f9ab55e00dc6 100644 --- a/core/java/android/app/AutomaticZenRule.java +++ b/core/java/android/app/AutomaticZenRule.java @@ -70,6 +70,8 @@ public final class AutomaticZenRule implements Parcelable { public static final int TYPE_SCHEDULE_CALENDAR = 2; /** * The type for rules triggered by bedtime/sleeping, like time of day, or snore detection. + * + * <p>Only the 'Wellbeing' app may own rules of this type. */ @FlaggedApi(Flags.FLAG_MODES_API) public static final int TYPE_BEDTIME = 3; @@ -95,6 +97,8 @@ public final class AutomaticZenRule implements Parcelable { /** * The type for rules created and managed by a device owner. These rules may not be fully * editable by the device user. + * + * <p>Only a 'Device Owner' app may own rules of this type. */ @FlaggedApi(Flags.FLAG_MODES_API) public static final int TYPE_MANAGED = 7; diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 014ddd41f8d4..edeec77d48fe 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -3481,24 +3481,13 @@ class ContextImpl extends Context { } mResources = r; - // only do this if the user already has more than one preferred locale - if (android.content.res.Flags.defaultLocale() - && r.getConfiguration().getLocales().size() > 1) { - LocaleConfig lc; - if (getUserId() < 0) { - lc = LocaleConfig.fromContextIgnoringOverride(this); - } else { - // This is needed because the app might have locale config overrides that need to - // be read from disk in order for resources to correctly choose which values to - // load. - StrictMode.ThreadPolicy policy = StrictMode.allowThreadDiskReads(); - try { - lc = new LocaleConfig(this); - } finally { - StrictMode.setThreadPolicy(policy); - } + if (r != null) { + // only do this if the user already has more than one preferred locale + if (android.content.res.Flags.defaultLocale() + && r.getConfiguration().getLocales().size() > 1) { + LocaleConfig lc = LocaleConfig.fromContextIgnoringOverride(this); + mResourcesManager.setLocaleConfig(lc); } - mResourcesManager.setLocaleConfig(lc); } } diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index be420debc88f..62db90f79091 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -744,20 +744,21 @@ public class WallpaperManager { params, userId, /* getCropped = */ true); Trace.endSection(); - if (pfd != null) { - try (InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) { - ImageDecoder.Source src = ImageDecoder.createSource(is.readAllBytes()); - return ImageDecoder.decodeBitmap(src, ((decoder, info, source) -> { - // Mutable and hardware config can't be set at the same time. - decoder.setMutableRequired(!hardware); - // Let's do color management - if (cmProxy != null) { - cmProxy.doColorManagement(decoder, info); - } - })); - } catch (OutOfMemoryError | IOException e) { - Log.w(TAG, "Can't decode file", e); - } + if (pfd == null) { + return null; + } + try (InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) { + ImageDecoder.Source src = ImageDecoder.createSource(context.getResources(), is); + return ImageDecoder.decodeBitmap(src, ((decoder, info, source) -> { + // Mutable and hardware config can't be set at the same time. + decoder.setMutableRequired(!hardware); + // Let's do color management + if (cmProxy != null) { + cmProxy.doColorManagement(decoder, info); + } + })); + } catch (OutOfMemoryError | IOException e) { + Log.w(TAG, "Can't decode file", e); } } catch (RemoteException e) { throw e.rethrowFromSystemServer(); diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 4a6349b1b02f..5c42b0ed975a 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -2598,8 +2598,8 @@ public class DevicePolicyManager { * There can be at most one app that has this delegation. * If another app already had delegated certificate selection access, * it will lose the delegation when a new app is delegated. - * <p> The delegaetd app can also call {@link #grantKeyPairToApp} and - * {@link #revokeKeyPairFromApp} to directly grant KeyCain keys to other apps. + * <p> The delegated app can also call {@link #grantKeyPairToApp} and + * {@link #revokeKeyPairFromApp} to directly grant KeyChain keys to other apps. * <p> Can be granted by Device Owner or Profile Owner. */ public static final String DELEGATION_CERT_SELECTION = "delegation-cert-selection"; diff --git a/core/java/android/companion/AssociationInfo.java b/core/java/android/companion/AssociationInfo.java index 161fa799f2f5..cdb92acc5256 100644 --- a/core/java/android/companion/AssociationInfo.java +++ b/core/java/android/companion/AssociationInfo.java @@ -458,6 +458,29 @@ public final class AssociationInfo implements Parcelable { mSystemDataSyncFlags = info.mSystemDataSyncFlags; } + /** + * This builder is used specifically to create a new association to be restored to a device + * that is potentially using a different user ID from the backed-up device. + * + * @hide + */ + public Builder(int id, int userId, @NonNull String packageName, AssociationInfo info) { + mId = id; + mUserId = userId; + mPackageName = packageName; + mTag = info.mTag; + mDeviceMacAddress = info.mDeviceMacAddress; + mDisplayName = info.mDisplayName; + mDeviceProfile = info.mDeviceProfile; + mAssociatedDevice = info.mAssociatedDevice; + mSelfManaged = info.mSelfManaged; + mNotifyOnDeviceNearby = info.mNotifyOnDeviceNearby; + mRevoked = info.mRevoked; + mTimeApprovedMs = info.mTimeApprovedMs; + mLastTimeConnectedMs = info.mLastTimeConnectedMs; + mSystemDataSyncFlags = info.mSystemDataSyncFlags; + } + /** @hide */ @FlaggedApi(Flags.FLAG_ASSOCIATION_TAG) @TestApi diff --git a/core/java/android/companion/datatransfer/PermissionSyncRequest.java b/core/java/android/companion/datatransfer/PermissionSyncRequest.java index 973fca30c765..34865f0f9afd 100644 --- a/core/java/android/companion/datatransfer/PermissionSyncRequest.java +++ b/core/java/android/companion/datatransfer/PermissionSyncRequest.java @@ -48,6 +48,15 @@ public class PermissionSyncRequest extends SystemDataTransferRequest implements } /** @hide */ + @Override + public PermissionSyncRequest copyWithNewId(int associationId) { + PermissionSyncRequest newRequest = new PermissionSyncRequest(associationId); + newRequest.mUserId = this.mUserId; + newRequest.mUserConsented = this.mUserConsented; + return newRequest; + } + + /** @hide */ @NonNull public static final Creator<PermissionSyncRequest> CREATOR = new Creator<PermissionSyncRequest>() { diff --git a/core/java/android/companion/datatransfer/SystemDataTransferRequest.java b/core/java/android/companion/datatransfer/SystemDataTransferRequest.java index 38a553d8a725..c3a2aa4742c3 100644 --- a/core/java/android/companion/datatransfer/SystemDataTransferRequest.java +++ b/core/java/android/companion/datatransfer/SystemDataTransferRequest.java @@ -103,4 +103,15 @@ public abstract class SystemDataTransferRequest { public int describeContents() { return 0; } + + /** + * Creates a copy of itself with new association ID. + * + * This method must be implemented to ensure that backup-and-restore can correctly re-map + * the restored requests to the restored associations that can potentially have different + * IDs than what was originally backed up. + * + * @hide + */ + public abstract SystemDataTransferRequest copyWithNewId(int associationId); } diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 183b9b0000d2..ee1d117bf71c 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -4207,7 +4207,9 @@ public class Intent implements Parcelable, Cloneable { * new state of quiet mode. This is only sent to registered receivers, not manifest receivers. * * <p>This broadcast is similar to {@link #ACTION_MANAGED_PROFILE_AVAILABLE} but functions as a - * generic broadcast for all profile users. + * generic broadcast for all users of type {@link android.os.UserManager#isProfile()}}. In + * case of a managed profile, both {@link #ACTION_MANAGED_PROFILE_AVAILABLE} and + * {@link #ACTION_PROFILE_AVAILABLE} broadcasts are sent. */ @FlaggedApi(FLAG_ALLOW_PRIVATE_PROFILE) public static final String ACTION_PROFILE_AVAILABLE = @@ -4221,7 +4223,9 @@ public class Intent implements Parcelable, Cloneable { * new state of quiet mode. This is only sent to registered receivers, not manifest receivers. * * <p>This broadcast is similar to {@link #ACTION_MANAGED_PROFILE_UNAVAILABLE} but functions as - * a generic broadcast for all profile users. + * a generic broadcast for all users of type {@link android.os.UserManager#isProfile()}}. In + * case of a managed profile, both {@link #ACTION_MANAGED_PROFILE_UNAVAILABLE} and + * {@link #ACTION_PROFILE_UNAVAILABLE} broadcasts are sent. */ @FlaggedApi(FLAG_ALLOW_PRIVATE_PROFILE) public static final String ACTION_PROFILE_UNAVAILABLE = @@ -6971,16 +6975,21 @@ public class Intent implements Parcelable, Cloneable { public static final int FLAG_DEBUG_LOG_RESOLUTION = 0x00000008; /** * If set, this intent will not match any components in packages that - * are currently stopped. If this is not set, then the default behavior - * is to include such applications in the result. + * are currently + * {@linkplain android.content.pm.ApplicationInfo#FLAG_STOPPED stopped}. + * If this is not set, then the default behavior is to include such + * applications in the result. */ public static final int FLAG_EXCLUDE_STOPPED_PACKAGES = 0x00000010; /** * If set, this intent will always match any components in packages that - * are currently stopped. This is the default behavior when + * are currently + * {@linkplain android.content.pm.ApplicationInfo#FLAG_STOPPED stopped}. + * This is the default behavior when * {@link #FLAG_EXCLUDE_STOPPED_PACKAGES} is not set. If both of these * flags are set, this one wins (it allows overriding of exclude for - * places where the framework may automatically set the exclude flag). + * places where the framework may automatically set the exclude flag, + * such as broadcasts). */ public static final int FLAG_INCLUDE_STOPPED_PACKAGES = 0x00000020; diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index 12da66515c07..30871e938e68 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -16,6 +16,7 @@ package android.content.pm; +import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.IntDef; import android.annotation.NonNull; @@ -42,6 +43,7 @@ import android.util.Printer; import android.window.OnBackInvokedCallback; import com.android.internal.util.Parcelling; +import com.android.window.flags.Flags; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -1099,6 +1101,8 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { @ChangeId @Overridable @Disabled + @TestApi + @FlaggedApi(Flags.FLAG_APP_COMPAT_PROPERTIES_API) public static final long OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED = 273509367L; // buganizer id diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 3713380485ea..869c621e8564 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -375,6 +375,19 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { /** * Value for {@link #flags}: true if this application's package is in * the stopped state. + * + * <p>Stopped is the initial state after an app is installed, before it is launched + * or otherwise directly interacted with by the user. The system tries not to + * start it unless initiated by a user interaction (typically launching its icon + * from the launcher, could also include user actions like adding it as an app widget, + * selecting it as a live wallpaper, selecting it as a keyboard, etc). Stopped + * applications will not receive broadcasts unless the sender specifies + * {@link android.content.Intent#FLAG_INCLUDE_STOPPED_PACKAGES}. + * + * <p>Applications should avoid launching activies, binding to or starting services, or + * otherwise causing a stopped application to run unless initiated by the user. + * + * <p>An app can also return to the stopped state by a "force stop". */ public static final int FLAG_STOPPED = 1<<21; diff --git a/core/java/android/content/pm/ModuleInfo.java b/core/java/android/content/pm/ModuleInfo.java index a7306a311ad8..a1c874725d4b 100644 --- a/core/java/android/content/pm/ModuleInfo.java +++ b/core/java/android/content/pm/ModuleInfo.java @@ -16,10 +16,15 @@ package android.content.pm; +import android.annotation.FlaggedApi; +import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Parcel; import android.os.Parcelable; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Objects; /** @@ -46,6 +51,13 @@ public final class ModuleInfo implements Parcelable { /** Whether or not this module is hidden from the user. */ private boolean mHidden; + /** + * The list of the package names of all APK-in-APEX apps in the module, or + * null if there are none. + */ + @Nullable + private List<String> mApkInApexPackageNames; + // TODO: Decide whether we need an additional metadata bundle to support out of band // updates to ModuleInfo. // @@ -61,6 +73,9 @@ public final class ModuleInfo implements Parcelable { mPackageName = orig.mPackageName; mHidden = orig.mHidden; mApexModuleName = orig.mApexModuleName; + if (orig.mApkInApexPackageNames != null) { + mApkInApexPackageNames = List.copyOf(orig.mApkInApexPackageNames); + } } /** @hide Sets the public name of this module. */ @@ -107,6 +122,25 @@ public final class ModuleInfo implements Parcelable { return mApexModuleName; } + /** @hide Sets the list of the package name of APK-in-APEX apps in this module. */ + public ModuleInfo setApkInApexPackageNames(@NonNull Collection<String> apkInApexPackageNames) { + Objects.requireNonNull(apkInApexPackageNames); + mApkInApexPackageNames = List.copyOf(apkInApexPackageNames); + return this; + } + + /** + * Gets the list of the package name of all APK-in-APEX apps in the module. + */ + @NonNull + @FlaggedApi(android.content.pm.Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX) + public Collection<String> getApkInApexPackageNames() { + if (mApkInApexPackageNames == null) { + return Collections.emptyList(); + } + return mApkInApexPackageNames; + } + /** Returns a string representation of this object. */ public String toString() { return "ModuleInfo{" @@ -125,6 +159,7 @@ public final class ModuleInfo implements Parcelable { hashCode = 31 * hashCode + Objects.hashCode(mName); hashCode = 31 * hashCode + Objects.hashCode(mPackageName); hashCode = 31 * hashCode + Objects.hashCode(mApexModuleName); + hashCode = 31 * hashCode + Objects.hashCode(mApkInApexPackageNames); hashCode = 31 * hashCode + Boolean.hashCode(mHidden); return hashCode; } @@ -138,6 +173,7 @@ public final class ModuleInfo implements Parcelable { return Objects.equals(mName, other.mName) && Objects.equals(mPackageName, other.mPackageName) && Objects.equals(mApexModuleName, other.mApexModuleName) + && Objects.equals(mApkInApexPackageNames, other.mApkInApexPackageNames) && mHidden == other.mHidden; } @@ -147,6 +183,8 @@ public final class ModuleInfo implements Parcelable { dest.writeString(mPackageName); dest.writeBoolean(mHidden); dest.writeString(mApexModuleName); + // Parcel#writeStringList handles null case, we can use it directly + dest.writeStringList(mApkInApexPackageNames); } private ModuleInfo(Parcel source) { @@ -154,6 +192,7 @@ public final class ModuleInfo implements Parcelable { mPackageName = source.readString(); mHidden = source.readBoolean(); mApexModuleName = source.readString(); + mApkInApexPackageNames = source.createStringArrayList(); } public static final @android.annotation.NonNull Parcelable.Creator<ModuleInfo> CREATOR = diff --git a/core/java/android/content/pm/PackageInfo.java b/core/java/android/content/pm/PackageInfo.java index 4f61613b9c6e..c1c9928e0571 100644 --- a/core/java/android/content/pm/PackageInfo.java +++ b/core/java/android/content/pm/PackageInfo.java @@ -499,6 +499,16 @@ public class PackageInfo implements Parcelable { */ public boolean isActiveApex; + /** + * If the package is an APEX package (i.e. the value of {@link #isApex} + * is true), this field is the package name of the APEX. If the package + * is one APK-in-APEX app, this field is the package name of the parent + * APEX that contains the app. If the package is not one of the above + * two cases, this field is {@code null}. + */ + @Nullable + private String mApexPackageName; + public PackageInfo() { } @@ -535,6 +545,26 @@ public class PackageInfo implements Parcelable { mArchiveTimeMillis = value; } + /** + * If the package is an APEX package (i.e. the value of {@link #isApex} + * is true), returns the package name of the APEX. If the package + * is one APK-in-APEX app, returns the package name of the parent + * APEX that contains the app. If the package is not one of the above + * two cases, returns {@code null}. + */ + @Nullable + @FlaggedApi(android.content.pm.Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX) + public String getApexPackageName() { + return mApexPackageName; + } + + /** + * @hide + */ + public void setApexPackageName(@Nullable String apexPackageName) { + mApexPackageName = apexPackageName; + } + @Override public String toString() { return "PackageInfo{" @@ -603,6 +633,12 @@ public class PackageInfo implements Parcelable { dest.writeBoolean(isApex); dest.writeBoolean(isActiveApex); dest.writeLong(mArchiveTimeMillis); + if (mApexPackageName != null) { + dest.writeInt(1); + dest.writeString8(mApexPackageName); + } else { + dest.writeInt(0); + } dest.restoreAllowSquashing(prevAllowSquashing); } @@ -669,5 +705,9 @@ public class PackageInfo implements Parcelable { isApex = source.readBoolean(); isActiveApex = source.readBoolean(); mArchiveTimeMillis = source.readLong(); + int hasApexPackageName = source.readInt(); + if (hasApexPackageName != 0) { + mApexPackageName = source.readString8(); + } } } diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 8151a9133f5b..0fb0993d0f04 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -11024,7 +11024,7 @@ public abstract class PackageManager { } /** - * Returns the property defined in the given package's <appliction> tag. + * Returns the property defined in the given package's <application> tag. * * @throws NameNotFoundException if either the given package is not installed or if the * given property is not defined within the <application> tag. diff --git a/core/java/android/content/rollback/OWNERS b/core/java/android/content/rollback/OWNERS index 3093fd686a21..8e5a0d8af550 100644 --- a/core/java/android/content/rollback/OWNERS +++ b/core/java/android/content/rollback/OWNERS @@ -1,5 +1,5 @@ -# Bug component: 557916 +# Bug component: 819107 -narayan@google.com -nandana@google.com -olilan@google.com +ancr@google.com +harshitmahajan@google.com +robertogil@google.com diff --git a/core/java/android/credentials/ui/Constants.java b/core/java/android/credentials/ui/Constants.java index 7092f291eea5..37f850bc46c5 100644 --- a/core/java/android/credentials/ui/Constants.java +++ b/core/java/android/credentials/ui/Constants.java @@ -29,6 +29,13 @@ public class Constants { public static final String EXTRA_RESULT_RECEIVER = "android.credentials.ui.extra.RESULT_RECEIVER"; + /** + * The intent extra key for indicating whether the bottom sheet should be started directly + * on the 'All Options' screen. + */ + public static final String EXTRA_REQ_FOR_ALL_OPTIONS = + "android.credentials.ui.extra.REQ_FOR_ALL_OPTIONS"; + /** The intent action for when the enabled Credential Manager providers has been updated. */ public static final String CREDMAN_ENABLED_PROVIDERS_UPDATED = "android.credentials.ui.action.CREDMAN_ENABLED_PROVIDERS_UPDATED"; diff --git a/core/java/android/credentials/ui/IntentFactory.java b/core/java/android/credentials/ui/IntentFactory.java index 5e8372d68eb2..49321d514128 100644 --- a/core/java/android/credentials/ui/IntentFactory.java +++ b/core/java/android/credentials/ui/IntentFactory.java @@ -35,6 +35,31 @@ import java.util.ArrayList; */ @TestApi public class IntentFactory { + + /** + * Generate a new launch intent to the Credential Selector UI. + * + * @hide + */ + @NonNull + public static Intent createCredentialSelectorIntent( + @NonNull RequestInfo requestInfo, + @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling. + @NonNull + ArrayList<ProviderData> enabledProviderDataList, + @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling. + @NonNull + ArrayList<DisabledProviderData> disabledProviderDataList, + @NonNull ResultReceiver resultReceiver, + boolean isRequestForAllOptions) { + + Intent intent = createCredentialSelectorIntent(requestInfo, enabledProviderDataList, + disabledProviderDataList, resultReceiver); + intent.putExtra(Constants.EXTRA_REQ_FOR_ALL_OPTIONS, isRequestForAllOptions); + + return intent; + } + /** Generate a new launch intent to the Credential Selector UI. */ @NonNull public static Intent createCredentialSelectorIntent( diff --git a/core/java/android/hardware/face/FaceAuthenticateOptions.java b/core/java/android/hardware/face/FaceAuthenticateOptions.java index 1c6de0464245..518f902acdcb 100644 --- a/core/java/android/hardware/face/FaceAuthenticateOptions.java +++ b/core/java/android/hardware/face/FaceAuthenticateOptions.java @@ -261,7 +261,7 @@ public class FaceAuthenticateOptions implements AuthenticateOptions, Parcelable * The reason for this operation when requested by the system (sysui), * otherwise AUTHENTICATE_REASON_UNKNOWN. * - * See frameworks/base/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt + * See packages/SystemUI/src/com/android/systemui/deviceentry/shared/FaceAuthReason.kt * for more details about each reason. */ @DataClass.Generated.Member @@ -524,7 +524,7 @@ public class FaceAuthenticateOptions implements AuthenticateOptions, Parcelable * The reason for this operation when requested by the system (sysui), * otherwise AUTHENTICATE_REASON_UNKNOWN. * - * See frameworks/base/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt + * See packages/SystemUI/src/com/android/systemui/deviceentry/shared/FaceAuthReason.kt * for more details about each reason. */ @DataClass.Generated.Member diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index 6626baffd134..7bea9aeeb86b 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -25,6 +25,7 @@ import android.hardware.input.IInputDeviceBatteryListener; import android.hardware.input.IInputDeviceBatteryState; import android.hardware.input.IKeyboardBacklightListener; import android.hardware.input.IKeyboardBacklightState; +import android.hardware.input.IStickyModifierStateListener; import android.hardware.input.ITabletModeChangedListener; import android.hardware.input.TouchCalibration; import android.os.CombinedVibration; @@ -241,4 +242,14 @@ interface IInputManager { void unregisterKeyboardBacklightListener(IKeyboardBacklightListener listener); HostUsiVersion getHostUsiVersionFromDisplayConfig(int displayId); + + @EnforcePermission("MONITOR_STICKY_MODIFIER_STATE") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.MONITOR_STICKY_MODIFIER_STATE)") + void registerStickyModifierStateListener(IStickyModifierStateListener listener); + + @EnforcePermission("MONITOR_STICKY_MODIFIER_STATE") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.MONITOR_STICKY_MODIFIER_STATE)") + void unregisterStickyModifierStateListener(IStickyModifierStateListener listener); } diff --git a/core/java/android/hardware/input/IStickyModifierStateListener.aidl b/core/java/android/hardware/input/IStickyModifierStateListener.aidl new file mode 100644 index 000000000000..bd139ab75698 --- /dev/null +++ b/core/java/android/hardware/input/IStickyModifierStateListener.aidl @@ -0,0 +1,26 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.input; + +/** @hide */ +oneway interface IStickyModifierStateListener { + + /** + * Called when the sticky modifier state is changed when A11y Sticky keys feature is enabled + */ + void onStickyModifierStateChanged(int modifierState, int lockedModifierState); +} diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index f941ad87bac5..4ebbde732747 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -1297,6 +1297,42 @@ public final class InputManager { } /** + * Registers a Sticky modifier state change listener to be notified about {@link + * StickyModifierState} changes. + * + * @param executor an executor on which the callback will be called + * @param listener the {@link StickyModifierStateListener} + * @throws IllegalArgumentException if {@code listener} has already been registered previously. + * @throws NullPointerException if {@code listener} or {@code executor} is null. + * @hide + * @see #unregisterStickyModifierStateListener(StickyModifierStateListener) + */ + @RequiresPermission(Manifest.permission.MONITOR_STICKY_MODIFIER_STATE) + public void registerStickyModifierStateListener(@NonNull Executor executor, + @NonNull StickyModifierStateListener listener) throws IllegalArgumentException { + if (!InputSettings.isAccessibilityStickyKeysFeatureEnabled()) { + return; + } + mGlobal.registerStickyModifierStateListener(executor, listener); + } + + /** + * Unregisters a previously added Sticky modifier state change listener. + * + * @param listener the {@link StickyModifierStateListener} + * @hide + * @see #registerStickyModifierStateListener(Executor, StickyModifierStateListener) + */ + @RequiresPermission(Manifest.permission.MONITOR_STICKY_MODIFIER_STATE) + public void unregisterStickyModifierStateListener( + @NonNull StickyModifierStateListener listener) { + if (!InputSettings.isAccessibilityStickyKeysFeatureEnabled()) { + return; + } + mGlobal.unregisterStickyModifierStateListener(listener); + } + + /** * A callback used to be notified about battery state changes for an input device. The * {@link #onBatteryStateChanged(int, long, BatteryState)} method will be called once after the * listener is successfully registered to provide the initial battery state of the device. @@ -1378,4 +1414,23 @@ public final class InputManager { void onKeyboardBacklightChanged( int deviceId, @NonNull KeyboardBacklightState state, boolean isTriggeredByKeyPress); } + + /** + * A callback used to be notified about sticky modifier state changes when A11y Sticky keys + * feature is enabled. + * + * @see #registerStickyModifierStateListener(Executor, StickyModifierStateListener) + * @see #unregisterStickyModifierStateListener(StickyModifierStateListener) + * @hide + */ + public interface StickyModifierStateListener { + /** + * Called when the sticky modifier state changes. + * This method will be called once after the listener is successfully registered to provide + * the initial modifier state. + * + * @param state the new sticky modifier state, never null. + */ + void onStickyModifierStateChanged(@NonNull StickyModifierState state); + } } diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java index 24a69116e77e..7c104a0ca946 100644 --- a/core/java/android/hardware/input/InputManagerGlobal.java +++ b/core/java/android/hardware/input/InputManagerGlobal.java @@ -27,6 +27,7 @@ import android.hardware.input.InputManager.InputDeviceBatteryListener; import android.hardware.input.InputManager.InputDeviceListener; import android.hardware.input.InputManager.KeyboardBacklightListener; import android.hardware.input.InputManager.OnTabletModeChangedListener; +import android.hardware.input.InputManager.StickyModifierStateListener; import android.hardware.lights.Light; import android.hardware.lights.LightState; import android.hardware.lights.LightsManager; @@ -52,6 +53,7 @@ import android.view.InputDevice; import android.view.InputEvent; import android.view.InputMonitor; import android.view.KeyCharacterMap; +import android.view.KeyEvent; import android.view.PointerIcon; import com.android.internal.annotations.GuardedBy; @@ -100,6 +102,14 @@ public final class InputManagerGlobal { @GuardedBy("mKeyboardBacklightListenerLock") @Nullable private IKeyboardBacklightListener mKeyboardBacklightListener; + private final Object mStickyModifierStateListenerLock = new Object(); + @GuardedBy("mStickyModifierStateListenerLock") + @Nullable + private ArrayList<StickyModifierStateListenerDelegate> mStickyModifierStateListeners; + @GuardedBy("mStickyModifierStateListenerLock") + @Nullable + private IStickyModifierStateListener mStickyModifierStateListener; + // InputDeviceSensorManager gets notified synchronously from the binder thread when input // devices change, so it must be synchronized with the input device listeners. @GuardedBy("mInputDeviceListeners") @@ -905,6 +915,158 @@ public final class InputManagerGlobal { } } + private static final class StickyModifierStateListenerDelegate { + final InputManager.StickyModifierStateListener mListener; + final Executor mExecutor; + + StickyModifierStateListenerDelegate(StickyModifierStateListener listener, + Executor executor) { + mListener = listener; + mExecutor = executor; + } + + void notifyStickyModifierStateChange(int modifierState, int lockedModifierState) { + mExecutor.execute(() -> + mListener.onStickyModifierStateChanged( + new LocalStickyModifierState(modifierState, lockedModifierState))); + } + } + + private class LocalStickyModifierStateListener extends IStickyModifierStateListener.Stub { + + @Override + public void onStickyModifierStateChanged(int modifierState, int lockedModifierState) { + synchronized (mStickyModifierStateListenerLock) { + if (mStickyModifierStateListeners == null) return; + final int numListeners = mStickyModifierStateListeners.size(); + for (int i = 0; i < numListeners; i++) { + mStickyModifierStateListeners.get(i) + .notifyStickyModifierStateChange(modifierState, lockedModifierState); + } + } + } + } + + // Implementation of the android.hardware.input.StickyModifierState interface used to report + // the sticky modifier state via the StickyModifierStateListener interfaces. + private static final class LocalStickyModifierState extends StickyModifierState { + + private final int mModifierState; + private final int mLockedModifierState; + + LocalStickyModifierState(int modifierState, int lockedModifierState) { + mModifierState = modifierState; + mLockedModifierState = lockedModifierState; + } + + @Override + public boolean isShiftModifierOn() { + return (mModifierState & KeyEvent.META_SHIFT_ON) != 0; + } + + @Override + public boolean isShiftModifierLocked() { + return (mLockedModifierState & KeyEvent.META_SHIFT_ON) != 0; + } + + @Override + public boolean isCtrlModifierOn() { + return (mModifierState & KeyEvent.META_CTRL_ON) != 0; + } + + @Override + public boolean isCtrlModifierLocked() { + return (mLockedModifierState & KeyEvent.META_CTRL_ON) != 0; + } + + @Override + public boolean isMetaModifierOn() { + return (mModifierState & KeyEvent.META_META_ON) != 0; + } + + @Override + public boolean isMetaModifierLocked() { + return (mLockedModifierState & KeyEvent.META_META_ON) != 0; + } + + @Override + public boolean isAltModifierOn() { + return (mModifierState & KeyEvent.META_ALT_LEFT_ON) != 0; + } + + @Override + public boolean isAltModifierLocked() { + return (mLockedModifierState & KeyEvent.META_ALT_LEFT_ON) != 0; + } + + @Override + public boolean isAltGrModifierOn() { + return (mModifierState & KeyEvent.META_ALT_RIGHT_ON) != 0; + } + + @Override + public boolean isAltGrModifierLocked() { + return (mLockedModifierState & KeyEvent.META_ALT_RIGHT_ON) != 0; + } + } + + /** + * @see InputManager#registerStickyModifierStateListener(Executor, StickyModifierStateListener) + */ + @RequiresPermission(Manifest.permission.MONITOR_STICKY_MODIFIER_STATE) + void registerStickyModifierStateListener(@NonNull Executor executor, + @NonNull StickyModifierStateListener listener) throws IllegalArgumentException { + Objects.requireNonNull(executor, "executor should not be null"); + Objects.requireNonNull(listener, "listener should not be null"); + + synchronized (mStickyModifierStateListenerLock) { + if (mStickyModifierStateListener == null) { + mStickyModifierStateListeners = new ArrayList<>(); + mStickyModifierStateListener = new LocalStickyModifierStateListener(); + + try { + mIm.registerStickyModifierStateListener(mStickyModifierStateListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + final int numListeners = mStickyModifierStateListeners.size(); + for (int i = 0; i < numListeners; i++) { + if (mStickyModifierStateListeners.get(i).mListener == listener) { + throw new IllegalArgumentException("Listener has already been registered!"); + } + } + StickyModifierStateListenerDelegate delegate = + new StickyModifierStateListenerDelegate(listener, executor); + mStickyModifierStateListeners.add(delegate); + } + } + + /** + * @see InputManager#unregisterStickyModifierStateListener(StickyModifierStateListener) + */ + @RequiresPermission(Manifest.permission.MONITOR_STICKY_MODIFIER_STATE) + void unregisterStickyModifierStateListener( + @NonNull StickyModifierStateListener listener) { + Objects.requireNonNull(listener, "listener should not be null"); + + synchronized (mStickyModifierStateListenerLock) { + if (mStickyModifierStateListeners == null) { + return; + } + mStickyModifierStateListeners.removeIf((delegate) -> delegate.mListener == listener); + if (mStickyModifierStateListeners.isEmpty()) { + try { + mIm.unregisterStickyModifierStateListener(mStickyModifierStateListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + mStickyModifierStateListeners = null; + mStickyModifierStateListener = null; + } + } + } + /** * @see InputManager#getKeyboardLayoutsForInputDevice(InputDeviceIdentifier) */ diff --git a/core/java/android/hardware/input/StickyModifierState.java b/core/java/android/hardware/input/StickyModifierState.java new file mode 100644 index 000000000000..a3f7a0ab542e --- /dev/null +++ b/core/java/android/hardware/input/StickyModifierState.java @@ -0,0 +1,127 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.input; + +/** + * The StickyModifierState class is a representation of a modifier state when A11y Sticky keys + * feature is enabled + * + * @hide + */ +public abstract class StickyModifierState { + + /** + * Represents whether current sticky modifier state includes 'Shift' modifier. + * <p> If {@code true} the next {@link android.view.KeyEvent} will contain 'Shift' modifier in + * its metaState. + * + * @return whether Shift modifier key is on. + */ + public abstract boolean isShiftModifierOn(); + + /** + * Represents whether current sticky modifier state includes 'Shift' modifier, and it is + * locked. + * <p> If {@code true} any subsequent {@link android.view.KeyEvent} will contain 'Shift' + * modifier in its metaState and this state will remain sticky (will not be cleared), until + * user presses 'Shift' key again to clear the locked state. + * + * @return whether Shift modifier key is locked. + */ + public abstract boolean isShiftModifierLocked(); + + /** + * Represents whether current sticky modifier state includes 'Ctrl' modifier. + * <p> If {@code true} the next {@link android.view.KeyEvent} will contain 'Ctrl' modifier in + * its metaState. + * + * @return whether Ctrl modifier key is on. + */ + public abstract boolean isCtrlModifierOn(); + + /** + * Represents whether current sticky modifier state includes 'Ctrl' modifier, and it is + * locked. + * <p> If {@code true} any subsequent {@link android.view.KeyEvent} will contain 'Ctrl' + * modifier in its metaState and this state will remain sticky (will not be cleared), until + * user presses 'Ctrl' key again to clear the locked state. + * + * @return whether Ctrl modifier key is locked. + */ + public abstract boolean isCtrlModifierLocked(); + + /** + * Represents whether current sticky modifier state includes 'Meta' modifier. + * <p> If {@code true} the next {@link android.view.KeyEvent} will contain 'Meta' modifier in + * its metaState. + * + * @return whether Meta modifier key is on. + */ + public abstract boolean isMetaModifierOn(); + + /** + * Represents whether current sticky modifier state includes 'Meta' modifier, and it is + * locked. + * <p> If {@code true} any subsequent {@link android.view.KeyEvent} will contain 'Meta' + * modifier in its metaState and this state will remain sticky (will not be cleared), until + * user presses 'Meta' key again to clear the locked state. + * + * @return whether Meta modifier key is locked. + */ + public abstract boolean isMetaModifierLocked(); + + /** + * Represents whether current sticky modifier state includes 'Alt' modifier. + * <p> If {@code true} the next {@link android.view.KeyEvent} will contain 'Alt' modifier in + * its metaState. + * + * @return whether Alt modifier key is on. + */ + public abstract boolean isAltModifierOn(); + + /** + * Represents whether current sticky modifier state includes 'Alt' modifier, and it is + * locked. + * <p> If {@code true} any subsequent {@link android.view.KeyEvent} will contain 'Alt' + * modifier in its metaState and this state will remain sticky (will not be cleared), until + * user presses 'Alt' key again to clear the locked state. + * + * @return whether Alt modifier key is locked. + */ + public abstract boolean isAltModifierLocked(); + + /** + * Represents whether current sticky modifier state includes 'AltGr' modifier. + * <p> If {@code true} the next {@link android.view.KeyEvent} will contain 'AltGr' modifier in + * its metaState. + * + * @return whether AltGr modifier key is on. + */ + public abstract boolean isAltGrModifierOn(); + + /** + * Represents whether current sticky modifier state includes 'AltGr' modifier, and it is + * locked. + * <p> If {@code true} any subsequent {@link android.view.KeyEvent} will contain 'AltGr' + * modifier in its metaState and this state will remain sticky (will not be cleared), until + * user presses 'AltGr' key again to clear the locked state. + * + * @return whether AltGr modifier key is locked. + */ + public abstract boolean isAltGrModifierLocked(); +} + diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 18d3e5e02fbe..71698e4f4469 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -127,6 +127,7 @@ import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.Flags; import android.view.inputmethod.ImeTracker; import android.view.inputmethod.InlineSuggestionsRequest; import android.view.inputmethod.InlineSuggestionsResponse; @@ -388,6 +389,9 @@ public class InputMethodService extends AbstractInputMethodService { private long mStylusHwSessionsTimeout = STYLUS_HANDWRITING_IDLE_TIMEOUT_MS; private Runnable mStylusWindowIdleTimeoutRunnable; private long mStylusWindowIdleTimeoutForTest; + /** Tracks last {@link MotionEvent#getToolType(int)} used for {@link MotionEvent#ACTION_DOWN}. + **/ + private int mLastUsedToolType; /** * Returns whether {@link InputMethodService} is responsible for rendering the back button and @@ -1005,7 +1009,7 @@ public class InputMethodService extends AbstractInputMethodService { */ @Override public void updateEditorToolType(@ToolType int toolType) { - onUpdateEditorToolType(toolType); + updateEditorToolTypeInternal(toolType); } /** @@ -1249,6 +1253,14 @@ public class InputMethodService extends AbstractInputMethodService { rootView.setSystemGestureExclusionRects(exclusionRects); } + private void updateEditorToolTypeInternal(int toolType) { + if (Flags.useHandwritingListenerForTooltype()) { + mLastUsedToolType = toolType; + mInputEditorInfo.setInitialToolType(toolType); + } + onUpdateEditorToolType(toolType); + } + /** * Concrete implementation of * {@link AbstractInputMethodService.AbstractInputMethodSessionImpl} that provides @@ -3110,6 +3122,9 @@ public class InputMethodService extends AbstractInputMethodService { null /* icProto */); mInputStarted = true; mStartedInputConnection = ic; + if (Flags.useHandwritingListenerForTooltype()) { + editorInfo.setInitialToolType(mLastUsedToolType); + } mInputEditorInfo = editorInfo; initialize(); mInlineSuggestionSessionController.notifyOnStartInput( @@ -3354,6 +3369,10 @@ public class InputMethodService extends AbstractInputMethodService { * had not seen the event at all. */ public boolean onKeyDown(int keyCode, KeyEvent event) { + if (Flags.useHandwritingListenerForTooltype()) { + // any KeyEvent keyDown should reset last toolType. + updateEditorToolTypeInternal(MotionEvent.TOOL_TYPE_UNKNOWN); + } if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { final ExtractEditText eet = getExtractEditTextIfVisible(); if (eet != null && eet.handleBackInTextActionModeIfNeeded(event)) { diff --git a/core/java/android/nfc/INfcAdapter.aidl b/core/java/android/nfc/INfcAdapter.aidl index 967a0cc92ef5..286cf2890eea 100644 --- a/core/java/android/nfc/INfcAdapter.aidl +++ b/core/java/android/nfc/INfcAdapter.aidl @@ -95,4 +95,6 @@ interface INfcAdapter void registerWlcStateListener(in INfcWlcStateListener listener); void unregisterWlcStateListener(in INfcWlcStateListener listener); WlcLDeviceInfo getWlcLDeviceInfo(); + + void updateDiscoveryTechnology(IBinder b, int pollFlags, int listenFlags); } diff --git a/core/java/android/nfc/INfcCardEmulation.aidl b/core/java/android/nfc/INfcCardEmulation.aidl index 191385a3c13d..f4b46046bc3e 100644 --- a/core/java/android/nfc/INfcCardEmulation.aidl +++ b/core/java/android/nfc/INfcCardEmulation.aidl @@ -43,4 +43,7 @@ interface INfcCardEmulation ApduServiceInfo getPreferredPaymentService(int userHandle); boolean setServiceEnabledForCategoryOther(int userHandle, in ComponentName app, boolean status); boolean isDefaultPaymentRegistered(); + + boolean overrideRoutingTable(int userHandle, String protocol, String technology); + boolean recoverRoutingTable(int userHandle); } diff --git a/core/java/android/nfc/NfcActivityManager.java b/core/java/android/nfc/NfcActivityManager.java index 8d75cac531fb..f03fc0af86b3 100644 --- a/core/java/android/nfc/NfcActivityManager.java +++ b/core/java/android/nfc/NfcActivityManager.java @@ -112,6 +112,9 @@ public final class NfcActivityManager extends IAppCallback.Stub Bundle readerModeExtras = null; Binder token; + int mPollTech = NfcAdapter.FLAG_USE_ALL_TECH; + int mListenTech = NfcAdapter.FLAG_USE_ALL_TECH; + public NfcActivityState(Activity activity) { if (activity.isDestroyed()) { throw new IllegalStateException("activity is already destroyed"); @@ -132,6 +135,9 @@ public final class NfcActivityManager extends IAppCallback.Stub readerModeFlags = 0; readerModeExtras = null; token = null; + + mPollTech = NfcAdapter.FLAG_USE_ALL_TECH; + mListenTech = NfcAdapter.FLAG_USE_ALL_TECH; } @Override public String toString() { @@ -278,6 +284,9 @@ public final class NfcActivityManager extends IAppCallback.Stub int readerModeFlags = 0; Bundle readerModeExtras = null; Binder token; + int pollTech; + int listenTech; + synchronized (NfcActivityManager.this) { NfcActivityState state = findActivityState(activity); if (DBG) Log.d(TAG, "onResume() for " + activity + " " + state); @@ -286,9 +295,15 @@ public final class NfcActivityManager extends IAppCallback.Stub token = state.token; readerModeFlags = state.readerModeFlags; readerModeExtras = state.readerModeExtras; + + pollTech = state.mPollTech; + listenTech = state.mListenTech; } if (readerModeFlags != 0) { setReaderMode(token, readerModeFlags, readerModeExtras); + } else if (listenTech != NfcAdapter.FLAG_USE_ALL_TECH + || pollTech != NfcAdapter.FLAG_USE_ALL_TECH) { + changeDiscoveryTech(token, pollTech, listenTech); } requestNfcServiceCallback(); } @@ -298,6 +313,9 @@ public final class NfcActivityManager extends IAppCallback.Stub public void onActivityPaused(Activity activity) { boolean readerModeFlagsSet; Binder token; + int pollTech; + int listenTech; + synchronized (NfcActivityManager.this) { NfcActivityState state = findActivityState(activity); if (DBG) Log.d(TAG, "onPause() for " + activity + " " + state); @@ -305,10 +323,17 @@ public final class NfcActivityManager extends IAppCallback.Stub state.resumed = false; token = state.token; readerModeFlagsSet = state.readerModeFlags != 0; + + pollTech = state.mPollTech; + listenTech = state.mListenTech; } if (readerModeFlagsSet) { // Restore default p2p modes setReaderMode(token, 0, null); + } else if (listenTech != NfcAdapter.FLAG_USE_ALL_TECH + || pollTech != NfcAdapter.FLAG_USE_ALL_TECH) { + changeDiscoveryTech(token, + NfcAdapter.FLAG_USE_ALL_TECH, NfcAdapter.FLAG_USE_ALL_TECH); } } @@ -333,4 +358,53 @@ public final class NfcActivityManager extends IAppCallback.Stub } } + /** setDiscoveryTechnology() implementation */ + public void setDiscoveryTech(Activity activity, int pollTech, int listenTech) { + boolean isResumed; + Binder token; + boolean readerModeFlagsSet; + synchronized (NfcActivityManager.this) { + NfcActivityState state = getActivityState(activity); + readerModeFlagsSet = state.readerModeFlags != 0; + state.mListenTech = listenTech; + state.mPollTech = pollTech; + token = state.token; + isResumed = state.resumed; + } + if (!readerModeFlagsSet && isResumed) { + changeDiscoveryTech(token, pollTech, listenTech); + } else if (readerModeFlagsSet) { + throw new IllegalStateException("Cannot be used when the Reader Mode is enabled"); + } + } + + /** resetDiscoveryTechnology() implementation */ + public void resetDiscoveryTech(Activity activity) { + boolean isResumed; + Binder token; + boolean readerModeFlagsSet; + synchronized (NfcActivityManager.this) { + NfcActivityState state = getActivityState(activity); + readerModeFlagsSet = state.readerModeFlags != 0; + state.mListenTech = NfcAdapter.FLAG_USE_ALL_TECH; + state.mPollTech = NfcAdapter.FLAG_USE_ALL_TECH; + token = state.token; + isResumed = state.resumed; + } + if (readerModeFlagsSet) { + disableReaderMode(activity); + } else if (isResumed) { + changeDiscoveryTech(token, NfcAdapter.FLAG_USE_ALL_TECH, NfcAdapter.FLAG_USE_ALL_TECH); + } + + } + + private void changeDiscoveryTech(Binder token, int pollTech, int listenTech) { + try { + NfcAdapter.sService.updateDiscoveryTechnology(token, pollTech, listenTech); + } catch (RemoteException e) { + mAdapter.attemptDeadServiceRecovery(e); + } + } + } diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java index 21e23ae53979..5a40e424ea91 100644 --- a/core/java/android/nfc/NfcAdapter.java +++ b/core/java/android/nfc/NfcAdapter.java @@ -333,6 +333,19 @@ public final class NfcAdapter { */ public static final int FLAG_READER_NFC_BARCODE = 0x10; + /** @hide */ + @IntDef(flag = true, prefix = {"FLAG_READER_"}, value = { + FLAG_READER_KEEP, + FLAG_READER_DISABLE, + FLAG_READER_NFC_A, + FLAG_READER_NFC_B, + FLAG_READER_NFC_F, + FLAG_READER_NFC_V, + FLAG_READER_NFC_BARCODE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface PollTechnology {} + /** * Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}. * <p> @@ -360,6 +373,76 @@ public final class NfcAdapter { public static final String EXTRA_READER_PRESENCE_CHECK_DELAY = "presence"; /** + * Flag for use with {@link #setDiscoveryTechnology(Activity, int, int)}. + * <p> + * Setting this flag enables listening for Nfc-A technology. + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH) + public static final int FLAG_LISTEN_NFC_PASSIVE_A = 0x1; + + /** + * Flag for use with {@link #setDiscoveryTechnology(Activity, int, int)}. + * <p> + * Setting this flag enables listening for Nfc-B technology. + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH) + public static final int FLAG_LISTEN_NFC_PASSIVE_B = 1 << 1; + + /** + * Flag for use with {@link #setDiscoveryTechnology(Activity, int, int)}. + * <p> + * Setting this flag enables listening for Nfc-F technology. + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH) + public static final int FLAG_LISTEN_NFC_PASSIVE_F = 1 << 2; + + /** + * Flags for use with {@link #setDiscoveryTechnology(Activity, int, int)}. + * <p> + * Setting this flag disables listening. + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH) + public static final int FLAG_LISTEN_DISABLE = 0x0; + + /** + * Flags for use with {@link #setDiscoveryTechnology(Activity, int, int)}. + * <p> + * Setting this flag disables polling. + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH) + public static final int FLAG_READER_DISABLE = 0x0; + + /** + * Flags for use with {@link #setDiscoveryTechnology(Activity, int, int)}. + * <p> + * Setting this flag makes listening to use current flags. + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH) + public static final int FLAG_LISTEN_KEEP = -1; + + /** + * Flags for use with {@link #setDiscoveryTechnology(Activity, int, int)}. + * <p> + * Setting this flag makes polling to use current flags. + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH) + public static final int FLAG_READER_KEEP = -1; + + /** @hide */ + public static final int FLAG_USE_ALL_TECH = 0xff; + + /** @hide */ + @IntDef(flag = true, prefix = {"FLAG_LISTEN_"}, value = { + FLAG_LISTEN_KEEP, + FLAG_LISTEN_DISABLE, + FLAG_LISTEN_NFC_PASSIVE_A, + FLAG_LISTEN_NFC_PASSIVE_B, + FLAG_LISTEN_NFC_PASSIVE_F + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ListenTechnology {} + + /** * @hide * @removed */ @@ -437,12 +520,14 @@ public final class NfcAdapter { @Retention(RetentionPolicy.SOURCE) public @interface TagIntentAppPreferenceResult {} - // Guarded by NfcAdapter.class + // Guarded by sLock static boolean sIsInitialized = false; static boolean sHasNfcFeature; static boolean sHasCeFeature; static boolean sHasNfcWlcFeature; + static Object sLock = new Object(); + // Final after first constructor, except for // attemptDeadServiceRecovery() when NFC crashes - we accept a best effort // recovery @@ -1235,7 +1320,7 @@ public final class NfcAdapter { @java.lang.Deprecated @UnsupportedAppUsage public void setBeamPushUris(Uri[] uris, Activity activity) { - synchronized (NfcAdapter.class) { + synchronized (sLock) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } @@ -1305,7 +1390,7 @@ public final class NfcAdapter { @java.lang.Deprecated @UnsupportedAppUsage public void setBeamPushUrisCallback(CreateBeamUrisCallback callback, Activity activity) { - synchronized (NfcAdapter.class) { + synchronized (sLock) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } @@ -1390,7 +1475,7 @@ public final class NfcAdapter { @UnsupportedAppUsage public void setNdefPushMessage(NdefMessage message, Activity activity, Activity ... activities) { - synchronized (NfcAdapter.class) { + synchronized (sLock) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } @@ -1404,7 +1489,7 @@ public final class NfcAdapter { @SystemApi @UnsupportedAppUsage public void setNdefPushMessage(NdefMessage message, Activity activity, int flags) { - synchronized (NfcAdapter.class) { + synchronized (sLock) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } @@ -1483,7 +1568,7 @@ public final class NfcAdapter { @UnsupportedAppUsage public void setNdefPushMessageCallback(CreateNdefMessageCallback callback, Activity activity, Activity ... activities) { - synchronized (NfcAdapter.class) { + synchronized (sLock) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } @@ -1534,7 +1619,7 @@ public final class NfcAdapter { @UnsupportedAppUsage public void setOnNdefPushCompleteCallback(OnNdefPushCompleteCallback callback, Activity activity, Activity ... activities) { - synchronized (NfcAdapter.class) { + synchronized (sLock) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } @@ -1577,7 +1662,7 @@ public final class NfcAdapter { */ public void enableForegroundDispatch(Activity activity, PendingIntent intent, IntentFilter[] filters, String[][] techLists) { - synchronized (NfcAdapter.class) { + synchronized (sLock) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } @@ -1612,7 +1697,7 @@ public final class NfcAdapter { * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. */ public void disableForegroundDispatch(Activity activity) { - synchronized (NfcAdapter.class) { + synchronized (sLock) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } @@ -1648,7 +1733,7 @@ public final class NfcAdapter { */ public void enableReaderMode(Activity activity, ReaderCallback callback, int flags, Bundle extras) { - synchronized (NfcAdapter.class) { + synchronized (sLock) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } @@ -1665,7 +1750,7 @@ public final class NfcAdapter { * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. */ public void disableReaderMode(Activity activity) { - synchronized (NfcAdapter.class) { + synchronized (sLock) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } @@ -1693,7 +1778,7 @@ public final class NfcAdapter { @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @SuppressLint("VisiblySynchronized") public void setReaderMode(boolean enablePolling) { - synchronized (NfcAdapter.class) { + synchronized (sLock) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } @@ -1708,6 +1793,80 @@ public final class NfcAdapter { } /** + * Set the NFC controller to enable specific poll/listen technologies, + * as specified in parameters, while this Activity is in the foreground. + * + * Use {@link #FLAG_READER_KEEP} to keep current polling technology. + * Use {@link #FLAG_LISTEN_KEEP} to keep current listenig technology. + * Use {@link #FLAG_READER_DISABLE} to disable polling. + * Use {@link #FLAG_LISTEN_DISABLE} to disable listening. + * Also refer to {@link #resetDiscoveryTechnology(Activity)} to restore these changes. + * </p> + * The pollTech, listenTech parameters can be one or several of below list. + * <pre> + * Poll Listen + * Passive A 0x01 (NFC_A) 0x01 (NFC_PASSIVE_A) + * Passive B 0x02 (NFC_B) 0x02 (NFC_PASSIVE_B) + * Passive F 0x04 (NFC_F) 0x04 (NFC_PASSIVE_F) + * ISO 15693 0x08 (NFC_V) - + * Kovio 0x10 (NFC_BARCODE) - + * </pre> + * <p>Example usage in an Activity that requires to disable poll, + * keep current listen technologies: + * <pre> + * protected void onResume() { + * mNfcAdapter = NfcAdapter.getDefaultAdapter(getApplicationContext()); + * mNfcAdapter.setDiscoveryTechnology(this, + * NfcAdapter.FLAG_READER_DISABLE, NfcAdapter.FLAG_LISTEN_KEEP); + * }</pre></p> + * @param activity The Activity that requests NFC controller to enable specific technologies. + * @param pollTech Flags indicating poll technologies. + * @param listenTech Flags indicating listen technologies. + * @throws UnsupportedOperationException if FEATURE_NFC, + * FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF are unavailable. + */ + + @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH) + public void setDiscoveryTechnology(@NonNull Activity activity, + @PollTechnology int pollTech, @ListenTechnology int listenTech) { + if (listenTech == FLAG_LISTEN_DISABLE) { + synchronized (sLock) { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + } + mNfcActivityManager.enableReaderMode(activity, null, pollTech, null); + return; + } + if (pollTech == FLAG_READER_DISABLE) { + synchronized (sLock) { + if (!sHasCeFeature) { + throw new UnsupportedOperationException(); + } + } + } else { + synchronized (sLock) { + if (!sHasNfcFeature || !sHasCeFeature) { + throw new UnsupportedOperationException(); + } + } + } + mNfcActivityManager.setDiscoveryTech(activity, pollTech, listenTech); + } + + /** + * Restore the poll/listen technologies of NFC controller, + * which were changed by {@link #setDiscoveryTechnology(Activity , int , int)} + * + * @param activity The Activity that requests to changed technologies. + */ + + @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH) + public void resetDiscoveryTechnology(@NonNull Activity activity) { + mNfcActivityManager.resetDiscoveryTech(activity); + } + + /** * Manually invoke Android Beam to share data. * * <p>The Android Beam animation is normally only shown when two NFC-capable @@ -1737,7 +1896,7 @@ public final class NfcAdapter { @java.lang.Deprecated @UnsupportedAppUsage public boolean invokeBeam(Activity activity) { - synchronized (NfcAdapter.class) { + synchronized (sLock) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } @@ -1775,7 +1934,7 @@ public final class NfcAdapter { @Deprecated @UnsupportedAppUsage public void enableForegroundNdefPush(Activity activity, NdefMessage message) { - synchronized (NfcAdapter.class) { + synchronized (sLock) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } @@ -1805,7 +1964,7 @@ public final class NfcAdapter { @Deprecated @UnsupportedAppUsage public void disableForegroundNdefPush(Activity activity) { - synchronized (NfcAdapter.class) { + synchronized (sLock) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } @@ -2085,7 +2244,7 @@ public final class NfcAdapter { @java.lang.Deprecated @UnsupportedAppUsage public boolean isNdefPushEnabled() { - synchronized (NfcAdapter.class) { + synchronized (sLock) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } @@ -2199,7 +2358,7 @@ public final class NfcAdapter { @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean addNfcUnlockHandler(final NfcUnlockHandler unlockHandler, String[] tagTechnologies) { - synchronized (NfcAdapter.class) { + synchronized (sLock) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } @@ -2248,7 +2407,7 @@ public final class NfcAdapter { @SystemApi @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean removeNfcUnlockHandler(NfcUnlockHandler unlockHandler) { - synchronized (NfcAdapter.class) { + synchronized (sLock) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } diff --git a/core/java/android/nfc/cardemulation/CardEmulation.java b/core/java/android/nfc/cardemulation/CardEmulation.java index 58b6179691e9..ad86d70db967 100644 --- a/core/java/android/nfc/cardemulation/CardEmulation.java +++ b/core/java/android/nfc/cardemulation/CardEmulation.java @@ -998,6 +998,87 @@ public final class CardEmulation { } } + /** + * Setting NFC controller routing table, which includes Protocol Route and Technology Route, + * while this Activity is in the foreground. + * + * The parameter set to null can be used to keep current values for that entry. + * <p> + * Example usage in an Activity that requires to set proto route to "ESE" and keep tech route: + * <pre> + * protected void onResume() { + * mNfcAdapter.overrideRoutingTable(this , "ESE" , null); + * }</pre> + * </p> + * Also activities must call this method when it goes to the background, + * with all parameters set to null. + * @param activity The Activity that requests NFC controller routing table to be changed. + * @param protocol ISO-DEP route destination, which can be "DH" or "UICC" or "ESE". + * @param technology Tech-A, Tech-B route destination, which can be "DH" or "UICC" or "ESE". + * @return true if operation is successful and false otherwise + * + * This is a high risk API and only included to support mainline effort + * @hide + */ + public boolean overrideRoutingTable(Activity activity, String protocol, String technology) { + if (activity == null) { + throw new NullPointerException("activity or service or category is null"); + } + if (!activity.isResumed()) { + throw new IllegalArgumentException("Activity must be resumed."); + } + try { + return sService.overrideRoutingTable(UserHandle.myUserId(), protocol, technology); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.overrideRoutingTable(UserHandle.myUserId(), protocol, technology); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + /** + * Restore the NFC controller routing table, + * which was changed by {@link #overrideRoutingTable(Activity, String, String)} + * + * @param activity The Activity that requested NFC controller routing table to be changed. + * @return true if operation is successful and false otherwise + * + * @hide + */ + public boolean recoverRoutingTable(Activity activity) { + if (activity == null) { + throw new NullPointerException("activity is null"); + } + if (!activity.isResumed()) { + throw new IllegalArgumentException("Activity must be resumed."); + } + try { + return sService.recoverRoutingTable(UserHandle.myUserId()); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.recoverRoutingTable(UserHandle.myUserId()); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + void recoverService() { NfcAdapter adapter = NfcAdapter.getDefaultAdapter(mContext); sService = adapter.getCardEmulationService(); diff --git a/core/java/android/nfc/flags.aconfig b/core/java/android/nfc/flags.aconfig index ce4f77725ef1..01a45708fddf 100644 --- a/core/java/android/nfc/flags.aconfig +++ b/core/java/android/nfc/flags.aconfig @@ -62,3 +62,10 @@ flag { description: "Flag for NFC charging changes" bug: "292143899" } + +flag { + name: "enable_nfc_set_discovery_tech" + namespace: "nfc" + description: "Flag for NFC set discovery tech API" + bug: "300351519" +} diff --git a/core/java/android/os/GraphicsEnvironment.java b/core/java/android/os/GraphicsEnvironment.java index 8fcff78fb025..3149de4c39e7 100644 --- a/core/java/android/os/GraphicsEnvironment.java +++ b/core/java/android/os/GraphicsEnvironment.java @@ -673,6 +673,7 @@ public class GraphicsEnvironment { if (anglePkg.isEmpty()) { return; } + intent.setPackage(anglePkg); context.sendOrderedBroadcast(intent, null, new BroadcastReceiver() { @Override diff --git a/core/java/android/os/HwNoService.java b/core/java/android/os/HwNoService.java index 117c3ad7ee48..084031496629 100644 --- a/core/java/android/os/HwNoService.java +++ b/core/java/android/os/HwNoService.java @@ -16,37 +16,127 @@ package android.os; +import android.hidl.manager.V1_2.IServiceManager; +import android.util.Log; + +import java.util.ArrayList; + /** * A fake hwservicemanager that is used locally when HIDL isn't supported on the device. * * @hide */ -final class HwNoService implements IHwBinder, IHwInterface { +final class HwNoService extends IServiceManager.Stub implements IHwBinder, IHwInterface { + private static final String TAG = "HwNoService"; + /** @hide */ @Override - public void transact(int code, HwParcel request, HwParcel reply, int flags) {} + public String toString() { + return "[HwNoService]"; + } - /** @hide */ @Override - public IHwInterface queryLocalInterface(String descriptor) { - return new HwNoService(); + public android.hidl.base.V1_0.IBase get(String fqName, String name) + throws android.os.RemoteException { + Log.i(TAG, "get " + fqName + "/" + name + " with no hwservicemanager"); + return null; } - /** @hide */ @Override - public boolean linkToDeath(DeathRecipient recipient, long cookie) { + public boolean add(String name, android.hidl.base.V1_0.IBase service) + throws android.os.RemoteException { + Log.i(TAG, "get " + name + " with no hwservicemanager"); + return false; + } + + @Override + public byte getTransport(String fqName, String name) throws android.os.RemoteException { + Log.i(TAG, "getTransoport " + fqName + "/" + name + " with no hwservicemanager"); + return 0x0; + } + + @Override + public java.util.ArrayList<String> list() throws android.os.RemoteException { + Log.i(TAG, "list with no hwservicemanager"); + return new ArrayList<String>(); + } + + @Override + public java.util.ArrayList<String> listByInterface(String fqName) + throws android.os.RemoteException { + Log.i(TAG, "listByInterface with no hwservicemanager"); + return new ArrayList<String>(); + } + + @Override + public boolean registerForNotifications( + String fqName, String name, android.hidl.manager.V1_0.IServiceNotification callback) + throws android.os.RemoteException { + Log.i(TAG, "registerForNotifications with no hwservicemanager"); return true; } - /** @hide */ @Override - public boolean unlinkToDeath(DeathRecipient recipient) { + public ArrayList<android.hidl.manager.V1_0.IServiceManager.InstanceDebugInfo> debugDump() + throws android.os.RemoteException { + Log.i(TAG, "debugDump with no hwservicemanager"); + return new ArrayList<android.hidl.manager.V1_0.IServiceManager.InstanceDebugInfo>(); + } + + @Override + public void registerPassthroughClient(String fqName, String name) + throws android.os.RemoteException { + Log.i(TAG, "registerPassthroughClient with no hwservicemanager"); + } + + @Override + public boolean unregisterForNotifications( + String fqName, String name, android.hidl.manager.V1_0.IServiceNotification callback) + throws android.os.RemoteException { + Log.i(TAG, "unregisterForNotifications with no hwservicemanager"); return true; } - /** @hide */ @Override - public IHwBinder asBinder() { - return this; + public boolean registerClientCallback( + String fqName, + String name, + android.hidl.base.V1_0.IBase server, + android.hidl.manager.V1_2.IClientCallback cb) + throws android.os.RemoteException { + Log.i( + TAG, + "registerClientCallback for " + fqName + "/" + name + " with no hwservicemanager"); + return true; + } + + @Override + public boolean unregisterClientCallback( + android.hidl.base.V1_0.IBase server, android.hidl.manager.V1_2.IClientCallback cb) + throws android.os.RemoteException { + Log.i(TAG, "unregisterClientCallback with no hwservicemanager"); + return true; + } + + @Override + public boolean addWithChain( + String name, android.hidl.base.V1_0.IBase service, java.util.ArrayList<String> chain) + throws android.os.RemoteException { + Log.i(TAG, "addWithChain with no hwservicemanager"); + return true; + } + + @Override + public java.util.ArrayList<String> listManifestByInterface(String fqName) + throws android.os.RemoteException { + Log.i(TAG, "listManifestByInterface for " + fqName + " with no hwservicemanager"); + return new ArrayList<String>(); + } + + @Override + public boolean tryUnregister(String fqName, String name, android.hidl.base.V1_0.IBase service) + throws android.os.RemoteException { + Log.i(TAG, "tryUnregister for " + fqName + "/" + name + " with no hwservicemanager"); + return true; } } diff --git a/core/java/android/os/StrictMode.java b/core/java/android/os/StrictMode.java index e32a8f32ce95..8c8af0ead227 100644 --- a/core/java/android/os/StrictMode.java +++ b/core/java/android/os/StrictMode.java @@ -2373,13 +2373,29 @@ public final class StrictMode { /** Assume locked until we hear otherwise */ private static volatile boolean sCeStorageUnlocked = false; + /** + * Avoid (potentially) costly and repeated lookups to the same mount service. + * Note that we don't use the Singleton wrapper as lookup may fail early during boot. + */ + private static volatile IStorageManager sStorageManager; + private static boolean isCeStorageUnlocked(int userId) { - final IStorageManager storage = IStorageManager.Stub + IStorageManager storage = sStorageManager; + if (storage == null) { + storage = IStorageManager.Stub .asInterface(ServiceManager.getService("mount")); + // As the queried handle may be null early during boot, only stash valid handles, + // avoiding races with concurrent service queries. + if (storage != null) { + sStorageManager = storage; + } + } if (storage != null) { try { return storage.isCeStorageUnlocked(userId); } catch (RemoteException ignored) { + // Conservatively clear the ref, allowing refresh if the remote process restarts. + sStorageManager = null; } } return false; diff --git a/core/java/android/os/UpdateEngineStable.java b/core/java/android/os/UpdateEngineStable.java new file mode 100644 index 000000000000..9e2593e39e0e --- /dev/null +++ b/core/java/android/os/UpdateEngineStable.java @@ -0,0 +1,192 @@ +/* + * 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.os; + +import android.annotation.IntDef; + +/** + * UpdateEngineStable handles calls to the update engine stalbe which takes care of A/B OTA updates. + * This interface has lesser functionalities than UpdateEngine and doesn't allow cancel. + * + * <p>The minimal flow is: + * + * <ol> + * <li>Create a new UpdateEngineStable instance. + * <li>Call {@link #bind}, provide callback function. + * <li>Call {@link #applyPayloadFd}. + * </ol> + * + * The APIs defined in this class and UpdateEngineStableCallback class must be in sync with the ones + * in {@code system/update_engine/stable/android/os/IUpdateEngineStable.aidl} and {@code + * ssystem/update_engine/stable/android/os/IUpdateEngineStableCallback.aidl}. + * + * @hide + */ +public class UpdateEngineStable { + private static final String TAG = "UpdateEngineStable"; + + private static final String UPDATE_ENGINE_STABLE_SERVICE = + "android.os.UpdateEngineStableService"; + + /** + * Error codes from update engine upon finishing a call to {@link applyPayloadFd}. Values will + * be passed via the callback function {@link + * UpdateEngineStableCallback#onPayloadApplicationComplete}. Values must agree with the ones in + * {@code system/update_engine/common/error_code.h}. + */ + /** @hide */ + @IntDef( + value = { + UpdateEngine.ErrorCodeConstants.SUCCESS, + UpdateEngine.ErrorCodeConstants.ERROR, + UpdateEngine.ErrorCodeConstants.FILESYSTEM_COPIER_ERROR, + UpdateEngine.ErrorCodeConstants.POST_INSTALL_RUNNER_ERROR, + UpdateEngine.ErrorCodeConstants.PAYLOAD_MISMATCHED_TYPE_ERROR, + UpdateEngine.ErrorCodeConstants.INSTALL_DEVICE_OPEN_ERROR, + UpdateEngine.ErrorCodeConstants.KERNEL_DEVICE_OPEN_ERROR, + UpdateEngine.ErrorCodeConstants.DOWNLOAD_TRANSFER_ERROR, + UpdateEngine.ErrorCodeConstants.PAYLOAD_HASH_MISMATCH_ERROR, + UpdateEngine.ErrorCodeConstants.PAYLOAD_SIZE_MISMATCH_ERROR, + UpdateEngine.ErrorCodeConstants.DOWNLOAD_PAYLOAD_VERIFICATION_ERROR, + UpdateEngine.ErrorCodeConstants.PAYLOAD_TIMESTAMP_ERROR, + UpdateEngine.ErrorCodeConstants.UPDATED_BUT_NOT_ACTIVE, + UpdateEngine.ErrorCodeConstants.NOT_ENOUGH_SPACE, + UpdateEngine.ErrorCodeConstants.DEVICE_CORRUPTED, + }) + public @interface ErrorCode {} + + private final IUpdateEngineStable mUpdateEngineStable; + private IUpdateEngineStableCallback mUpdateEngineStableCallback = null; + private final Object mUpdateEngineStableCallbackLock = new Object(); + + /** + * Creates a new instance. + * + * @hide + */ + public UpdateEngineStable() { + mUpdateEngineStable = + IUpdateEngineStable.Stub.asInterface( + ServiceManager.getService(UPDATE_ENGINE_STABLE_SERVICE)); + if (mUpdateEngineStable == null) { + throw new IllegalStateException("Failed to find " + UPDATE_ENGINE_STABLE_SERVICE); + } + } + + /** + * Prepares this instance for use. The callback will be notified on any status change, and when + * the update completes. A handler can be supplied to control which thread runs the callback, or + * null. + * + * @hide + */ + public boolean bind(final UpdateEngineStableCallback callback, final Handler handler) { + synchronized (mUpdateEngineStableCallbackLock) { + mUpdateEngineStableCallback = + new IUpdateEngineStableCallback.Stub() { + @Override + public void onStatusUpdate(final int status, final float percent) { + if (handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + callback.onStatusUpdate(status, percent); + } + }); + } else { + callback.onStatusUpdate(status, percent); + } + } + + @Override + public void onPayloadApplicationComplete(final int errorCode) { + if (handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + callback.onPayloadApplicationComplete(errorCode); + } + }); + } else { + callback.onPayloadApplicationComplete(errorCode); + } + } + + @Override + public int getInterfaceVersion() { + return super.VERSION; + } + + @Override + public String getInterfaceHash() { + return super.HASH; + } + }; + + try { + return mUpdateEngineStable.bind(mUpdateEngineStableCallback); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Equivalent to {@code bind(callback, null)}. + * + * @hide + */ + public boolean bind(final UpdateEngineStableCallback callback) { + return bind(callback, null); + } + + /** + * Applies payload from given ParcelFileDescriptor. Usage is same as UpdateEngine#applyPayload + * + * @hide + */ + public void applyPayloadFd( + ParcelFileDescriptor fd, long offset, long size, String[] headerKeyValuePairs) { + try { + mUpdateEngineStable.applyPayloadFd(fd, offset, size, headerKeyValuePairs); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Unbinds the last bound callback function. + * + * @hide + */ + public boolean unbind() { + synchronized (mUpdateEngineStableCallbackLock) { + if (mUpdateEngineStableCallback == null) { + return true; + } + try { + boolean result = mUpdateEngineStable.unbind(mUpdateEngineStableCallback); + mUpdateEngineStableCallback = null; + return result; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } +} diff --git a/core/java/android/os/UpdateEngineStableCallback.java b/core/java/android/os/UpdateEngineStableCallback.java new file mode 100644 index 000000000000..4bcfb4baae95 --- /dev/null +++ b/core/java/android/os/UpdateEngineStableCallback.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 android.os; + +/** + * Callback function for UpdateEngineStable. Used to keep the caller up to date with progress, so + * the UI (if any) can be updated. + * + * <p>The APIs defined in this class and UpdateEngineStable class must be in sync with the ones in + * system/update_engine/stable/android/os/IUpdateEngineStable.aidl and + * system/update_engine/stable/android/os/IUpdateEngineStableCallback.aidl. + * + * <p>{@hide} + */ +public abstract class UpdateEngineStableCallback { + + /** + * Invoked when anything changes. The value of {@code status} will be one of the values from + * {@link UpdateEngine.UpdateStatusConstants}, and {@code percent} will be valid + * + * @hide + */ + public abstract void onStatusUpdate(int status, float percent); + + /** + * Invoked when the payload has been applied, whether successfully or unsuccessfully. The value + * of {@code errorCode} will be one of the values from {@link UpdateEngine.ErrorCodeConstants}. + * + * @hide + */ + public abstract void onPayloadApplicationComplete(@UpdateEngineStable.ErrorCode int errorCode); +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 54cc5f471c33..84fddcb2c22a 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7428,6 +7428,20 @@ public final class Settings { public static final String DEFAULT_INPUT_METHOD = "default_input_method"; /** + * Used only by {@link com.android.server.inputmethod.InputMethodManagerService} as a + * temporary data store of {@link #DEFAULT_INPUT_METHOD} while a virtual-device-specific + * input method is set as default.</p> + * + * <p>This should be considered to be an implementation detail of + * {@link com.android.server.inputmethod.InputMethodManagerService}. Other system + * components should never rely on this value.</p> + * + * @see #DEFAULT_INPUT_METHOD + * @hide + */ + public static final String DEFAULT_DEVICE_INPUT_METHOD = "default_device_input_method"; + + /** * Setting to record the input method subtype used by default, holding the ID * of the desired method. */ @@ -14347,6 +14361,19 @@ public final class Settings { "mute_alarm_stream_with_ringer_mode"; /** + * The user's choice for whether or not Alarm stream should always be muted with Ringer. + * + * <p>Note that this is different from {@link #MUTE_ALARM_STREAM_WITH_RINGER_MODE}, which + * controls the real state of whether or not the Alarm stream and Ringer association occurs. + * The two Settings are not necessarily equal, if the final decision for the association + * depends on factors beyond the user's preference. + * + * @hide + */ + public static final String MUTE_ALARM_STREAM_WITH_RINGER_MODE_USER_PREFERENCE = + "mute_alarm_stream_with_ringer_mode_user_preference"; + + /** * Overlay display devices setting. * The associated value is a specially formatted string that describes the * size and density of simulated secondary display devices. diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java index e4af2da57f40..d47ff2e2cd99 100644 --- a/core/java/android/provider/Telephony.java +++ b/core/java/android/provider/Telephony.java @@ -368,11 +368,13 @@ public final class Telephony { * <p> * As of Android 11 apps will need specific permission to query other packages. To use * this method an app must include in their AndroidManifest: + * <pre>{@code * <queries> * <intent> * <action android:name="android.provider.Telephony.SMS_DELIVER"/> * </intent> * </queries> + * }</pre> * Which will allow them to query packages which declare intent filters that include * the {@link android.provider.Telephony.Sms.Intents#SMS_DELIVER_ACTION} intent. * </p> diff --git a/core/java/android/security/responsible_apis_flags.aconfig b/core/java/android/security/responsible_apis_flags.aconfig index 4e5588cce1c9..fe6c4a4321e9 100644 --- a/core/java/android/security/responsible_apis_flags.aconfig +++ b/core/java/android/security/responsible_apis_flags.aconfig @@ -20,3 +20,10 @@ flag { description: "Enables toasts when ASM restrictions are triggered" bug: "230590090" } + +flag { + name: "content_uri_permission_apis" + namespace: "responsible_apis" + description: "Enables the content URI permission APIs" + bug: "293467489" +} diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 54116a2749e0..1a2be15b2e8d 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -26,6 +26,8 @@ import static android.graphics.Matrix.MSKEW_Y; import static android.view.View.SYSTEM_UI_FLAG_VISIBLE; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; +import static com.android.window.flags.Flags.noConsecutiveVisibilityEvents; + import android.animation.AnimationHandler; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -1431,27 +1433,36 @@ public abstract class WallpaperService extends Service { } if (didSurface && !mReportedVisible) { - // This wallpaper is currently invisible, but its - // surface has changed. At this point let's tell it - // again that it is invisible in case the report about - // the surface caused it to start running. We really - // don't want wallpapers running when not visible. if (mIsCreating) { - // Some wallpapers will ignore this call if they - // had previously been told they were invisble, - // so if we are creating a new surface then toggle - // the state to get them to notice. - if (DEBUG) Log.v(TAG, "onVisibilityChanged(true) at surface: " - + this); - Trace.beginSection("WPMS.Engine.onVisibilityChanged-true"); - onVisibilityChanged(true); + // The surface has been created, but the wallpaper isn't visible. + // Trigger onVisibilityChanged(true) then onVisibilityChanged(false) + // to make sure the wallpaper is stopped even after the events + // onSurfaceCreated() and onSurfaceChanged(). + if (noConsecutiveVisibilityEvents()) { + if (DEBUG) Log.v(TAG, "toggling onVisibilityChanged"); + Trace.beginSection("WPMS.Engine.onVisibilityChanged-true"); + onVisibilityChanged(true); + Trace.endSection(); + Trace.beginSection("WPMS.Engine.onVisibilityChanged-false"); + onVisibilityChanged(false); + Trace.endSection(); + } else { + if (DEBUG) { + Log.v(TAG, "onVisibilityChanged(true) at surface: " + this); + } + Trace.beginSection("WPMS.Engine.onVisibilityChanged-true"); + onVisibilityChanged(true); + Trace.endSection(); + } + } + if (!noConsecutiveVisibilityEvents()) { + if (DEBUG) { + Log.v(TAG, "onVisibilityChanged(false) at surface: " + this); + } + Trace.beginSection("WPMS.Engine.onVisibilityChanged-false"); + onVisibilityChanged(false); Trace.endSection(); } - if (DEBUG) Log.v(TAG, "onVisibilityChanged(false) at surface: " - + this); - Trace.beginSection("WPMS.Engine.onVisibilityChanged-false"); - onVisibilityChanged(false); - Trace.endSection(); } } finally { mIsCreating = false; diff --git a/core/java/android/view/InputEventReceiver.java b/core/java/android/view/InputEventReceiver.java index 19e6836ed261..9c430cd4acb4 100644 --- a/core/java/android/view/InputEventReceiver.java +++ b/core/java/android/view/InputEventReceiver.java @@ -54,6 +54,7 @@ public abstract class InputEventReceiver { InputChannel inputChannel, MessageQueue messageQueue); private static native void nativeDispose(long receiverPtr); private static native void nativeFinishInputEvent(long receiverPtr, int seq, boolean handled); + private static native boolean nativeProbablyHasInput(long receiverPtr); private static native void nativeReportTimeline(long receiverPtr, int inputEventId, long gpuCompletedTime, long presentTime); private static native boolean nativeConsumeBatchedInputEvents(long receiverPtr, @@ -92,6 +93,17 @@ public abstract class InputEventReceiver { } /** + * Checks the receiver for input availability. + * May return false negatives. + */ + public boolean probablyHasInput() { + if (mReceiverPtr == 0) { + return false; + } + return nativeProbablyHasInput(mReceiverPtr); + } + + /** * Disposes the receiver. * Must be called on the same Looper thread to which the receiver is attached. */ diff --git a/core/java/android/view/Surface.java b/core/java/android/view/Surface.java index ad0bf7c95c70..785055441d59 100644 --- a/core/java/android/view/Surface.java +++ b/core/java/android/view/Surface.java @@ -274,7 +274,8 @@ public class Surface implements Parcelable { @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = {"FRAME_RATE_CATEGORY_"}, value = {FRAME_RATE_CATEGORY_DEFAULT, FRAME_RATE_CATEGORY_NO_PREFERENCE, - FRAME_RATE_CATEGORY_LOW, FRAME_RATE_CATEGORY_NORMAL, FRAME_RATE_CATEGORY_HIGH}) + FRAME_RATE_CATEGORY_LOW, FRAME_RATE_CATEGORY_NORMAL, + FRAME_RATE_CATEGORY_HIGH_HINT, FRAME_RATE_CATEGORY_HIGH}) public @interface FrameRateCategory {} // From native_window.h or window.h. Keep these in sync. @@ -308,11 +309,21 @@ public class Surface implements Parcelable { public static final int FRAME_RATE_CATEGORY_NORMAL = 3; /** + * Hints that, as a result of a user interaction, an animation is likely to start. + * This category is a signal that a user interaction heuristic determined the need of a + * high refresh rate, and is not an explicit request from the app. + * As opposed to {@link #FRAME_RATE_CATEGORY_HIGH}, this vote may be ignored in favor of + * more explicit votes. + * @hide + */ + public static final int FRAME_RATE_CATEGORY_HIGH_HINT = 4; + + /** * Indicates a frame rate suitable for animations that require a high frame rate, which may * increase smoothness but may also increase power usage. * @hide */ - public static final int FRAME_RATE_CATEGORY_HIGH = 4; + public static final int FRAME_RATE_CATEGORY_HIGH = 5; /** * Create an empty surface, which will later be filled in by readFromParcel(). diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 442ea661585c..2b99e1e9f79b 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -32,6 +32,7 @@ import static android.view.displayhash.DisplayHashResultCallback.EXTRA_DISPLAY_H import static android.view.displayhash.DisplayHashResultCallback.EXTRA_DISPLAY_HASH_ERROR_CODE; import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY; import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API; +import static android.view.flags.Flags.enableUseMeasureCacheDuringForceLayout; import static android.view.flags.Flags.toolkitMetricsForFrameRateDecision; import static android.view.flags.Flags.toolkitSetFrameRateReadOnly; import static android.view.flags.Flags.viewVelocityApi; @@ -955,6 +956,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private static boolean sAlwaysRemeasureExactly = false; /** + * When true makes it possible to use onMeasure caches also when the force layout flag is + * enabled. This helps avoiding multiple measures in the same frame with the same dimensions. + */ + private static boolean sUseMeasureCacheDuringForceLayoutFlagValue; + + /** * Allow setForeground/setBackground to be called (and ignored) on a textureview, * without throwing */ @@ -2396,6 +2403,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, sToolkitSetFrameRateReadOnlyFlagValue = toolkitSetFrameRateReadOnly(); sToolkitMetricsForFrameRateDecisionFlagValue = toolkitMetricsForFrameRateDecision(); + sUseMeasureCacheDuringForceLayoutFlagValue = enableUseMeasureCacheDuringForceLayout(); } /** @@ -22848,6 +22856,36 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Determines whether an unprocessed input event is available on the window. + * + * This is only a performance hint (a.k.a. the Input Hint) and may return false negative + * results. Callers should not rely on availability of the input event based on the return + * value of this method. + * + * The Input Hint functionality is experimental, and can be removed in the future OS releases. + * + * This method only returns nontrivial results on a View that is attached to a Window. Such View + * can be acquired using `Activity.getWindow().getDecorView()`, and only after the view + * hierarchy is attached (via {@link android.app.Activity#setContentView(android.view.View)}). + * + * In multi-window mode the View can provide the Input Hint only for the window it is attached + * to. Therefore, checking input availability for the whole application would require asking + * for the hint from more than one View. + * + * The initial implementation does not return false positives, but callers should not rely on + * it: false positives may occur in future OS releases. + * + * @hide + */ + public boolean probablyHasInput() { + ViewRootImpl viewRootImpl = getViewRootImpl(); + if (viewRootImpl == null) { + return false; + } + return viewRootImpl.probablyHasInput(); + } + + /** * Destroys all hardware rendering resources. This method is invoked * when the system needs to reclaim resources. Upon execution of this * method, you should free any OpenGL resources created by the view. @@ -27417,7 +27455,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, resolveRtlPropertiesIfNeeded(); - int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key); + int cacheIndex; + if (sUseMeasureCacheDuringForceLayoutFlagValue) { + cacheIndex = mMeasureCache.indexOfKey(key); + } else { + cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key); + } + if (cacheIndex < 0 || sIgnoreMeasureCache) { if (isTraversalTracingEnabled()) { Trace.beginSection(mTracingStrings.onMeasure); diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 287c7b29813f..fbefbf31a9f0 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -49,6 +49,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Parcelable; import android.os.SystemClock; +import android.service.autofill.Flags; import android.util.AttributeSet; import android.util.IntArray; import android.util.Log; @@ -3752,7 +3753,16 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager && !child.isActivityDeniedForAutofillForUnimportantView()) || (shouldIncludeAllChildrenViewWithAutofillTypeNotNone(afm) && child.getAutofillType() != AUTOFILL_TYPE_NONE) - || shouldIncludeAllChildrenViews(afm)){ + || shouldIncludeAllChildrenViews(afm) + || (Flags.includeInvisibleViewGroupInAssistStructure() + && child instanceof ViewGroup && child.getVisibility() != View.VISIBLE)) { + // If the child is a ViewGroup object and its visibility is not visible, include + // it as part of the assist structure. The children of these invisible ViewGroup + // objects are parsed and included in the assist structure. When the Autofill + // Provider determines the visibility of these children, it looks at their + // visibility as well as their parent's visibility. Omitting invisible parents + // will lead to the Autofill Provider incorrectly assuming that these children + // of invisible parents are actually visible. list.add(child); } else if (child instanceof ViewGroup) { ((ViewGroup) child).populateChildrenForAutofill(list, flags); diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 32afe065857d..8529b4e044fa 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -10554,6 +10554,18 @@ public final class ViewRootImpl implements ViewParent, } /** + * Checks the input event receiver for input availability. + * May return false negatives. + * @hide + */ + public boolean probablyHasInput() { + if (mInputEventReceiver == null) { + return false; + } + return mInputEventReceiver.probablyHasInput(); + } + + /** * Adds a scroll capture callback to this window. * * @param callback the callback to add diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index f76822f14189..61cf1266177a 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -972,10 +972,8 @@ public interface WindowManager extends ViewManager { * android:value="false"/> * </application> * </pre> - * - * @hide */ - // TODO(b/274924641): Make this public API. + @FlaggedApi(Flags.FLAG_APP_COMPAT_PROPERTIES_API) String PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED = "android.window.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED"; @@ -1309,9 +1307,8 @@ public interface WindowManager extends ViewManager { * android:value="true|false"/> * </application> * </pre> - * @hide */ - // TODO(b/280052089): Make this public API. + @FlaggedApi(Flags.FLAG_APP_COMPAT_PROPERTIES_API) String PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES = "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES"; diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index 0cc19fb70fbc..48f8f1bbe367 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -24,6 +24,13 @@ flag { } flag { + namespace: "accessibility" + name: "braille_display_hid" + description: "Enables new APIs for an AccessibilityService to communicate with a HID Braille display" + bug: "303522222" +} + +flag { name: "cleanup_accessibility_warning_dialog" namespace: "accessibility" description: "Cleans up duplicated or broken logic surrounding the accessibility warning dialog." diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig new file mode 100644 index 000000000000..a74b06a491e8 --- /dev/null +++ b/core/java/android/view/flags/view_flags.aconfig @@ -0,0 +1,10 @@ +package: "android.view.flags" + +flag { + name: "enable_use_measure_cache_during_force_layout" + namespace: "toolkit" + description: "Enables using the measure cache during a view force layout from the second " + "onMeasure call onwards during the same traversal." + bug: "316170253" + is_fixed_read_only: true +} diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig index dc6aa6cdc048..bb7677d6a571 100644 --- a/core/java/android/view/inputmethod/flags.aconfig +++ b/core/java/android/view/inputmethod/flags.aconfig @@ -38,3 +38,12 @@ flag { description: "Feature flag for supporting stylus handwriting delegation from RemoteViews on the home screen" bug: "279959705" } + +flag { + name: "use_handwriting_listener_for_tooltype" + namespace: "input_method" + description: "Feature flag for using handwriting spy for determining pointer toolType." + bug: "309554999" + is_fixed_read_only: true +} + diff --git a/core/java/android/window/ActivityWindowInfo.aidl b/core/java/android/window/ActivityWindowInfo.aidl new file mode 100644 index 000000000000..d0526bc68fd4 --- /dev/null +++ b/core/java/android/window/ActivityWindowInfo.aidl @@ -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 android.window; + +/** + * Stores information about a particular Activity Window. + * @hide + */ +parcelable ActivityWindowInfo; diff --git a/core/java/android/window/ActivityWindowInfo.java b/core/java/android/window/ActivityWindowInfo.java new file mode 100644 index 000000000000..946bb823398c --- /dev/null +++ b/core/java/android/window/ActivityWindowInfo.java @@ -0,0 +1,147 @@ +/* + * 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.window; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Rect; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Stores the window information about a particular Activity. + * It contains the info that is not part of {@link android.content.res.Configuration}. + * @hide + */ +public final class ActivityWindowInfo implements Parcelable { + + private boolean mIsEmbedded; + + @NonNull + private final Rect mTaskBounds = new Rect(); + + @NonNull + private final Rect mTaskFragmentBounds = new Rect(); + + public ActivityWindowInfo() {} + + public ActivityWindowInfo(@NonNull ActivityWindowInfo info) { + set(info); + } + + /** Copies fields from {@code info}. */ + public void set(@NonNull ActivityWindowInfo info) { + set(info.mIsEmbedded, info.mTaskBounds, info.mTaskFragmentBounds); + } + + /** Sets to the given values. */ + public void set(boolean isEmbedded, @NonNull Rect taskBounds, + @NonNull Rect taskFragmentBounds) { + mIsEmbedded = isEmbedded; + mTaskBounds.set(taskBounds); + mTaskFragmentBounds.set(taskFragmentBounds); + } + + /** + * Whether this activity is embedded, which means it is a TaskFragment that doesn't fill the + * leaf Task. + */ + public boolean isEmbedded() { + return mIsEmbedded; + } + + /** + * The bounds of the leaf Task window in display space. + */ + @NonNull + public Rect getTaskBounds() { + return mTaskBounds; + } + + /** + * The bounds of the leaf TaskFragment window in display space. + * This can be referring to the bounds of the same window as {@link #getTaskBounds()} when + * the activity is not embedded. + */ + @NonNull + public Rect getTaskFragmentBounds() { + return mTaskFragmentBounds; + } + + private ActivityWindowInfo(@NonNull Parcel in) { + mIsEmbedded = in.readBoolean(); + mTaskBounds.readFromParcel(in); + mTaskFragmentBounds.readFromParcel(in); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeBoolean(mIsEmbedded); + mTaskBounds.writeToParcel(dest, flags); + mTaskFragmentBounds.writeToParcel(dest, flags); + } + + @NonNull + public static final Creator<ActivityWindowInfo> CREATOR = + new Creator<>() { + @Override + public ActivityWindowInfo createFromParcel(@NonNull Parcel in) { + return new ActivityWindowInfo(in); + } + + @Override + public ActivityWindowInfo[] newArray(int size) { + return new ActivityWindowInfo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final ActivityWindowInfo other = (ActivityWindowInfo) o; + return mIsEmbedded == other.mIsEmbedded + && mTaskBounds.equals(other.mTaskBounds) + && mTaskFragmentBounds.equals(other.mTaskFragmentBounds); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (mIsEmbedded ? 1 : 0); + result = 31 * result + mTaskBounds.hashCode(); + result = 31 * result + mTaskFragmentBounds.hashCode(); + return result; + } + + @Override + public String toString() { + return "ActivityWindowInfo{isEmbedded=" + mIsEmbedded + + ", taskBounds=" + mTaskBounds + + ", taskFragmentBounds=" + mTaskFragmentBounds + + "}"; + } +} diff --git a/core/java/android/window/TaskFragmentOperation.java b/core/java/android/window/TaskFragmentOperation.java index 0ec9ffe6390b..acc6a749e9b7 100644 --- a/core/java/android/window/TaskFragmentOperation.java +++ b/core/java/android/window/TaskFragmentOperation.java @@ -120,6 +120,11 @@ public final class TaskFragmentOperation implements Parcelable { */ public static final int OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE = 15; + /** + * Applies dimming on the parent Task which could cross two TaskFragments. + */ + public static final int OP_TYPE_SET_DIM_ON_TASK = 16; + @IntDef(prefix = { "OP_TYPE_" }, value = { OP_TYPE_UNKNOWN, OP_TYPE_CREATE_TASK_FRAGMENT, @@ -138,6 +143,7 @@ public final class TaskFragmentOperation implements Parcelable { OP_TYPE_REORDER_TO_TOP_OF_TASK, OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE, OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE, + OP_TYPE_SET_DIM_ON_TASK, }) @Retention(RetentionPolicy.SOURCE) public @interface OperationType {} @@ -165,12 +171,14 @@ public final class TaskFragmentOperation implements Parcelable { private final boolean mIsolatedNav; + private final boolean mDimOnTask; + private TaskFragmentOperation(@OperationType int opType, @Nullable TaskFragmentCreationParams taskFragmentCreationParams, @Nullable IBinder activityToken, @Nullable Intent activityIntent, @Nullable Bundle bundle, @Nullable IBinder secondaryFragmentToken, @Nullable TaskFragmentAnimationParams animationParams, - boolean isolatedNav) { + boolean isolatedNav, boolean dimOnTask) { mOpType = opType; mTaskFragmentCreationParams = taskFragmentCreationParams; mActivityToken = activityToken; @@ -179,6 +187,7 @@ public final class TaskFragmentOperation implements Parcelable { mSecondaryFragmentToken = secondaryFragmentToken; mAnimationParams = animationParams; mIsolatedNav = isolatedNav; + mDimOnTask = dimOnTask; } private TaskFragmentOperation(Parcel in) { @@ -190,6 +199,7 @@ public final class TaskFragmentOperation implements Parcelable { mSecondaryFragmentToken = in.readStrongBinder(); mAnimationParams = in.readTypedObject(TaskFragmentAnimationParams.CREATOR); mIsolatedNav = in.readBoolean(); + mDimOnTask = in.readBoolean(); } @Override @@ -202,6 +212,7 @@ public final class TaskFragmentOperation implements Parcelable { dest.writeStrongBinder(mSecondaryFragmentToken); dest.writeTypedObject(mAnimationParams, flags); dest.writeBoolean(mIsolatedNav); + dest.writeBoolean(mDimOnTask); } @NonNull @@ -282,6 +293,13 @@ public final class TaskFragmentOperation implements Parcelable { return mIsolatedNav; } + /** + * Returns whether the dim layer should apply on the parent Task. + */ + public boolean isDimOnTask() { + return mDimOnTask; + } + @Override public String toString() { final StringBuilder sb = new StringBuilder(); @@ -305,6 +323,7 @@ public final class TaskFragmentOperation implements Parcelable { sb.append(", animationParams=").append(mAnimationParams); } sb.append(", isolatedNav=").append(mIsolatedNav); + sb.append(", dimOnTask=").append(mDimOnTask); sb.append('}'); return sb.toString(); @@ -313,7 +332,7 @@ public final class TaskFragmentOperation implements Parcelable { @Override public int hashCode() { return Objects.hash(mOpType, mTaskFragmentCreationParams, mActivityToken, mActivityIntent, - mBundle, mSecondaryFragmentToken, mAnimationParams, mIsolatedNav); + mBundle, mSecondaryFragmentToken, mAnimationParams, mIsolatedNav, mDimOnTask); } @Override @@ -329,7 +348,8 @@ public final class TaskFragmentOperation implements Parcelable { && Objects.equals(mBundle, other.mBundle) && Objects.equals(mSecondaryFragmentToken, other.mSecondaryFragmentToken) && Objects.equals(mAnimationParams, other.mAnimationParams) - && mIsolatedNav == other.mIsolatedNav; + && mIsolatedNav == other.mIsolatedNav + && mDimOnTask == other.mDimOnTask; } @Override @@ -363,6 +383,8 @@ public final class TaskFragmentOperation implements Parcelable { private boolean mIsolatedNav; + private boolean mDimOnTask; + /** * @param opType the {@link OperationType} of this {@link TaskFragmentOperation}. */ @@ -435,13 +457,22 @@ public final class TaskFragmentOperation implements Parcelable { } /** + * Sets the dimming to apply on the parent Task if any. + */ + @NonNull + public Builder setDimOnTask(boolean dimOnTask) { + mDimOnTask = dimOnTask; + return this; + } + + /** * Constructs the {@link TaskFragmentOperation}. */ @NonNull public TaskFragmentOperation build() { return new TaskFragmentOperation(mOpType, mTaskFragmentCreationParams, mActivityToken, mActivityIntent, mBundle, mSecondaryFragmentToken, mAnimationParams, - mIsolatedNav); + mIsolatedNav, mDimOnTask); } } } diff --git a/core/java/android/window/flags/wallpaper_manager.aconfig b/core/java/android/window/flags/wallpaper_manager.aconfig index f03c993a9c66..ea9da96496c7 100644 --- a/core/java/android/window/flags/wallpaper_manager.aconfig +++ b/core/java/android/window/flags/wallpaper_manager.aconfig @@ -13,3 +13,10 @@ flag { description: "Support storing different wallpaper crops for different display dimensions. Only effective after rebooting." bug: "281648899" } + +flag { + name: "no_consecutive_visibility_events" + namespace: "systemui" + description: "Prevent the system from sending consecutive onVisibilityChanged(false) events." + bug: "285631818" +}
\ No newline at end of file diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 216acdce38fb..3366a7ee23c0 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -38,6 +38,14 @@ flag { } flag { + name: "draw_magnifier_border_outside_wmlock" + namespace: "windowing_frontend" + description: "Avoid holding WM locks for a long time when executing lockCanvas" + bug: "316075123" + is_fixed_read_only: true +} + +flag { name: "introduce_smoother_dimmer" namespace: "windowing_frontend" description: "Refactor dim to fix flickers" diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig index 59d7b0e55e85..f743ab74d1f5 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -38,4 +38,12 @@ flag { name: "activity_embedding_interactive_divider_flag" description: "Whether the interactive divider feature is enabled" bug: "293654166" +} + +flag { + namespace: "windowing_sdk" + name: "activity_window_info_flag" + description: "To dispatch ActivityWindowInfo through ClientTransaction" + bug: "287582673" + is_fixed_read_only: true }
\ No newline at end of file diff --git a/core/java/com/android/internal/foldables/FoldGracePeriodProvider.java b/core/java/com/android/internal/foldables/FoldGracePeriodProvider.java new file mode 100644 index 000000000000..53164f3e1ec9 --- /dev/null +++ b/core/java/com/android/internal/foldables/FoldGracePeriodProvider.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.foldables; + +import android.os.Build; +import android.sysprop.FoldLockBehaviorProperties; +import android.util.Slog; + +import com.android.internal.foldables.flags.Flags; + +import java.util.function.Supplier; + +/** + * Wrapper class to access {@link FoldLockBehaviorProperties}. + */ +public class FoldGracePeriodProvider { + + private static final String TAG = "FoldGracePeriodProvider"; + private final Supplier<Boolean> mFoldGracePeriodEnabled = Flags::foldGracePeriodEnabled; + + /** + * Whether the fold grace period feature is enabled. + */ + public boolean isEnabled() { + if ((Build.IS_ENG || Build.IS_USERDEBUG) + && FoldLockBehaviorProperties.fold_grace_period_enabled().orElse(false)) { + return true; + } + try { + return mFoldGracePeriodEnabled.get(); + } catch (Throwable ex) { + Slog.i(TAG, + "Flags not ready yet. Return false for " + + Flags.FLAG_FOLD_GRACE_PERIOD_ENABLED, + ex); + return false; + } + } +} diff --git a/core/java/com/android/internal/foldables/fold_lock_setting_flags.aconfig b/core/java/com/android/internal/foldables/fold_lock_setting_flags.aconfig index 44f436eaaa19..d73e62373732 100644 --- a/core/java/com/android/internal/foldables/fold_lock_setting_flags.aconfig +++ b/core/java/com/android/internal/foldables/fold_lock_setting_flags.aconfig @@ -7,3 +7,11 @@ flag { bug: "274447767" is_fixed_read_only: true } + +flag { + name: "fold_grace_period_enabled" + namespace: "display_manager" + description: "Feature flag for Folding Grace Period" + bug: "308417021" + is_fixed_read_only: true +} diff --git a/core/java/com/android/internal/policy/IKeyguardService.aidl b/core/java/com/android/internal/policy/IKeyguardService.aidl index 36b7ee5b3b62..d62c8f378af0 100644 --- a/core/java/com/android/internal/policy/IKeyguardService.aidl +++ b/core/java/com/android/internal/policy/IKeyguardService.aidl @@ -130,4 +130,10 @@ oneway interface IKeyguardService { * Note that it's called only if the device is interactive. */ void onSystemKeyPressed(int keycode); + + /** + * Requests to show the keyguard immediately without locking the device. Keyguard will show + * whether a screen lock was configured or not (including if screen lock is SWIPE or NONE). + */ + void showDismissibleKeyguard(); } diff --git a/core/java/com/android/internal/util/function/pooled/PooledLambda.java b/core/java/com/android/internal/util/function/pooled/PooledLambda.java index bdc8a668a7f7..1d6d69cee967 100755 --- a/core/java/com/android/internal/util/function/pooled/PooledLambda.java +++ b/core/java/com/android/internal/util/function/pooled/PooledLambda.java @@ -1009,7 +1009,7 @@ public interface PooledLambda { K arg11, L arg12) { synchronized (Message.sPoolSync) { PooledRunnable callback = acquire(PooledLambdaImpl.sMessageCallbacksPool, - function, 11, 0, ReturnType.VOID, arg1, arg2, arg3, arg4, arg5, arg6, arg7, + function, 12, 0, ReturnType.VOID, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12); return Message.obtain().setCallback(callback.recycleOnUse()); } diff --git a/core/jni/android_os_HwBinder.cpp b/core/jni/android_os_HwBinder.cpp index 781895eeeaba..477bd096b11a 100644 --- a/core/jni/android_os_HwBinder.cpp +++ b/core/jni/android_os_HwBinder.cpp @@ -258,14 +258,59 @@ static void JHwBinder_native_setup(JNIEnv *env, jobject thiz) { JHwBinder::SetNativeContext(env, thiz, context); } -static void JHwBinder_native_transact( - JNIEnv * /* env */, - jobject /* thiz */, - jint /* code */, - jobject /* requestObj */, - jobject /* replyObj */, - jint /* flags */) { - CHECK(!"Should not be here"); +static void JHwBinder_native_transact(JNIEnv *env, jobject thiz, jint code, jobject requestObj, + jobject replyObj, jint flags) { + if (requestObj == NULL) { + jniThrowException(env, "java/lang/NullPointerException", NULL); + return; + } + sp<hardware::IBinder> binder = JHwBinder::GetNativeBinder(env, thiz); + sp<android::hidl::base::V1_0::IBase> base = new android::hidl::base::V1_0::BpHwBase(binder); + hidl_string desc; + auto ret = base->interfaceDescriptor( + [&desc](const hidl_string &descriptor) { desc = descriptor; }); + ret.assertOk(); + // Only the fake hwservicemanager is allowed to be used locally like this. + if (desc != "android.hidl.manager@1.2::IServiceManager" && + desc != "android.hidl.manager@1.1::IServiceManager" && + desc != "android.hidl.manager@1.0::IServiceManager") { + LOG(FATAL) << "Local binders are not supported!"; + } + if (replyObj == nullptr) { + LOG(FATAL) << "Unexpected null replyObj. code: " << code; + return; + } + const hardware::Parcel *request = JHwParcel::GetNativeContext(env, requestObj)->getParcel(); + sp<JHwParcel> replyContext = JHwParcel::GetNativeContext(env, replyObj); + hardware::Parcel *reply = replyContext->getParcel(); + + request->setDataPosition(0); + + bool isOneway = (flags & IBinder::FLAG_ONEWAY) != 0; + if (!isOneway) { + replyContext->setTransactCallback([](auto &replyParcel) {}); + } + + env->CallVoidMethod(thiz, gFields.onTransactID, code, requestObj, replyObj, flags); + + if (env->ExceptionCheck()) { + jthrowable excep = env->ExceptionOccurred(); + env->ExceptionDescribe(); + env->ExceptionClear(); + + binder_report_exception(env, excep, "Uncaught error or exception in hwbinder!"); + + env->DeleteLocalRef(excep); + } + + if (!isOneway) { + if (!replyContext->wasSent()) { + // The implementation never finished the transaction. + LOG(ERROR) << "The reply failed to send!"; + } + } + + reply->setDataPosition(0); } static void JHwBinder_native_registerService( diff --git a/core/jni/android_view_InputEventReceiver.cpp b/core/jni/android_view_InputEventReceiver.cpp index 5b68e8ed1ad8..f7d815283885 100644 --- a/core/jni/android_view_InputEventReceiver.cpp +++ b/core/jni/android_view_InputEventReceiver.cpp @@ -82,6 +82,7 @@ public: status_t initialize(); void dispose(); status_t finishInputEvent(uint32_t seq, bool handled); + bool probablyHasInput(); status_t reportTimeline(int32_t inputEventId, nsecs_t gpuCompletedTime, nsecs_t presentTime); status_t consumeEvents(JNIEnv* env, bool consumeBatches, nsecs_t frameTime, bool* outConsumedBatch); @@ -165,6 +166,10 @@ status_t NativeInputEventReceiver::finishInputEvent(uint32_t seq, bool handled) return processOutboundEvents(); } +bool NativeInputEventReceiver::probablyHasInput() { + return mInputConsumer.probablyHasInput(); +} + status_t NativeInputEventReceiver::reportTimeline(int32_t inputEventId, nsecs_t gpuCompletedTime, nsecs_t presentTime) { if (kDebugDispatchCycle) { @@ -547,6 +552,12 @@ static void nativeFinishInputEvent(JNIEnv* env, jclass clazz, jlong receiverPtr, } } +static bool nativeProbablyHasInput(JNIEnv* env, jclass clazz, jlong receiverPtr) { + sp<NativeInputEventReceiver> receiver = + reinterpret_cast<NativeInputEventReceiver*>(receiverPtr); + return receiver->probablyHasInput(); +} + static void nativeReportTimeline(JNIEnv* env, jclass clazz, jlong receiverPtr, jint inputEventId, jlong gpuCompletedTime, jlong presentTime) { if (IdGenerator::getSource(inputEventId) != IdGenerator::Source::INPUT_READER) { @@ -597,6 +608,7 @@ static const JNINativeMethod gMethods[] = { (void*)nativeInit}, {"nativeDispose", "(J)V", (void*)nativeDispose}, {"nativeFinishInputEvent", "(JIZ)V", (void*)nativeFinishInputEvent}, + {"nativeProbablyHasInput", "(J)Z", (void*)nativeProbablyHasInput}, {"nativeReportTimeline", "(JIJJ)V", (void*)nativeReportTimeline}, {"nativeConsumeBatchedInputEvents", "(JJ)Z", (void*)nativeConsumeBatchedInputEvents}, {"nativeDump", "(JLjava/lang/String;)Ljava/lang/String;", (void*)nativeDump}, diff --git a/core/jni/hwbinder/EphemeralStorage.cpp b/core/jni/hwbinder/EphemeralStorage.cpp index 95bb42ea57c6..ef0750c815d7 100644 --- a/core/jni/hwbinder/EphemeralStorage.cpp +++ b/core/jni/hwbinder/EphemeralStorage.cpp @@ -164,7 +164,7 @@ void EphemeralStorage::release(JNIEnv *env) { } default: - CHECK(!"Should not be here"); + CHECK(!"Should not be here") << "Item type: " << item.mType; } } diff --git a/core/proto/android/server/windowmanagerservice.proto b/core/proto/android/server/windowmanagerservice.proto index 382a82cd090e..52e0124cc681 100644 --- a/core/proto/android/server/windowmanagerservice.proto +++ b/core/proto/android/server/windowmanagerservice.proto @@ -395,6 +395,8 @@ message ActivityRecordProto { optional bool should_refresh_activity_for_camera_compat = 40; optional bool should_refresh_activity_via_pause_for_camera_compat = 41; optional bool should_override_min_aspect_ratio = 42; + optional bool should_ignore_orientation_request_loop = 43; + optional bool should_override_force_resize_app = 44; } /* represents WindowToken */ @@ -404,7 +406,7 @@ message WindowTokenProto { optional WindowContainerProto window_container = 1; optional int32 hash_code = 2; repeated WindowStateProto windows = 3 [deprecated=true]; - optional bool waiting_to_show = 5; + optional bool waiting_to_show = 5 [deprecated=true]; optional bool paused = 6; } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 232a36fb6cb3..1eeffb9e2875 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1709,6 +1709,7 @@ <!-- @SystemApi Allows camera access by Headless System User 0 when device is running in HSUM Mode. + @FlaggedApi("com.android.internal.camera.flags.camera_hsum_permission") @hide --> <permission android:name="android.permission.CAMERA_HEADLESS_SYSTEM_USER" android:permissionGroup="android.permission-group.UNDEFINED" @@ -7692,6 +7693,13 @@ <permission android:name="android.permission.MONITOR_KEYBOARD_BACKLIGHT" android:protectionLevel="signature" /> + <!-- Allows low-level access to monitor sticky modifier state changes when A11Y Sticky keys + feature is enabled. + <p>Not for use by third-party applications. + @hide --> + <permission android:name="android.permission.MONITOR_STICKY_MODIFIER_STATE" + android:protectionLevel="signature" /> + <uses-permission android:name="android.permission.HANDLE_QUERY_PACKAGE_RESTART" /> <!-- Allows financed device kiosk apps to perform actions on the Device Lock service diff --git a/core/res/res/values-ar/strings.xml b/core/res/res/values-ar/strings.xml index 70f0c932b88e..0cc49a57dd1f 100644 --- a/core/res/res/values-ar/strings.xml +++ b/core/res/res/values-ar/strings.xml @@ -2316,7 +2316,7 @@ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="7089318886628390827">"إزالة الحظر"</string> <string name="sensor_privacy_notification_channel_label" msgid="936036783155261349">"الخصوصية في جهاز الاستشعار"</string> <string name="splash_screen_view_icon_description" msgid="180638751260598187">"رمز التطبيق"</string> - <string name="splash_screen_view_branding_description" msgid="7911129347402728216">"الصورة الذهنية للعلامة التجارية للتطبيق"</string> + <string name="splash_screen_view_branding_description" msgid="7911129347402728216">"هوية العلامة التجارية للتطبيق"</string> <string name="view_and_control_notification_title" msgid="4300765399209912240">"التحقّق من إعدادات الوصول"</string> <string name="view_and_control_notification_content" msgid="8003766498562604034">"يمكن لخدمة <xliff:g id="SERVICE_NAME">%s</xliff:g> الاطّلاع على شاشتك والتحكّم فيها. انقر لمراجعة الإعدادات."</string> <string name="ui_translation_accessibility_translated_text" msgid="3197547218178944544">"<xliff:g id="MESSAGE">%1$s</xliff:g> (مُترجَم)."</string> diff --git a/core/res/res/values-ca/strings.xml b/core/res/res/values-ca/strings.xml index a4b01e8c822f..ec14677965a7 100644 --- a/core/res/res/values-ca/strings.xml +++ b/core/res/res/values-ca/strings.xml @@ -2340,9 +2340,9 @@ <string name="permdesc_startForegroundServicesFromBackground" msgid="4071826571656001537">"Permet que una aplicació complementària iniciï serveis en primer pla des d\'un segon pla."</string> <string name="mic_access_on_toast" msgid="2666925317663845156">"El micròfon està disponible"</string> <string name="mic_access_off_toast" msgid="8111040892954242437">"El micròfon està bloquejat"</string> - <string name="connected_display_unavailable_notification_title" msgid="5265409360902073556">"No es pot projectar a la pantalla"</string> + <string name="connected_display_unavailable_notification_title" msgid="5265409360902073556">"No es pot duplicar a la pantalla"</string> <string name="connected_display_unavailable_notification_content" msgid="3845903313751217516">"Utilitza un altre cable i torna-ho a provar"</string> - <string name="connected_display_thermally_unavailable_notification_content" msgid="9205758199439955949">"El dispositiu està massa calent i no pot projectar a la pantalla fins que es refredi"</string> + <string name="connected_display_thermally_unavailable_notification_content" msgid="9205758199439955949">"El dispositiu està massa calent i no pot duplicar a la pantalla fins que es refredi"</string> <string name="concurrent_display_notification_name" msgid="1526911253558311131">"Pantalla dual"</string> <string name="concurrent_display_notification_active_title" msgid="4892473462327943673">"La pantalla dual està activada"</string> <string name="concurrent_display_notification_active_content" msgid="5889355473710601270">"<xliff:g id="APP_NAME">%1$s</xliff:g> està utilitzant les dues pantalles per mostrar contingut"</string> diff --git a/core/res/res/values-da/strings.xml b/core/res/res/values-da/strings.xml index 7119131048a4..d3f8550b58c7 100644 --- a/core/res/res/values-da/strings.xml +++ b/core/res/res/values-da/strings.xml @@ -2050,7 +2050,7 @@ <string name="autofill_save_type_password" msgid="5624528786144539944">"adgangskode"</string> <string name="autofill_save_type_address" msgid="3111006395818252885">"adresse"</string> <string name="autofill_save_type_credit_card" msgid="3583795235862046693">"kreditkort"</string> - <string name="autofill_save_type_debit_card" msgid="3169397504133097468">"betalingskort"</string> + <string name="autofill_save_type_debit_card" msgid="3169397504133097468">"debetkort"</string> <string name="autofill_save_type_payment_card" msgid="6555012156728690856">"betalingskort"</string> <string name="autofill_save_type_generic_card" msgid="1019367283921448608">"kort"</string> <string name="autofill_save_type_username" msgid="1018816929884640882">"brugernavn"</string> diff --git a/core/res/res/values-de/strings.xml b/core/res/res/values-de/strings.xml index c0e12c3d0a03..514d6955e629 100644 --- a/core/res/res/values-de/strings.xml +++ b/core/res/res/values-de/strings.xml @@ -355,7 +355,7 @@ <string name="permlab_fullScreenIntent" msgid="4310888199502509104">"Benachrichtigungen auf einem gesperrten Gerät als Vollbildaktivitäten anzeigen"</string> <string name="permdesc_fullScreenIntent" msgid="1100721419406643997">"Ermöglicht der App, Benachrichtigungen auf einem gesperrten Gerät als Vollbildaktivitäten anzuzeigen"</string> <string name="permlab_install_shortcut" msgid="7451554307502256221">"Verknüpfungen installieren"</string> - <string name="permdesc_install_shortcut" msgid="4476328467240212503">"ohne Zutun des Nutzers Verknüpfungen zum Startbildschirm hinzufügen."</string> + <string name="permdesc_install_shortcut" msgid="4476328467240212503">"Ermöglicht einer App, dem Startbildschirm ohne Zutun des Nutzers Verknüpfungen hinzuzufügen."</string> <string name="permlab_uninstall_shortcut" msgid="295263654781900390">"Verknüpfungen deinstallieren"</string> <string name="permdesc_uninstall_shortcut" msgid="1924735350988629188">"Ermöglicht einer App das Entfernen von Verknüpfungen vom Startbildschirm ohne Eingriff des Nutzers"</string> <string name="permlab_processOutgoingCalls" msgid="4075056020714266558">"Ausgehende Anrufe umleiten"</string> diff --git a/core/res/res/values-eu/strings.xml b/core/res/res/values-eu/strings.xml index cf18eb907e1c..2b9c5550d9a4 100644 --- a/core/res/res/values-eu/strings.xml +++ b/core/res/res/values-eu/strings.xml @@ -2331,7 +2331,7 @@ <string name="default_card_name" msgid="9198284935962911468">"<xliff:g id="CARDNUMBER">%d</xliff:g> TXARTELA"</string> <string name="permlab_companionProfileWatch" msgid="2457738382085872542">"Aplikazio osagarrien erloju-profilaren baimena erlojuak kudeatzeko"</string> <string name="permdesc_companionProfileWatch" msgid="5655698581110449397">"Erlojuak kudeatzeko baimena ematen die aplikazio osagarriei."</string> - <string name="permlab_observeCompanionDevicePresence" msgid="9008994909653990465">"Begiratu gailu osagarrien presentzia"</string> + <string name="permlab_observeCompanionDevicePresence" msgid="9008994909653990465">"Begiratu gailu osagarrien presentziari"</string> <string name="permdesc_observeCompanionDevicePresence" msgid="3011699826788697852">"Gailu osagarrien presentzia begiratzeko baimena ematen die aplikazio osagarriei gailuak inguruan edo urrun daudenean."</string> <string name="permlab_deliverCompanionMessages" msgid="3931552294842980887">"Entregatu aplikazio osagarrien mezuak"</string> <string name="permdesc_deliverCompanionMessages" msgid="2170847384281412850">"Beste gailuetan mezuak entregatzeko baimena ematen die aplikazio osagarriei."</string> diff --git a/core/res/res/values-fr/strings.xml b/core/res/res/values-fr/strings.xml index d435c62b9b49..3b242309e9b7 100644 --- a/core/res/res/values-fr/strings.xml +++ b/core/res/res/values-fr/strings.xml @@ -1724,7 +1724,7 @@ <string name="color_inversion_feature_name" msgid="2672824491933264951">"Inversion des couleurs"</string> <string name="color_correction_feature_name" msgid="7975133554160979214">"Correction des couleurs"</string> <string name="one_handed_mode_feature_name" msgid="2334330034828094891">"Mode une main"</string> - <string name="reduce_bright_colors_feature_name" msgid="3222994553174604132">"Encore moins lumineux"</string> + <string name="reduce_bright_colors_feature_name" msgid="3222994553174604132">"Luminosité ultra-réduite"</string> <string name="hearing_aids_feature_name" msgid="1125892105105852542">"Appareils auditifs"</string> <string name="accessibility_shortcut_enabling_service" msgid="5473495203759847687">"Touches de volume appuyées de manière prolongée. Service <xliff:g id="SERVICE_NAME">%1$s</xliff:g> activé."</string> <string name="accessibility_shortcut_disabling_service" msgid="8675244165062700619">"Touches de volume appuyées de manière prolongée. Service <xliff:g id="SERVICE_NAME">%1$s</xliff:g> désactivé."</string> diff --git a/core/res/res/values-sw/strings.xml b/core/res/res/values-sw/strings.xml index 2ff302df7a21..9a063fb65b0f 100644 --- a/core/res/res/values-sw/strings.xml +++ b/core/res/res/values-sw/strings.xml @@ -575,7 +575,7 @@ <string name="permdesc_changeWimaxState" product="tablet" msgid="4011097664859480108">"Inaruhusu programu kuunganisha kompyuta kibao, na kukata kompyuta kibao kutoka mitandao ya WiMAX."</string> <string name="permdesc_changeWimaxState" product="tv" msgid="5373274458799425276">"Huruhusu programu iunganishe na kutenganisha kifaa chako cha Android TV na mitandao ya WiMAX."</string> <string name="permdesc_changeWimaxState" product="default" msgid="1551666203780202101">"Inaruhusu programu kuunganisha simu kwenye, na kukata simu kutoka mitandao ya WiMAX."</string> - <string name="permlab_bluetooth" msgid="586333280736937209">"oanisha na vifaa vya Bluetooth"</string> + <string name="permlab_bluetooth" msgid="586333280736937209">"unganisha na vifaa vya Bluetooth"</string> <string name="permdesc_bluetooth" product="tablet" msgid="3053222571491402635">"Huruhusu programu kuona usanidi wa Bluetooth kwenye kompyuta kibao, na kutuma na kukubali miunganisho kwa vifaa vilivyooanishwa."</string> <string name="permdesc_bluetooth" product="tv" msgid="8851534496561034998">"Huruhusu programu iangalie mipangilio iliyowekwa ya Bluetooth kwenye kifaa chako cha Android TV na kufanya na kukubali miunganisho na vifaa vilivyooanishwa."</string> <string name="permdesc_bluetooth" product="default" msgid="2779606714091276746">"Huruhusu programu kuona usanidi wa Bluetooth kwenye simu, na kutuma na kukubali miunganisho kwa vifaa vilivyooanishwa."</string> diff --git a/core/sysprop/FoldLockBehaviorProperties.sysprop b/core/sysprop/FoldLockBehaviorProperties.sysprop index d337954ff2a0..120e4bbc743a 100644 --- a/core/sysprop/FoldLockBehaviorProperties.sysprop +++ b/core/sysprop/FoldLockBehaviorProperties.sysprop @@ -22,3 +22,11 @@ prop { scope: Internal access: Readonly } + +prop { + api_name: "fold_grace_period_enabled" + type: Boolean + prop_name: "persist.fold_grace_period_enabled" + scope: Internal + access: Readonly +} diff --git a/core/tests/coretests/src/android/content/pm/ModuleInfoTest.java b/core/tests/coretests/src/android/content/pm/ModuleInfoTest.java new file mode 100644 index 000000000000..4366e02cdf23 --- /dev/null +++ b/core/tests/coretests/src/android/content/pm/ModuleInfoTest.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2023 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.content.pm; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Parcel; +import android.platform.test.annotations.AppModeFull; +import android.text.TextUtils; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@AppModeFull +public class ModuleInfoTest { + + private static final String APEX_MODULE_NAME = "apexModuleName"; + private static final String APK_IN_APEX_PACKAGE_NAME = "apkInApexPackageName"; + private static final String MODULE_PACKAGE_NAME = "modulePackageName"; + private static final String MODULE_NAME = "moduleName"; + + @Test + public void testSimple() { + ModuleInfo info = new ModuleInfo(); + assertThat(info.toString()).isNotNull(); + } + + @Test + public void testDefaultCopy() { + ModuleInfo oldInfo = new ModuleInfo(); + ModuleInfo newInfo = new ModuleInfo(oldInfo); + assertThat(newInfo).isEqualTo(oldInfo); + } + + @Test + public void testCopy() { + boolean isHidden = false; + ModuleInfo info = new ModuleInfo(); + info.setHidden(isHidden); + info.setApexModuleName(APEX_MODULE_NAME); + info.setPackageName(MODULE_PACKAGE_NAME); + info.setName(MODULE_NAME); + info.setApkInApexPackageNames(List.of(APK_IN_APEX_PACKAGE_NAME)); + + ModuleInfo newInfo = new ModuleInfo(info); + assertThat(newInfo).isEqualTo(info); + } + + @Test + public void testGetApkInApexPackageNamesReturnEmptyListInDefault() { + ModuleInfo info = new ModuleInfo(); + assertThat(info.getApkInApexPackageNames()).isNotNull(); + assertThat(info.getApkInApexPackageNames()).isEmpty(); + } + + @Test + public void testModuleInfoParcelizeDeparcelize() { + boolean isHidden = false; + ModuleInfo info = new ModuleInfo(); + info.setHidden(isHidden); + info.setApexModuleName(APEX_MODULE_NAME); + info.setPackageName(MODULE_PACKAGE_NAME); + info.setName(MODULE_NAME); + info.setApkInApexPackageNames(List.of(APK_IN_APEX_PACKAGE_NAME)); + + final Parcel p = Parcel.obtain(); + info.writeToParcel(p, 0); + p.setDataPosition(0); + + final ModuleInfo targetInfo = ModuleInfo.CREATOR.createFromParcel(p); + p.recycle(); + + assertThat(info.isHidden()).isEqualTo(targetInfo.isHidden()); + assertThat(info.getApexModuleName()).isEqualTo(targetInfo.getApexModuleName()); + assertThat(info.getPackageName()).isEqualTo(targetInfo.getPackageName()); + assertThat(TextUtils.equals(info.getName(), targetInfo.getName())).isTrue(); + assertThat(info.getApkInApexPackageNames().toArray()).isEqualTo( + targetInfo.getApkInApexPackageNames().toArray()); + } +} diff --git a/data/etc/preinstalled-packages-platform.xml b/data/etc/preinstalled-packages-platform.xml index 421bc25d60e9..bf6094469215 100644 --- a/data/etc/preinstalled-packages-platform.xml +++ b/data/etc/preinstalled-packages-platform.xml @@ -128,4 +128,9 @@ to pre-existing users, but cannot uninstall pre-existing system packages from pr <install-in-user-type package="com.android.wallpaperbackup"> <install-in user-type="FULL" /> </install-in-user-type> + + <!-- AvatarPicker (AvatarPicker app)--> + <install-in-user-type package="com.android.avatarpicker"> + <install-in user-type="FULL" /> + </install-in-user-type> </config> diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java index ca3d8d18db83..592f9a57884c 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -19,6 +19,7 @@ package androidx.window.extensions.embedding; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_FRONT; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_DIM_ON_TASK; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ISOLATED_NAVIGATION; import static androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior; @@ -356,6 +357,13 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { wct.addTaskFragmentOperation(fragmentToken, operation); } + void setTaskFragmentDimOnTask(@NonNull WindowContainerTransaction wct, + @NonNull IBinder fragmentToken, boolean dimOnTask) { + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_SET_DIM_ON_TASK).setDimOnTask(dimOnTask).build(); + wct.addTaskFragmentOperation(fragmentToken, operation); + } + void updateTaskFragmentInfo(@NonNull TaskFragmentInfo taskFragmentInfo) { mFragmentInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 65597de44255..066f38b61eec 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -819,14 +819,20 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen Log.e(TAG, "onTaskFragmentParentInfoChanged on empty Task id=" + taskId); return; } + // Checks if container should be updated before apply new parentInfo. + final boolean shouldUpdateContainer = taskContainer.shouldUpdateContainer(parentInfo); taskContainer.updateTaskFragmentParentInfo(parentInfo); if (!taskContainer.isVisible()) { // Don't update containers if the task is not visible. We only update containers when // parentInfo#isVisibleRequested is true. return; } - if (isInPictureInPicture(parentInfo.getConfiguration())) { - // No need to update presentation in PIP until the Task exit PIP. + + // If the last direct activity of the host task is dismissed and the overlay container is + // the only taskFragment, the overlay container should also be dismissed. + dismissOverlayContainerIfNeeded(wct, taskContainer); + + if (!shouldUpdateContainer) { return; } updateContainersInTask(wct, taskContainer); @@ -1947,11 +1953,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen void updateOverlayContainer(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) { final TaskContainer taskContainer = container.getTaskContainer(); - // Dismiss the overlay container if it's the only container in the task and there's no - // direct activity in the parent task. - if (taskContainer.getTaskFragmentContainers().size() == 1 - && !taskContainer.hasDirectActivity()) { - container.finish(false /* shouldFinishDependent */, mPresenter, wct, this); + + if (dismissOverlayContainerIfNeeded(wct, taskContainer)) { return; } @@ -1968,6 +1971,24 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } } + /** Dismisses the overlay container in the {@code taskContainer} if needed. */ + @GuardedBy("mLock") + private boolean dismissOverlayContainerIfNeeded(@NonNull WindowContainerTransaction wct, + @NonNull TaskContainer taskContainer) { + final TaskFragmentContainer overlayContainer = taskContainer.getOverlayContainer(); + if (overlayContainer == null) { + return false; + } + // Dismiss the overlay container if it's the only container in the task and there's no + // direct activity in the parent task. + if (taskContainer.getTaskFragmentContainers().size() == 1 + && !taskContainer.hasDirectActivity()) { + mPresenter.cleanupContainer(wct, overlayContainer, false /* shouldFinishDependant */); + return true; + } + return false; + } + /** * Updates {@link SplitContainer} with the given {@link SplitAttributes} if the * {@link SplitContainer} is the top most and not finished. If passed {@link SplitAttributes} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index 543570c63ad7..6f356fa35d41 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -20,6 +20,8 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.content.pm.PackageManager.MATCH_ALL; +import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK; + import android.app.Activity; import android.app.ActivityThread; import android.app.WindowConfiguration; @@ -56,6 +58,7 @@ import androidx.window.extensions.layout.WindowLayoutComponentImpl; import androidx.window.extensions.layout.WindowLayoutInfo; import com.android.internal.annotations.VisibleForTesting; +import com.android.window.flags.Flags; import java.util.ArrayList; import java.util.List; @@ -384,6 +387,13 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { setCompanionTaskFragment(wct, primaryContainer.getTaskFragmentToken(), secondaryContainer.getTaskFragmentToken(), splitRule, isStacked); + // Sets the dim area when the two TaskFragments are adjacent. + final boolean dimOnTask = !isStacked + && splitAttributes.getWindowAttributes().getDimArea() == DIM_AREA_ON_TASK + && Flags.fullscreenDimFlag(); + setTaskFragmentDimOnTask(wct, primaryContainer.getTaskFragmentToken(), dimOnTask); + setTaskFragmentDimOnTask(wct, secondaryContainer.getTaskFragmentToken(), dimOnTask); + // Setting isolated navigation and clear non-sticky pinned container if needed. final SplitPinRule splitPinRule = splitRule instanceof SplitPinRule ? (SplitPinRule) splitRule : null; @@ -578,6 +588,23 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { bounds.isEmpty() ? WINDOWING_MODE_FULLSCREEN : WINDOWING_MODE_MULTI_WINDOW); } + @Override + void setTaskFragmentDimOnTask(@NonNull WindowContainerTransaction wct, + @NonNull IBinder fragmentToken, boolean dimOnTask) { + final TaskFragmentContainer container = mController.getContainer(fragmentToken); + if (container == null) { + throw new IllegalStateException("setTaskFragmentDimOnTask on TaskFragment that is" + + " not registered with controller."); + } + + if (container.isLastDimOnTask() == dimOnTask) { + return; + } + + container.setLastDimOnTask(dimOnTask); + super.setTaskFragmentDimOnTask(wct, fragmentToken, dimOnTask); + } + /** * Expands the split container if the current split bounds are smaller than the Activity or * Intent that is added to the container. diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index 64ad4faa421d..71195b6df97e 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -138,6 +138,21 @@ class TaskContainer { } /** + * Returns {@code true} if the container should be updated with {@code info}. + */ + boolean shouldUpdateContainer(@NonNull TaskFragmentParentInfo info) { + final Configuration configuration = info.getConfiguration(); + + return info.isVisible() + // No need to update presentation in PIP until the Task exit PIP. + && !isInPictureInPicture(configuration) + // If the task properties equals regardless of starting position, don't need to + // update the container. + && (mConfiguration.diffPublicOnly(configuration) != 0 + || mDisplayId != info.getDisplayId()); + } + + /** * Returns the windowing mode for the TaskFragments below this Task, which should be split with * other TaskFragments. * @@ -161,7 +176,11 @@ class TaskContainer { } boolean isInPictureInPicture() { - return getWindowingMode() == WINDOWING_MODE_PINNED; + return isInPictureInPicture(mConfiguration); + } + + private static boolean isInPictureInPicture(@NonNull Configuration configuration) { + return configuration.windowConfiguration.getWindowingMode() == WINDOWING_MODE_PINNED; } boolean isInMultiWindow() { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index 810bded8a7f0..6fe8e50f105f 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -172,6 +172,11 @@ class TaskFragmentContainer { private boolean mIsIsolatedNavigationEnabled; /** + * Whether to apply dimming on the parent Task that was requested last. + */ + private boolean mLastDimOnTask; + + /** * @see #TaskFragmentContainer(Activity, Intent, TaskContainer, SplitController, * TaskFragmentContainer, String, Bundle) */ @@ -611,6 +616,9 @@ class TaskFragmentContainer { * Removes all activities that belong to this process and finishes other containers/activities * configured to finish together. */ + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(container.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") @GuardedBy("mController.mLock") void finish(boolean shouldFinishDependent, @NonNull SplitPresenter presenter, @NonNull WindowContainerTransaction wct, @NonNull SplitController controller) { @@ -836,6 +844,16 @@ class TaskFragmentContainer { mIsIsolatedNavigationEnabled = isolatedNavigationEnabled; } + /** Sets whether to apply dim on the parent Task. */ + void setLastDimOnTask(boolean lastDimOnTask) { + mLastDimOnTask = lastDimOnTask; + } + + /** Returns whether to apply dim on the parent Task. */ + boolean isLastDimOnTask() { + return mLastDimOnTask; + } + /** * Adds the pending appeared activity that has requested to be launched in this task fragment. * @see android.app.ActivityClient#isRequestedToLaunchInTaskFragment diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TransactionManager.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TransactionManager.java index 396956e56bb5..6624c703f027 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TransactionManager.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TransactionManager.java @@ -77,9 +77,11 @@ class TransactionManager { @NonNull TransactionRecord startNewTransaction(@Nullable IBinder taskFragmentTransactionToken) { if (mCurrentTransaction != null) { + final TransactionRecord lastTransaction = mCurrentTransaction; mCurrentTransaction = null; throw new IllegalStateException( - "The previous transaction has not been applied or aborted,"); + "The previous transaction:" + lastTransaction + " has not been applied or " + + "aborted."); } mCurrentTransaction = new TransactionRecord(taskFragmentTransactionToken); return mCurrentTransaction; @@ -199,5 +201,15 @@ class TransactionManager { ? mOriginType : TASK_FRAGMENT_TRANSIT_CHANGE; } + + @Override + @NonNull + public String toString() { + return TransactionRecord.class.getSimpleName() + "{" + + "token=" + mTaskFragmentTransactionToken + + ", type=" + getTransactionTransitionType() + + ", transaction=" + mTransaction + + "}"; + } } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java index 5ef6a5263f96..bc921010b469 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java @@ -472,6 +472,29 @@ public class OverlayPresentationTest { verify(mSplitPresenter).applyActivityStackAttributes(any(), eq(container), eq(attrs)); } + @Test + public void testOnTaskFragmentParentInfoChanged_positionOnlyChange_earlyReturn() { + final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test"); + + final TaskContainer taskContainer = overlayContainer.getTaskContainer(); + spyOn(taskContainer); + final TaskContainer.TaskProperties taskProperties = taskContainer.getTaskProperties(); + final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo( + new Configuration(taskProperties.getConfiguration()), taskProperties.getDisplayId(), + true /* visible */, false /* hasDirectActivity */, null /* decorSurface */); + parentInfo.getConfiguration().windowConfiguration.getBounds().offset(10, 10); + + mSplitController.onTaskFragmentParentInfoChanged(mTransaction, TASK_ID, parentInfo); + + // The parent info must be applied to the task container + verify(taskContainer).updateTaskFragmentParentInfo(parentInfo); + verify(mSplitController, never()).updateContainer(any(), any()); + + assertWithMessage("The overlay container must still be dismissed even if " + + "#updateContainer is not called") + .that(taskContainer.getOverlayContainer()).isNull(); + } + /** * A simplified version of {@link SplitController.ActivityStartMonitor * #createOrUpdateOverlayTaskFragmentIfNeeded} diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java index 6981d9d7ebb8..941b4e1c3e41 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java @@ -235,6 +235,19 @@ public class SplitPresenterTest { } @Test + public void testSetTaskFragmentDimOnTask() { + final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID); + + mPresenter.setTaskFragmentDimOnTask(mTransaction, container.getTaskFragmentToken(), true); + verify(mTransaction).addTaskFragmentOperation(eq(container.getTaskFragmentToken()), any()); + + // No request to set the same adjacent TaskFragments. + clearInvocations(mTransaction); + mPresenter.setTaskFragmentDimOnTask(mTransaction, container.getTaskFragmentToken(), true); + verify(mTransaction, never()).addTaskFragmentOperation(any(), any()); + } + + @Test public void testUpdateAnimationParams() { final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID); diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml index cec7ee233236..ef7478c04dda 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml @@ -18,13 +18,13 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/desktop_mode_caption" - android:layout_width="match_parent" + android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center_horizontal"> <ImageButton android:id="@+id/caption_handle" - android:layout_width="128dp" + android:layout_width="@dimen/desktop_mode_fullscreen_decor_caption_width" android:layout_height="@dimen/desktop_mode_fullscreen_decor_caption_height" android:paddingVertical="16dp" android:contentDescription="@string/handle_text" diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 0a40cea3134d..28e709845e88 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -413,6 +413,9 @@ <!-- Height of desktop mode caption for fullscreen tasks. --> <dimen name="desktop_mode_fullscreen_decor_caption_height">36dp</dimen> + <!-- Width of desktop mode caption for fullscreen tasks. --> + <dimen name="desktop_mode_fullscreen_decor_caption_width">128dp</dimen> + <!-- Required empty space to be visible for partially offscreen tasks. --> <dimen name="freeform_required_visible_empty_space_in_header">48dp</dimen> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java index 6213f628dfd3..8f04f126960c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java @@ -63,6 +63,10 @@ class TouchTracker { if ((touchX < mStartThresholdX && mSwipeEdge == BackEvent.EDGE_LEFT) || (touchX > mStartThresholdX && mSwipeEdge == BackEvent.EDGE_RIGHT)) { mStartThresholdX = touchX; + if ((mSwipeEdge == BackEvent.EDGE_LEFT && mStartThresholdX < mInitTouchX) + || (mSwipeEdge == BackEvent.EDGE_RIGHT && mStartThresholdX > mInitTouchX)) { + mInitTouchX = mStartThresholdX; + } } mLatestTouchX = touchX; mLatestTouchY = touchY; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index 48a0a46dccc1..3b0e7c139bed 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -19,6 +19,7 @@ package com.android.wm.shell.pip2.phone; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_PIP; +import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; @@ -54,6 +55,8 @@ public class PipTransition extends PipTransitionController { @Nullable private WindowContainerToken mPipTaskToken; @Nullable + private IBinder mEnterTransition; + @Nullable private IBinder mAutoEnterButtonNavTransition; @Nullable private IBinder mExitViaExpandTransition; @@ -98,11 +101,8 @@ public class PipTransition extends PipTransitionController { @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @NonNull TransitionRequestInfo request) { - if (isAutoEnterInButtonNavigation(request)) { - mAutoEnterButtonNavTransition = transition; - return getEnterPipTransaction(transition, request); - } else if (isLegacyEnter(request)) { - mLegacyEnterTransition = transition; + if (isAutoEnterInButtonNavigation(request) || isEnterPictureInPictureModeRequest(request)) { + mEnterTransition = transition; return getEnterPipTransaction(transition, request); } return null; @@ -111,12 +111,9 @@ public class PipTransition extends PipTransitionController { @Override public void augmentRequest(@NonNull IBinder transition, @NonNull TransitionRequestInfo request, @NonNull WindowContainerTransaction outWct) { - if (isAutoEnterInButtonNavigation(request)) { + if (isAutoEnterInButtonNavigation(request) || isEnterPictureInPictureModeRequest(request)) { outWct.merge(getEnterPipTransaction(transition, request), true /* transfer */); - mAutoEnterButtonNavTransition = transition; - } else if (isLegacyEnter(request)) { - outWct.merge(getEnterPipTransaction(transition, request), true /* transfer */); - mLegacyEnterTransition = transition; + mEnterTransition = transition; } } @@ -162,7 +159,7 @@ public class PipTransition extends PipTransitionController { && pipTask.pictureInPictureParams.isAutoEnterEnabled(); } - private boolean isLegacyEnter(@NonNull TransitionRequestInfo requestInfo) { + private boolean isEnterPictureInPictureModeRequest(@NonNull TransitionRequestInfo requestInfo) { return requestInfo.getType() == TRANSIT_PIP; } @@ -172,13 +169,15 @@ public class PipTransition extends PipTransitionController { @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { - if (transition == mAutoEnterButtonNavTransition) { - mAutoEnterButtonNavTransition = null; - return startAutoEnterButtonNavAnimation(info, startTransaction, finishTransaction, - finishCallback); - } else if (transition == mLegacyEnterTransition) { - mLegacyEnterTransition = null; - return startLegacyEnterAnimation(info, startTransaction, finishTransaction, + if (transition == mEnterTransition) { + mEnterTransition = null; + if (isLegacyEnter(info)) { + // If this is a legacy-enter-pip (auto-enter is off and PiP activity went to pause), + // then we should run an ALPHA type (cross-fade) animation. + return startAlphaTypeEnterAnimation(info, startTransaction, finishTransaction, + finishCallback); + } + return startBoundsTypeEnterAnimation(info, startTransaction, finishTransaction, finishCallback); } else if (transition == mExitViaExpandTransition) { mExitViaExpandTransition = null; @@ -187,7 +186,15 @@ public class PipTransition extends PipTransitionController { return false; } - private boolean startAutoEnterButtonNavAnimation(@NonNull TransitionInfo info, + private boolean isLegacyEnter(@NonNull TransitionInfo info) { + TransitionInfo.Change pipChange = getPipChange(info); + // If the only change in the changes list is a TO_FRONT mode PiP task, + // then this is legacy-enter PiP. + return pipChange != null && pipChange.getMode() == TRANSIT_TO_FRONT + && info.getChanges().size() == 1; + } + + private boolean startBoundsTypeEnterAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { @@ -205,7 +212,7 @@ public class PipTransition extends PipTransitionController { return true; } - private boolean startLegacyEnterAnimation(@NonNull TransitionInfo info, + private boolean startAlphaTypeEnterAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java index 34c015f05c68..84f21f693eb8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java @@ -330,7 +330,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { continue; } if (isHide) { - if (pending.mType == TRANSIT_TO_BACK) { + if (pending != null && pending.mType == TRANSIT_TO_BACK) { // TO_BACK is only used when setting the task view visibility immediately, // so in that case we can also hide the surface immediately startTransaction.hide(chg.getLeash()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 4fd362591151..61a8e9b5dd59 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -726,7 +726,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private void handleEventOutsideFocusedCaption(MotionEvent ev, DesktopModeWindowDecoration relevantDecor) { // Returns if event occurs within caption - if (relevantDecor == null || relevantDecor.checkTouchEventInCaption(ev)) { + if (relevantDecor == null || relevantDecor.checkTouchEventInCaptionHandle(ev)) { return; } @@ -761,7 +761,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { || windowingMode == WINDOWING_MODE_MULTI_WINDOW; } - if (dragFromStatusBarAllowed && relevantDecor.checkTouchEventInHandle(ev)) { + if (dragFromStatusBarAllowed + && relevantDecor.checkTouchEventInCaptionHandle(ev)) { mTransitionDragActive = true; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 0c8e93b48d02..d08b655e43f8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -317,6 +317,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin relayoutParams.mLayoutResId = getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode()); relayoutParams.mCaptionHeightId = getCaptionHeightIdStatic(taskInfo.getWindowingMode()); + relayoutParams.mCaptionWidthId = getCaptionWidthId(relayoutParams.mLayoutResId); if (DesktopModeStatus.useWindowShadow(/* isFocusedWindow= */ taskInfo.isFocused)) { relayoutParams.mShadowRadiusId = taskInfo.isFocused ? R.dimen.freeform_decor_shadow_focused_thickness @@ -345,6 +346,17 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } } + /** + * If task has focused window decor, return the caption id of the fullscreen caption size + * resource. Otherwise, return ID_NULL and caption width be set to task width. + */ + private static int getCaptionWidthId(int layoutResId) { + if (layoutResId == R.layout.desktop_mode_focused_window_decor) { + return R.dimen.desktop_mode_fullscreen_decor_caption_width; + } + return Resources.ID_NULL; + } + private PointF calculateMaximizeMenuPosition() { final PointF position = new PointF(); @@ -558,7 +570,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin .setOnClickListener(mOnCaptionButtonClickListener) .setOnTouchListener(mOnCaptionTouchListener) .setLayoutId(mRelayoutParams.mLayoutResId) - .setCaptionPosition(mRelayoutParams.mCaptionX, mRelayoutParams.mCaptionY) .setWindowingButtonsVisible(DesktopModeStatus.isEnabled()) .setCaptionHeight(mResult.mCaptionHeight) .build(); @@ -635,35 +646,25 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mTaskOrganizer.getRunningTaskInfo(mTaskInfo.taskId); if (taskInfo == null) return result; final Point positionInParent = taskInfo.positionInParent; - result.offset(-mRelayoutParams.mCaptionX, -mRelayoutParams.mCaptionY); result.offset(-positionInParent.x, -positionInParent.y); return result; } /** - * Determine if a passed MotionEvent is in a view in caption + * Checks if motion event occurs in the caption handle area. This should be used in cases where + * onTouchListener will not work (i.e. when caption is in status bar area). * * @param ev the {@link MotionEvent} to check - * @param layoutId the id of the view * @return {@code true} if event is inside the specified view, {@code false} if not */ - private boolean checkEventInCaptionView(MotionEvent ev, int layoutId) { - if (mResult.mRootView == null) return false; + boolean checkTouchEventInCaptionHandle(MotionEvent ev) { + if (isHandleMenuActive() || !(mWindowDecorViewHolder + instanceof DesktopModeFocusedWindowDecorationViewHolder)) { + return false; + } final PointF inputPoint = offsetCaptionLocation(ev); - final View view = mResult.mRootView.findViewById(layoutId); - return view != null && pointInView(view, inputPoint.x, inputPoint.y); - } - - boolean checkTouchEventInHandle(MotionEvent ev) { - if (isHandleMenuActive()) return false; - return checkEventInCaptionView(ev, R.id.caption_handle); - } - - /** - * Returns true if motion event is within the caption's root view's bounds. - */ - boolean checkTouchEventInCaption(MotionEvent ev) { - return checkEventInCaptionView(ev, getCaptionViewId()); + return ((DesktopModeFocusedWindowDecorationViewHolder) mWindowDecorViewHolder) + .pointInCaption(inputPoint, mResult.mCaptionX); } /** @@ -676,24 +677,19 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin void checkClickEvent(MotionEvent ev) { if (mResult.mRootView == null) return; if (!isHandleMenuActive()) { + // Click if point in caption handle view final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption); final View handle = caption.findViewById(R.id.caption_handle); - clickIfPointInView(new PointF(ev.getX(), ev.getY()), handle); + if (checkTouchEventInCaptionHandle(ev)) { + mOnCaptionButtonClickListener.onClick(handle); + } } else { mHandleMenu.checkClickEvent(ev); closeHandleMenuIfNeeded(ev); } } - private boolean clickIfPointInView(PointF inputPoint, View v) { - if (pointInView(v, inputPoint.x, inputPoint.y)) { - mOnCaptionButtonClickListener.onClick(v); - return true; - } - return false; - } - - boolean pointInView(View v, float x, float y) { + private boolean pointInView(View v, float x, float y) { return v != null && v.getLeft() <= x && v.getRight() >= x && v.getTop() <= y && v.getBottom() >= y; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java index 652a2ed39c67..b37dd0d6fd2d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java @@ -64,8 +64,6 @@ class HandleMenu { private final View.OnTouchListener mOnTouchListener; private final RunningTaskInfo mTaskInfo; private final int mLayoutResId; - private final int mCaptionX; - private final int mCaptionY; private int mMarginMenuTop; private int mMarginMenuStart; private int mMenuHeight; @@ -74,16 +72,13 @@ class HandleMenu { private HandleMenuAnimator mHandleMenuAnimator; - HandleMenu(WindowDecoration parentDecor, int layoutResId, int captionX, int captionY, - View.OnClickListener onClickListener, View.OnTouchListener onTouchListener, - Bitmap appIcon, CharSequence appName, boolean shouldShowWindowingPill, - int captionHeight) { + HandleMenu(WindowDecoration parentDecor, int layoutResId, View.OnClickListener onClickListener, + View.OnTouchListener onTouchListener, Bitmap appIcon, CharSequence appName, + boolean shouldShowWindowingPill, int captionHeight) { mParentDecor = parentDecor; mContext = mParentDecor.mDecorWindowContext; mTaskInfo = mParentDecor.mTaskInfo; mLayoutResId = layoutResId; - mCaptionX = captionX; - mCaptionY = captionY; mOnClickListener = onClickListener; mOnTouchListener = onTouchListener; mAppIconBitmap = appIcon; @@ -225,12 +220,12 @@ class HandleMenu { if (mLayoutResId == R.layout.desktop_mode_app_controls_window_decor) { // Align the handle menu to the left of the caption. - menuX = mCaptionX + mMarginMenuStart; - menuY = mCaptionY + mMarginMenuTop; + menuX = mMarginMenuStart; + menuY = mMarginMenuTop; } else { // Position the handle menu at the center of the caption. - menuX = mCaptionX + (captionWidth / 2) - (mMenuWidth / 2); - menuY = mCaptionY + mMarginMenuStart; + menuX = (captionWidth / 2) - (mMenuWidth / 2); + menuY = mMarginMenuStart; } // Handle Menu position setup. @@ -346,8 +341,6 @@ class HandleMenu { private View.OnClickListener mOnClickListener; private View.OnTouchListener mOnTouchListener; private int mLayoutId; - private int mCaptionX; - private int mCaptionY; private boolean mShowWindowingPill; private int mCaptionHeight; @@ -381,12 +374,6 @@ class HandleMenu { return this; } - Builder setCaptionPosition(int captionX, int captionY) { - mCaptionX = captionX; - mCaptionY = captionY; - return this; - } - Builder setWindowingButtonsVisible(boolean windowingButtonsVisible) { mShowWindowingPill = windowingButtonsVisible; return this; @@ -398,8 +385,8 @@ class HandleMenu { } HandleMenu build() { - return new HandleMenu(mParent, mLayoutId, mCaptionX, mCaptionY, mOnClickListener, - mOnTouchListener, mAppIcon, mName, mShowWindowingPill, mCaptionHeight); + return new HandleMenu(mParent, mLayoutId, mOnClickListener, mOnTouchListener, + mAppIcon, mName, mShowWindowingPill, mCaptionHeight); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index b5373c67c602..6a9258c68acf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -279,9 +279,12 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } outResult.mCaptionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId); - final int captionWidth = taskBounds.width(); + final int captionWidth = params.mCaptionWidthId != Resources.ID_NULL + ? loadDimensionPixelSize(resources, params.mCaptionWidthId) : taskBounds.width(); + outResult.mCaptionX = (outResult.mWidth - captionWidth) / 2; startT.setWindowCrop(mCaptionContainerSurface, captionWidth, outResult.mCaptionHeight) + .setPosition(mCaptionContainerSurface, outResult.mCaptionX, 0 /* y */) .setLayer(mCaptionContainerSurface, CAPTION_LAYER_Z_ORDER) .show(mCaptionContainerSurface); @@ -292,7 +295,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mCaptionInsetsRect.set(taskBounds); if (mIsCaptionVisible) { mCaptionInsetsRect.bottom = - mCaptionInsetsRect.top + outResult.mCaptionHeight + params.mCaptionY; + mCaptionInsetsRect.top + outResult.mCaptionHeight; wct.addInsetsSource(mTaskInfo.token, mOwner, 0 /* index */, WindowInsets.Type.captionBar(), mCaptionInsetsRect); wct.addInsetsSource(mTaskInfo.token, @@ -554,9 +557,6 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> int mCornerRadius; - int mCaptionX; - int mCaptionY; - Configuration mWindowDecorConfig; boolean mApplyStartTransactionOnDraw; @@ -570,9 +570,6 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mCornerRadius = 0; - mCaptionX = 0; - mCaptionY = 0; - mApplyStartTransactionOnDraw = false; mSetTaskPositionAndCrop = false; mWindowDecorConfig = null; @@ -581,6 +578,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> static class RelayoutResult<T extends View & TaskFocusStateConsumer> { int mCaptionHeight; + int mCaptionX; int mWidth; int mHeight; T mRootView; @@ -589,6 +587,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mWidth = 0; mHeight = 0; mCaptionHeight = 0; + mCaptionX = 0; mRootView = null; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt index 4930cb721336..5f77022a577d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt @@ -5,6 +5,7 @@ import android.app.ActivityManager.RunningTaskInfo import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.content.res.ColorStateList import android.graphics.Color +import android.graphics.PointF import android.view.View import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS import android.widget.ImageButton @@ -35,9 +36,6 @@ internal class DesktopModeFocusedWindowDecorationViewHolder( } override fun bindData(taskInfo: RunningTaskInfo) { - taskInfo.taskDescription?.statusBarColor?.let { captionColor -> - captionView.setBackgroundColor(captionColor) - } captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo)) } @@ -49,6 +47,17 @@ internal class DesktopModeFocusedWindowDecorationViewHolder( animateCaptionHandleAlpha(startValue = 0f, endValue = 1f) } + /** + * Returns true if input point is in the caption's view. + * @param inputPoint the input point relative to the task in full "focus" (i.e. fullscreen). + */ + fun pointInCaption(inputPoint: PointF, captionX: Int): Boolean { + return inputPoint.x >= captionX && + inputPoint.x <= captionX + captionView.width && + inputPoint.y >= 0 && + inputPoint.y <= captionView.height + } + private fun getCaptionHandleBarColor(taskInfo: RunningTaskInfo): Int { return if (shouldUseLightCaptionColors(taskInfo)) { context.getColor(R.color.desktop_mode_caption_handle_bar_light) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt index 690b4e4be122..81bc34c876b6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt @@ -17,9 +17,9 @@ internal abstract class DesktopModeWindowDecorationViewHolder(rootView: View) { */ abstract fun bindData(taskInfo: RunningTaskInfo) - /** Callback when the handle menu is opened. */ - abstract fun onHandleMenuOpened() + /** Callback when the handle menu is opened. */ + abstract fun onHandleMenuOpened() - /** Callback when the handle menu is closed. */ - abstract fun onHandleMenuClosed() + /** Callback when the handle menu is closed. */ + abstract fun onHandleMenuClosed() } diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt index 32f12592135d..143f7a726ed3 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt @@ -69,7 +69,7 @@ open class NetflixEnterPipTest(flicker: LegacyFlickerTest) : AppsEnterPipTransit setup { standardAppHelper.launchViaIntent( wmHelper, - NetflixAppHelper.getNetflixWatchVideoIntent("70184207"), + NetflixAppHelper.getNetflixWatchVideoIntent("81605060"), ComponentNameMatcher(NetflixAppHelper.PACKAGE_NAME, NetflixAppHelper.WATCH_ACTIVITY) ) standardAppHelper.waitForVideoPlaying() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.kt index bf07dccd0658..6dbb1e2b8d92 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.kt @@ -170,6 +170,71 @@ class TouchTrackerTest { nonLinearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / nonLinearTarget) } + @Test + fun restartingGesture_resetsInitialTouchX_leftEdge() { + val linearTracker = linearTouchTracker() + linearTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0f, BackEvent.EDGE_LEFT) + var touchX = 100f + val velocityX = 0f + val velocityY = 0f + + // assert that progress is increased when increasing touchX + linearTracker.update(touchX, 0f, velocityX, velocityY) + linearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / MAX_DISTANCE) + + // assert that progress is reset to 0 when start location is updated + linearTracker.updateStartLocation() + linearTracker.assertProgress(0f) + + // assert that progress remains 0 when touchX is decreased + touchX -= 50 + linearTracker.update(touchX, 0f, velocityX, velocityY) + linearTracker.assertProgress(0f) + + // assert that progress uses new minimal touchX for progress calculation + val newInitialTouchX = touchX + touchX += 100 + linearTracker.update(touchX, 0f, velocityX, velocityY) + linearTracker.assertProgress((touchX - newInitialTouchX) / MAX_DISTANCE) + + // assert the same for triggerBack==true + linearTracker.triggerBack = true + linearTracker.assertProgress((touchX - newInitialTouchX) / MAX_DISTANCE) + } + + @Test + fun restartingGesture_resetsInitialTouchX_rightEdge() { + val linearTracker = linearTouchTracker() + linearTracker.setGestureStartLocation(INITIAL_X_RIGHT_EDGE, 0f, BackEvent.EDGE_RIGHT) + + var touchX = INITIAL_X_RIGHT_EDGE - 100f + val velocityX = 0f + val velocityY = 0f + + // assert that progress is increased when decreasing touchX + linearTracker.update(touchX, 0f, velocityX, velocityY) + linearTracker.assertProgress((INITIAL_X_RIGHT_EDGE - touchX) / MAX_DISTANCE) + + // assert that progress is reset to 0 when start location is updated + linearTracker.updateStartLocation() + linearTracker.assertProgress(0f) + + // assert that progress remains 0 when touchX is increased + touchX += 50 + linearTracker.update(touchX, 0f, velocityX, velocityY) + linearTracker.assertProgress(0f) + + // assert that progress uses new maximal touchX for progress calculation + val newInitialTouchX = touchX + touchX -= 100 + linearTracker.update(touchX, 0f, velocityX, velocityY) + linearTracker.assertProgress((newInitialTouchX - touchX) / MAX_DISTANCE) + + // assert the same for triggerBack==true + linearTracker.triggerBack = true + linearTracker.assertProgress((newInitialTouchX - touchX) / MAX_DISTANCE) + } + companion object { private const val MAX_DISTANCE = 500f private const val LINEAR_DISTANCE = 400f diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index 32a91461e40f..7b53f70a771c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -772,15 +772,13 @@ public class WindowDecorationTests extends ShellTestCase { private WindowDecoration.AdditionalWindow addTestWindow() { final Resources resources = mDecorWindowContext.getResources(); - int x = mRelayoutParams.mCaptionX; - int y = mRelayoutParams.mCaptionY; int width = loadDimensionPixelSize(resources, mCaptionMenuWidthId); int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId); String name = "Test Window"; WindowDecoration.AdditionalWindow additionalWindow = addWindow(R.layout.desktop_mode_window_decor_handle_menu, name, - mMockSurfaceControlAddWindowT, mMockSurfaceSyncGroup, x, y, - width, height); + mMockSurfaceControlAddWindowT, mMockSurfaceSyncGroup, 0 /* x */, + 0 /* y */, width, height); return additionalWindow; } } diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 47411701e5ab..eebf8aabd89c 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -38,6 +38,7 @@ aconfig_declarations { cc_aconfig_library { name: "hwui_flags_cc_lib", + host_supported: true, aconfig_declarations: "hwui_flags", } @@ -109,12 +110,15 @@ cc_defaults { "libbase", "libharfbuzz_ng", "libminikin", + "server_configurable_flags", ], static_libs: [ "libui-types", ], + whole_static_libs: ["hwui_flags_cc_lib"], + target: { android: { shared_libs: [ @@ -146,7 +150,6 @@ cc_defaults { "libstatspull_lazy", "libstatssocket_lazy", "libtonemap", - "hwui_flags_cc_lib", ], }, host: { diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index ca119757e816..c156c46a5a9b 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -15,6 +15,13 @@ flag { } flag { + name: "high_contrast_text_luminance" + namespace: "accessibility" + description: "Use luminance to determine how to make text more high contrast, instead of RGB heuristic" + bug: "186567103" +} + +flag { name: "hdr_10bit_plus" namespace: "core_graphics" description: "Use 10101010 and FP16 formats for HDR-UI when available" diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h index 2e6e97634aec..8f999904a8ab 100644 --- a/libs/hwui/hwui/DrawTextFunctor.h +++ b/libs/hwui/hwui/DrawTextFunctor.h @@ -16,7 +16,9 @@ #include <SkFontMetrics.h> #include <SkRRect.h> +#include <com_android_graphics_hwui_flags.h> +#include "../utils/Color.h" #include "Canvas.h" #include "FeatureFlags.h" #include "MinikinUtils.h" @@ -27,6 +29,8 @@ #include "hwui/PaintFilter.h" #include "pipeline/skia/SkiaRecordingCanvas.h" +namespace flags = com::android::graphics::hwui::flags; + namespace android { static inline void drawStroke(SkScalar left, SkScalar right, SkScalar top, SkScalar thickness, @@ -73,8 +77,14 @@ public: if (CC_UNLIKELY(canvas->isHighContrastText() && paint.getAlpha() != 0)) { // high contrast draw path int color = paint.getColor(); - int channelSum = SkColorGetR(color) + SkColorGetG(color) + SkColorGetB(color); - bool darken = channelSum < (128 * 3); + bool darken; + if (flags::high_contrast_text_luminance()) { + uirenderer::Lab lab = uirenderer::sRGBToLab(color); + darken = lab.L <= 50; + } else { + int channelSum = SkColorGetR(color) + SkColorGetG(color) + SkColorGetB(color); + darken = channelSum < (128 * 3); + } // outline gDrawTextBlobMode = DrawTextBlobMode::HctOutline; diff --git a/libs/hwui/utils/ForceDark.h b/libs/hwui/utils/ForceDark.h index 28538c4b7a7b..ecfe41f39ecb 100644 --- a/libs/hwui/utils/ForceDark.h +++ b/libs/hwui/utils/ForceDark.h @@ -17,6 +17,8 @@ #ifndef FORCEDARKUTILS_H #define FORCEDARKUTILS_H +#include <stdint.h> + namespace android { namespace uirenderer { @@ -26,9 +28,9 @@ namespace uirenderer { * This should stay in sync with the java @IntDef in * frameworks/base/graphics/java/android/graphics/ForceDarkType.java */ -enum class ForceDarkType : __uint8_t { NONE = 0, FORCE_DARK = 1, FORCE_INVERT_COLOR_DARK = 2 }; +enum class ForceDarkType : uint8_t { NONE = 0, FORCE_DARK = 1, FORCE_INVERT_COLOR_DARK = 2 }; } /* namespace uirenderer */ } /* namespace android */ -#endif // FORCEDARKUTILS_H
\ No newline at end of file +#endif // FORCEDARKUTILS_H diff --git a/location/java/android/location/flags/gnss.aconfig b/location/java/android/location/flags/gnss.aconfig index 794a555e22cb..5f1279ecceea 100644 --- a/location/java/android/location/flags/gnss.aconfig +++ b/location/java/android/location/flags/gnss.aconfig @@ -41,3 +41,10 @@ flag { description: "Flag for GNSS configuration from resource" bug: "317734846" } + +flag { + name: "replace_future_elapsed_realtime_jni" + namespace: "location" + description: "Flag for replacing future elapsedRealtime in JNI" + bug: "314328533" +} diff --git a/media/java/android/media/LoudnessCodecConfigurator.java b/media/java/android/media/LoudnessCodecConfigurator.java index de9d87c0b28c..aadd78328d68 100644 --- a/media/java/android/media/LoudnessCodecConfigurator.java +++ b/media/java/android/media/LoudnessCodecConfigurator.java @@ -234,19 +234,21 @@ public class LoudnessCodecConfigurator { * @param mediaCodec the codec to start receiving asynchronous loudness * updates. The codec has to be in a configured or started * state in order to add it for loudness updates. - * @throws IllegalArgumentException if the {@code mediaCodec} was not configured, - * does not contain loudness metadata or if it - * was already added before + * @throws IllegalArgumentException if the same {@code mediaCodec} was already + * added before. + * @return {@code false} if the {@code mediaCodec} was not configured or does + * not contain loudness metadata, {@code true} otherwise. */ @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API) - public void addMediaCodec(@NonNull MediaCodec mediaCodec) { + public boolean addMediaCodec(@NonNull MediaCodec mediaCodec) { final MediaCodec mc = Objects.requireNonNull(mediaCodec, "MediaCodec for addMediaCodec cannot be null"); int piid = PLAYER_PIID_INVALID; final LoudnessCodecInfo mcInfo = getCodecInfo(mc); if (mcInfo == null) { - throw new IllegalArgumentException("Could not extract codec loudness information"); + Log.v(TAG, "Could not extract codec loudness information"); + return false; } synchronized (mConfiguratorLock) { final AtomicBoolean containsCodec = new AtomicBoolean(false); @@ -271,6 +273,8 @@ public class LoudnessCodecConfigurator { if (piid != PLAYER_PIID_INVALID) { mLcDispatcher.addLoudnessCodecInfo(piid, mediaCodec.hashCode(), mcInfo); } + + return true; } /** diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java index b7c972083657..470a8ac279c7 100644 --- a/media/java/android/media/MediaCodec.java +++ b/media/java/android/media/MediaCodec.java @@ -3401,13 +3401,15 @@ final public class MediaCodec { } /** - * Set a harware graphic buffer to this queue request. Exactly one buffer must + * Set a hardware graphic buffer to this queue request. Exactly one buffer must * be set for a queue request before calling {@link #queue}. * <p> * Note: buffers should have format {@link HardwareBuffer#YCBCR_420_888}, * a single layer, and an appropriate usage ({@link HardwareBuffer#USAGE_CPU_READ_OFTEN} * for software codecs and {@link HardwareBuffer#USAGE_VIDEO_ENCODE} for hardware) - * for codecs to recognize. Codecs may throw exception if the buffer is not recognizable. + * for codecs to recognize. Format {@link ImageFormat#PRIVATE} together with + * usage {@link HardwareBuffer#USAGE_VIDEO_ENCODE} will also work for hardware codecs. + * Codecs may throw exception if the buffer is not recognizable. * * @param buffer The hardware graphic buffer object * @return this object diff --git a/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecConfiguratorTest.java b/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecConfiguratorTest.java index ce1004c4c58c..74e5612c0486 100644 --- a/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecConfiguratorTest.java +++ b/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecConfiguratorTest.java @@ -19,6 +19,7 @@ package com.android.loudnesscodecapitest; import static android.media.audio.Flags.FLAG_LOUDNESS_CONFIGURATOR_API; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -191,6 +192,18 @@ public class LoudnessCodecConfiguratorTest { @Test @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API) + public void addUnconfiguredMediaCodec_returnsFalse() throws Exception { + final MediaCodec mediaCodec = MediaCodec.createDecoderByType("audio/mpeg"); + + try { + assertFalse(mLcc.addMediaCodec(mediaCodec)); + } finally { + mediaCodec.release(); + } + } + + @Test + @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API) public void setClearTrack_removeAllAudioServicePiidCodecs() throws Exception { final ArgumentCaptor<List> argument = ArgumentCaptor.forClass(List.class); final AudioTrack track = createAudioTrack(); diff --git a/packages/CompanionDeviceManager/res/values-ar/strings.xml b/packages/CompanionDeviceManager/res/values-ar/strings.xml index 5b893b0e1b37..6ddd5d3a5240 100644 --- a/packages/CompanionDeviceManager/res/values-ar/strings.xml +++ b/packages/CompanionDeviceManager/res/values-ar/strings.xml @@ -56,28 +56,18 @@ <string name="permission_nearby_devices" msgid="7530973297737123481">"الأجهزة المجاورة"</string> <string name="permission_media_routing_control" msgid="5498639511586715253">"تغيير جهاز إخراج الوسائط"</string> <string name="permission_storage" msgid="6831099350839392343">"الصور والوسائط"</string> - <!-- no translation found for permission_notifications (4099418516590632909) --> - <skip /> + <string name="permission_notifications" msgid="4099418516590632909">"الإشعارات"</string> <string name="permission_app_streaming" msgid="6009695219091526422">"التطبيقات"</string> <string name="permission_nearby_device_streaming" msgid="1023325519477349499">"البثّ"</string> - <!-- no translation found for permission_phone_summary (8246321093970051702) --> - <skip /> - <!-- no translation found for permission_call_logs_summary (7545243592757693321) --> - <skip /> - <!-- no translation found for permission_sms_summary (8499509535410068616) --> - <skip /> - <!-- no translation found for permission_contacts_summary (2840800622763086808) --> - <skip /> - <!-- no translation found for permission_calendar_summary (8430353935747336165) --> - <skip /> - <!-- no translation found for permission_microphone_summary (4862628553869973259) --> - <skip /> - <!-- no translation found for permission_nearby_devices_summary (1306752848196464817) --> - <skip /> - <!-- no translation found for permission_notification_listener_access_summary (7856071768185367749) --> - <skip /> - <!-- no translation found for permission_notifications_summary (2272810466047367030) --> - <skip /> + <string name="permission_phone_summary" msgid="8246321093970051702">"إجراء المكالمات الهاتفية وإدارتها"</string> + <string name="permission_call_logs_summary" msgid="7545243592757693321">"قراءة سجلّ المكالمات الهاتفية والكتابة إليه"</string> + <string name="permission_sms_summary" msgid="8499509535410068616">"إرسال الرسائل القصيرة وعرضها"</string> + <string name="permission_contacts_summary" msgid="2840800622763086808">"الوصول إلى جهات اتصالك"</string> + <string name="permission_calendar_summary" msgid="8430353935747336165">"الوصول إلى تقويمك"</string> + <string name="permission_microphone_summary" msgid="4862628553869973259">"تسجيل الصوت"</string> + <string name="permission_nearby_devices_summary" msgid="1306752848196464817">"يمكن العثور على الموضع النسبي للأجهزة المجاورة والربط بها وتحديدها."</string> + <string name="permission_notification_listener_access_summary" msgid="7856071768185367749">"يمكن لهذا الملف الشخصي قراءة جميع الإشعارات، بما في ذلك المعلومات، مثل جهات الاتصال والرسائل والصور."</string> + <string name="permission_notifications_summary" msgid="2272810466047367030">"• قراءة كل الإشعارات بما فيها المعلومات، مثل جهات الاتصال والرسائل والصور<br/>• إرسال الإشعارات<br/><br/>يمكنك إدارة الإذن الممنوح لهذا التطبيق بقراءة الإشعارات وإرسالها في أي وقت من خلال الإعدادات > الإشعارات."</string> <string name="permission_app_streaming_summary" msgid="606923325679670624">"بث تطبيقات هاتفك"</string> <string name="permission_storage_summary" msgid="3918240895519506417"></string> <string name="permission_nearby_device_streaming_summary" msgid="8280824871197081246">"بثّ التطبيقات وميزات النظام الأخرى من هاتفك"</string> diff --git a/packages/CredentialManager/res/drawable/more_horiz_24px.xml b/packages/CredentialManager/res/drawable/more_horiz_24px.xml new file mode 100644 index 000000000000..7b235f84f0fa --- /dev/null +++ b/packages/CredentialManager/res/drawable/more_horiz_24px.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M240,560Q207,560 183.5,536.5Q160,513 160,480Q160,447 183.5,423.5Q207,400 240,400Q273,400 296.5,423.5Q320,447 320,480Q320,513 296.5,536.5Q273,560 240,560ZM480,560Q447,560 423.5,536.5Q400,513 400,480Q400,447 423.5,423.5Q447,400 480,400Q513,400 536.5,423.5Q560,447 560,480Q560,513 536.5,536.5Q513,560 480,560ZM720,560Q687,560 663.5,536.5Q640,513 640,480Q640,447 663.5,423.5Q687,400 720,400Q753,400 776.5,423.5Q800,447 800,480Q800,513 776.5,536.5Q753,560 720,560Z"/> +</vector> diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index a78509d897aa..c0d71494e020 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -53,6 +53,7 @@ class CredentialManagerRepo( isNewActivity: Boolean, ) { val requestInfo: RequestInfo? + var isReqForAllOptions: Boolean = false private val providerEnabledList: List<ProviderData> private val providerDisabledList: List<DisabledProviderData>? val resultReceiver: ResultReceiver? @@ -102,6 +103,11 @@ class CredentialManagerRepo( ResultReceiver::class.java ) + isReqForAllOptions = intent.getBooleanExtra( + Constants.EXTRA_REQ_FOR_ALL_OPTIONS, + /*defaultValue=*/ false + ) + val cancellationRequest = getCancelUiRequest(intent) val cancelUiRequestState = cancellationRequest?.let { CancelUiRequestState(getAppLabel(context.getPackageManager(), it.appPackageName)) @@ -141,7 +147,8 @@ class CredentialManagerRepo( ) } RequestInfo.TYPE_GET -> { - val getCredentialInitialUiState = getCredentialInitialUiState(originName)!! + val getCredentialInitialUiState = getCredentialInitialUiState(originName, + isReqForAllOptions)!! val autoSelectEntry = findAutoSelectEntry(getCredentialInitialUiState.providerDisplayInfo) UiState( @@ -216,14 +223,18 @@ class CredentialManagerRepo( } // IMPORTANT: new invocation should be mindful that this method can throw. - private fun getCredentialInitialUiState(originName: String?): GetCredentialUiState? { + private fun getCredentialInitialUiState( + originName: String?, + isReqForAllOptions: Boolean + ): GetCredentialUiState? { val providerEnabledList = GetFlowUtils.toProviderList( providerEnabledList as List<GetCredentialProviderData>, context ) val requestDisplayInfo = GetFlowUtils.toRequestDisplayInfo(requestInfo, context, originName) return GetCredentialUiState( - providerEnabledList, - requestDisplayInfo ?: return null, + isReqForAllOptions, + providerEnabledList, + requestDisplayInfo ?: return null ) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt index dfa5735fcaec..8ac364e72fef 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt @@ -305,6 +305,14 @@ class CredentialAutofillService : AutofillService() { var i = 0 var datasetAdded = false + val duplicateDisplayNames: MutableMap<String, Boolean> = mutableMapOf() + providerDisplayInfo.sortedUserNameToCredentialEntryList.forEach { + val credentialEntry = it.sortedCredentialEntryList.first() + credentialEntry.displayName?.let {displayName -> + val duplicateEntry = duplicateDisplayNames.contains(displayName) + duplicateDisplayNames[displayName] = duplicateEntry + } + } providerDisplayInfo.sortedUserNameToCredentialEntryList.forEach usernameLoop@{ val primaryEntry = it.sortedCredentialEntryList.first() val pendingIntent = primaryEntry.pendingIntent @@ -339,10 +347,14 @@ class CredentialAutofillService : AutofillService() { } else { spec = inlinePresentationSpecs[inlinePresentationSpecsCount - 1] } + val displayName : String = primaryEntry.displayName ?: primaryEntry.userName val sliceBuilder = InlineSuggestionUi .newContentBuilder(pendingIntent) - .setTitle(primaryEntry.userName) + .setTitle(displayName) sliceBuilder.setStartIcon(icon) + if (duplicateDisplayNames[displayName] == true) { + sliceBuilder.setSubtitle(primaryEntry.userName) + } inlinePresentation = InlinePresentation( sliceBuilder.build().slice, spec, /* pinned= */ false) } @@ -398,7 +410,7 @@ class CredentialAutofillService : AutofillService() { val sliceBuilder = InlineSuggestionUi .newContentBuilder(bottomSheetPendingIntent) .setStartIcon(Icon.createWithResource(this, - com.android.credentialmanager.R.drawable.ic_other_sign_in_24)) + com.android.credentialmanager.R.drawable.more_horiz_24px)) val presentationBuilder = Presentations.Builder() .setInlinePresentation(InlinePresentation( sliceBuilder.build().slice, spec, /* pinned= */ true)) diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt index 46bebc4073ab..a291f59021f0 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt @@ -26,10 +26,12 @@ import com.android.credentialmanager.model.get.RemoteEntryInfo import com.android.internal.util.Preconditions data class GetCredentialUiState( + val isRequestForAllOptions: Boolean, val providerInfoList: List<ProviderInfo>, val requestDisplayInfo: RequestDisplayInfo, val providerDisplayInfo: ProviderDisplayInfo = toProviderDisplayInfo(providerInfoList), - val currentScreenState: GetScreenState = toGetScreenState(providerDisplayInfo), + val currentScreenState: GetScreenState = toGetScreenState( + providerDisplayInfo, isRequestForAllOptions), val activeEntry: EntryInfo? = toActiveEntry(providerDisplayInfo), val isNoAccount: Boolean = false, ) @@ -184,7 +186,8 @@ private fun toActiveEntry( } private fun toGetScreenState( - providerDisplayInfo: ProviderDisplayInfo + providerDisplayInfo: ProviderDisplayInfo, + isRequestForAllOptions: Boolean ): GetScreenState { return if (providerDisplayInfo.sortedUserNameToCredentialEntryList.isEmpty() && providerDisplayInfo.remoteEntry == null && @@ -194,6 +197,8 @@ private fun toGetScreenState( providerDisplayInfo.authenticationEntryList.isEmpty() && providerDisplayInfo.remoteEntry != null) GetScreenState.REMOTE_ONLY + else if (isRequestForAllOptions) + GetScreenState.ALL_SIGN_IN_OPTIONS else GetScreenState.PRIMARY_SELECTION } diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt index 2a7e9e16a10e..59e6142f3687 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt @@ -21,11 +21,14 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.android.credentialmanager.model.Request import com.android.credentialmanager.client.CredentialManagerClient +import com.android.credentialmanager.model.get.ActionEntryInfo +import com.android.credentialmanager.model.get.CredentialEntryInfo import com.android.credentialmanager.ui.mappers.toGet import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import javax.inject.Inject @@ -33,15 +36,15 @@ import javax.inject.Inject class CredentialSelectorViewModel @Inject constructor( private val credentialManagerClient: CredentialManagerClient, ) : ViewModel() { - + private val isPrimaryScreen = MutableStateFlow(false) val uiState: StateFlow<CredentialSelectorUiState> = credentialManagerClient.requests - .map { request -> + .combine(isPrimaryScreen) { request, isPrimary -> when (request) { null -> CredentialSelectorUiState.Idle is Request.Cancel -> CredentialSelectorUiState.Cancel(request.appName) is Request.Close -> CredentialSelectorUiState.Close is Request.Create -> CredentialSelectorUiState.Create - is Request.Get -> request.toGet() + is Request.Get -> request.toGet(isPrimary) } } .stateIn( @@ -57,9 +60,18 @@ class CredentialSelectorViewModel @Inject constructor( sealed class CredentialSelectorUiState { data object Idle : CredentialSelectorUiState() - sealed class Get : CredentialSelectorUiState() { - data object SingleProviderSinglePasskey : Get() - data object SingleProviderSinglePassword : Get() + sealed class Get() : CredentialSelectorUiState() { + data class SingleEntry(val entry: CredentialEntryInfo) : Get() + data class SingleEntryPerAccount(val sortedEntries: List<CredentialEntryInfo>) : Get() + data class MultipleEntry( + val accounts: List<PerUserNameEntries>, + val actionEntryList: List<ActionEntryInfo>, + ) : Get() { + data class PerUserNameEntries( + val userName: String, + val sortedCredentialEntryList: List<CredentialEntryInfo>, + ) + } // TODO: b/301206470 add the remaining states } diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt index 7e0ea3077559..790f5b1f27f3 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt @@ -26,6 +26,7 @@ import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState import androidx.wear.compose.navigation.rememberSwipeDismissableNavController import androidx.wear.compose.navigation.rememberSwipeDismissableNavHostState import com.android.credentialmanager.CredentialSelectorUiState +import com.android.credentialmanager.CredentialSelectorUiState.Get.SingleEntry import com.android.credentialmanager.CredentialSelectorViewModel import com.android.credentialmanager.ui.screens.LoadingScreen import com.android.credentialmanager.ui.screens.single.password.SinglePasswordScreen @@ -57,6 +58,7 @@ fun WearApp( scrollable(Screen.SinglePasswordScreen.route) { SinglePasswordScreen( + state = viewModel.uiState.value as SingleEntry, columnState = it.columnState, onCloseApp = onCloseApp, ) @@ -100,7 +102,7 @@ private fun handleGetNavigation( onCloseApp: () -> Unit, ) { when (state) { - is CredentialSelectorUiState.Get.SingleProviderSinglePassword -> { + is SingleEntry -> { navController.navigateToSinglePasswordScreen() } diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt index 14b992a0d0e9..44a838d51a04 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt @@ -18,18 +18,45 @@ package com.android.credentialmanager.ui.mappers import com.android.credentialmanager.model.Request import com.android.credentialmanager.CredentialSelectorUiState +import com.android.credentialmanager.CredentialSelectorUiState.Get.MultipleEntry.PerUserNameEntries +import com.android.credentialmanager.model.CredentialType +import com.android.credentialmanager.model.get.CredentialEntryInfo -fun Request.Get.toGet(): CredentialSelectorUiState.Get { +fun Request.Get.toGet(isPrimary: Boolean): CredentialSelectorUiState.Get { // TODO: b/301206470 returning a hard coded state for MVP - if (true) return CredentialSelectorUiState.Get.SingleProviderSinglePassword - - return if (providerInfos.size == 1) { - if (providerInfos.first().credentialEntryList.size == 1) { - CredentialSelectorUiState.Get.SingleProviderSinglePassword + if (true) return CredentialSelectorUiState.Get.SingleEntry( + providerInfos + .flatMap { it.credentialEntryList } + .first { it.credentialType == CredentialType.PASSWORD } + ) + val accounts = providerInfos + .flatMap { it.credentialEntryList } + .groupBy { it.userName} + .entries + .toList() + return if (isPrimary) { + if (accounts.size == 1) { + CredentialSelectorUiState.Get.SingleEntry( + accounts[0].value.minWith(comparator) + ) } else { - TODO() // b/301206470 - Implement other get flows + CredentialSelectorUiState.Get.SingleEntryPerAccount( + accounts.map { it.value.minWith(comparator) }.sortedWith(comparator) + ) } } else { - TODO() // b/301206470 - Implement other get flows + CredentialSelectorUiState.Get.MultipleEntry( + accounts = accounts.map { PerUserNameEntries( + it.key, + it.value.sortedWith(comparator) + ) + }, + actionEntryList = providerInfos.flatMap { it.actionEntryList }, + ) } } +val comparator = compareBy<CredentialEntryInfo> { entryInfo -> + // Passkey type always go first + entryInfo.credentialType.let{ if (it == CredentialType.PASSKEY) 0 else 1 } +} + .thenByDescending{ it.lastUsedTimeMillis } diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt index b64f58192d23..9f971ae1e327 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.credentialmanager.CredentialSelectorUiState.Get.SingleEntry import com.android.credentialmanager.R import com.android.credentialmanager.TAG import com.android.credentialmanager.activity.StartBalIntentSenderForResultContract @@ -44,12 +45,13 @@ import com.google.android.horologist.compose.tools.WearPreview @Composable fun SinglePasswordScreen( + state: SingleEntry, columnState: ScalingLazyColumnState, onCloseApp: () -> Unit, modifier: Modifier = Modifier, viewModel: SinglePasswordScreenViewModel = hiltViewModel(), ) { - viewModel.initialize() + viewModel.initialize(state.entry) val uiState by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreenViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreenViewModel.kt index 26bee1f8d191..4f9fc46e0fce 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreenViewModel.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreenViewModel.kt @@ -19,12 +19,9 @@ package com.android.credentialmanager.ui.screens.single.password import android.content.Intent import android.credentials.ui.ProviderPendingIntentResponse import android.credentials.ui.UserSelectionDialogResult -import android.util.Log import androidx.activity.result.IntentSenderRequest import androidx.annotation.MainThread import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.android.credentialmanager.TAG import com.android.credentialmanager.ktx.getIntentSenderRequest import com.android.credentialmanager.model.Request import com.android.credentialmanager.client.CredentialManagerClient @@ -33,7 +30,6 @@ import com.android.credentialmanager.ui.model.PasswordUiModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @@ -51,32 +47,14 @@ class SinglePasswordScreenViewModel @Inject constructor( val uiState: StateFlow<SinglePasswordScreenUiState> = _uiState @MainThread - fun initialize() { + fun initialize(entryInfo: CredentialEntryInfo) { if (initializeCalled) return initializeCalled = true - - viewModelScope.launch { - val request = credentialManagerClient.requests.value - Log.d(TAG, "request: $request, client instance: $credentialManagerClient") - - if (request !is Request.Get) { - _uiState.value = SinglePasswordScreenUiState.Error - } else { - requestGet = request - - if (requestGet.providerInfos.all { it.credentialEntryList.isEmpty() }) { - Log.d(TAG, "Empty passwordEntries") - _uiState.value = SinglePasswordScreenUiState.Error - } else { - entryInfo = requestGet.providerInfos.first().credentialEntryList.first() - _uiState.value = SinglePasswordScreenUiState.Loaded( - PasswordUiModel( - email = entryInfo.userName, - ) - ) - } - } - } + _uiState.value = SinglePasswordScreenUiState.Loaded( + PasswordUiModel( + email = entryInfo.userName, + ) + ) } fun onCancelClick() { diff --git a/packages/InputDevices/res/values-uk/strings.xml b/packages/InputDevices/res/values-uk/strings.xml index 5368f2c64828..71b3496c929f 100644 --- a/packages/InputDevices/res/values-uk/strings.xml +++ b/packages/InputDevices/res/values-uk/strings.xml @@ -3,49 +3,49 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="app_label" msgid="8016145283189546017">"Пристрої вводу"</string> <string name="keyboard_layouts_label" msgid="6688773268302087545">"Клавіатура Android"</string> - <string name="keyboard_layout_english_uk_label" msgid="6664258463319999632">"англійська (Велика Британія)"</string> - <string name="keyboard_layout_english_us_label" msgid="8994890249649106291">"англійська (США)"</string> - <string name="keyboard_layout_english_us_intl" msgid="3705168594034233583">"англійська (США), міжнародна"</string> - <string name="keyboard_layout_english_us_colemak_label" msgid="4194969610343455380">"англійська (США), розкладка Colemak"</string> - <string name="keyboard_layout_english_us_dvorak_label" msgid="793528923171145202">"англійська (США), розкладка Дворака"</string> - <string name="keyboard_layout_english_us_workman_label" msgid="2944541595262173111">"англійська (США), розкладка Workman"</string> - <string name="keyboard_layout_german_label" msgid="8451565865467909999">"німецька"</string> - <string name="keyboard_layout_french_label" msgid="813450119589383723">"французька"</string> - <string name="keyboard_layout_french_ca_label" msgid="365352601060604832">"французька (Канада)"</string> - <string name="keyboard_layout_russian_label" msgid="8724879775815042968">"російська"</string> - <string name="keyboard_layout_russian_mac_label" msgid="3795866869038264796">"російська, розкладка Mac"</string> - <string name="keyboard_layout_spanish_label" msgid="7091555148131908240">"іспанська"</string> - <string name="keyboard_layout_swiss_french_label" msgid="4659191025396371684">"французька (Швейцарія)"</string> - <string name="keyboard_layout_swiss_german_label" msgid="2305520941993314258">"німецька (Швейцарія)"</string> - <string name="keyboard_layout_belgian" msgid="2011984572838651558">"бельгійська"</string> - <string name="keyboard_layout_bulgarian" msgid="8951224309972028398">"болгарська"</string> + <string name="keyboard_layout_english_uk_label" msgid="6664258463319999632">"Англійська (Велика Британія)"</string> + <string name="keyboard_layout_english_us_label" msgid="8994890249649106291">"Англійська (США)"</string> + <string name="keyboard_layout_english_us_intl" msgid="3705168594034233583">"Англійська (США), міжнародна"</string> + <string name="keyboard_layout_english_us_colemak_label" msgid="4194969610343455380">"Англійська (США), розкладка Colemak"</string> + <string name="keyboard_layout_english_us_dvorak_label" msgid="793528923171145202">"Англійська (США), розкладка Дворака"</string> + <string name="keyboard_layout_english_us_workman_label" msgid="2944541595262173111">"Англійська (США), розкладка Workman"</string> + <string name="keyboard_layout_german_label" msgid="8451565865467909999">"Німецька"</string> + <string name="keyboard_layout_french_label" msgid="813450119589383723">"Французька"</string> + <string name="keyboard_layout_french_ca_label" msgid="365352601060604832">"Французька (Канада)"</string> + <string name="keyboard_layout_russian_label" msgid="8724879775815042968">"Російська"</string> + <string name="keyboard_layout_russian_mac_label" msgid="3795866869038264796">"Російська, розкладка Mac"</string> + <string name="keyboard_layout_spanish_label" msgid="7091555148131908240">"Іспанська"</string> + <string name="keyboard_layout_swiss_french_label" msgid="4659191025396371684">"Французька (Швейцарія)"</string> + <string name="keyboard_layout_swiss_german_label" msgid="2305520941993314258">"Німецька (Швейцарія)"</string> + <string name="keyboard_layout_belgian" msgid="2011984572838651558">"Бельгійська"</string> + <string name="keyboard_layout_bulgarian" msgid="8951224309972028398">"Болгарська"</string> <string name="keyboard_layout_bulgarian_phonetic" msgid="7568914730360106653">"Болгарська (фонетична)"</string> - <string name="keyboard_layout_italian" msgid="6497079660449781213">"італійська"</string> - <string name="keyboard_layout_danish" msgid="8036432066627127851">"данська"</string> - <string name="keyboard_layout_norwegian" msgid="9090097917011040937">"норвезька"</string> - <string name="keyboard_layout_swedish" msgid="732959109088479351">"шведська"</string> - <string name="keyboard_layout_finnish" msgid="5585659438924315466">"фінська"</string> - <string name="keyboard_layout_croatian" msgid="4172229471079281138">"хорватська"</string> - <string name="keyboard_layout_czech" msgid="1349256901452975343">"чеська"</string> + <string name="keyboard_layout_italian" msgid="6497079660449781213">"Італійська"</string> + <string name="keyboard_layout_danish" msgid="8036432066627127851">"Данська"</string> + <string name="keyboard_layout_norwegian" msgid="9090097917011040937">"Норвезька"</string> + <string name="keyboard_layout_swedish" msgid="732959109088479351">"Шведська"</string> + <string name="keyboard_layout_finnish" msgid="5585659438924315466">"Фінська"</string> + <string name="keyboard_layout_croatian" msgid="4172229471079281138">"Хорватська"</string> + <string name="keyboard_layout_czech" msgid="1349256901452975343">"Чеська"</string> <string name="keyboard_layout_czech_qwerty" msgid="3331402534128515501">"Чеська (QWERTY)"</string> - <string name="keyboard_layout_estonian" msgid="8775830985185665274">"естонська"</string> - <string name="keyboard_layout_hungarian" msgid="4154963661406035109">"угорська"</string> - <string name="keyboard_layout_icelandic" msgid="5836645650912489642">"ісландська"</string> - <string name="keyboard_layout_brazilian" msgid="5117896443147781939">"бразильська"</string> - <string name="keyboard_layout_portuguese" msgid="2888198587329660305">"португальська"</string> - <string name="keyboard_layout_slovak" msgid="2469379934672837296">"словацька"</string> - <string name="keyboard_layout_slovenian" msgid="1735933028924982368">"словенська"</string> - <string name="keyboard_layout_turkish" msgid="7736163250907964898">"турецька"</string> + <string name="keyboard_layout_estonian" msgid="8775830985185665274">"Естонська"</string> + <string name="keyboard_layout_hungarian" msgid="4154963661406035109">"Угорська"</string> + <string name="keyboard_layout_icelandic" msgid="5836645650912489642">"Ісландська"</string> + <string name="keyboard_layout_brazilian" msgid="5117896443147781939">"Бразильська"</string> + <string name="keyboard_layout_portuguese" msgid="2888198587329660305">"Португальська"</string> + <string name="keyboard_layout_slovak" msgid="2469379934672837296">"Словацька"</string> + <string name="keyboard_layout_slovenian" msgid="1735933028924982368">"Словенська"</string> + <string name="keyboard_layout_turkish" msgid="7736163250907964898">"Турецька"</string> <string name="keyboard_layout_turkish_f" msgid="9130320856010776018">"Турецька-F"</string> - <string name="keyboard_layout_ukrainian" msgid="8176637744389480417">"українська"</string> - <string name="keyboard_layout_arabic" msgid="5671970465174968712">"арабська"</string> + <string name="keyboard_layout_ukrainian" msgid="8176637744389480417">"Українська"</string> + <string name="keyboard_layout_arabic" msgid="5671970465174968712">"Арабська"</string> <string name="keyboard_layout_greek" msgid="7289253560162386040">"Грецька"</string> <string name="keyboard_layout_hebrew" msgid="7241473985890173812">"Іврит"</string> <string name="keyboard_layout_lithuanian" msgid="6943110873053106534">"Литовська"</string> <string name="keyboard_layout_spanish_latin" msgid="5690539836069535697">"Іспанська (латиниця)"</string> <string name="keyboard_layout_latvian" msgid="4405417142306250595">"Латвійська"</string> <string name="keyboard_layout_persian" msgid="3920643161015888527">"Перська"</string> - <string name="keyboard_layout_azerbaijani" msgid="7315895417176467567">"азербайджанська"</string> + <string name="keyboard_layout_azerbaijani" msgid="7315895417176467567">"Азербайджанська"</string> <string name="keyboard_layout_polish" msgid="1121588624094925325">"Польська"</string> <string name="keyboard_layout_belarusian" msgid="7619281752698687588">"Білоруська"</string> <string name="keyboard_layout_mongolian" msgid="7678483495823936626">"Монгольська"</string> diff --git a/packages/PackageInstaller/res/values-ar/strings.xml b/packages/PackageInstaller/res/values-ar/strings.xml index e4da5b736d70..224c6dc90903 100644 --- a/packages/PackageInstaller/res/values-ar/strings.xml +++ b/packages/PackageInstaller/res/values-ar/strings.xml @@ -44,8 +44,7 @@ <string name="unknown_apps_user_restriction_dlg_text" msgid="151020786933988344">"يتعذر على هذا المستخدم تثبيت التطبيقات غير المعروفة"</string> <string name="install_apps_user_restriction_dlg_text" msgid="2154119597001074022">"غير مسموح لهذا المستخدم بتثبيت التطبيقات"</string> <string name="ok" msgid="7871959885003339302">"حسنًا"</string> - <!-- no translation found for archive (4447791830199354721) --> - <skip /> + <string name="archive" msgid="4447791830199354721">"أرشفة"</string> <string name="update_anyway" msgid="8792432341346261969">"التحديث على أي حال"</string> <string name="manage_applications" msgid="5400164782453975580">"إدارة التطبيقات"</string> <string name="out_of_space_dlg_title" msgid="4156690013884649502">"نفدت مساحة التخزين"</string> @@ -60,16 +59,11 @@ <string name="uninstall_update_title" msgid="824411791011583031">"إزالة التحديث"</string> <string name="uninstall_activity_text" msgid="1928194674397770771">"<xliff:g id="ACTIVITY_NAME">%1$s</xliff:g> هو جزء من التطبيق التالي:"</string> <string name="uninstall_application_text" msgid="3816830743706143980">"هل تريد إزالة هذا التطبيق؟"</string> - <!-- no translation found for archive_application_text (8482325710714386348) --> - <skip /> - <!-- no translation found for archive_application_text_all_users (3151229641681672580) --> - <skip /> - <!-- no translation found for archive_application_text_current_user_work_profile (1450487362134779752) --> - <skip /> - <!-- no translation found for archive_application_text_user (2586558895535581451) --> - <skip /> - <!-- no translation found for archive_application_text_current_user_private_profile (1958423158655599132) --> - <skip /> + <string name="archive_application_text" msgid="8482325710714386348">"سيتم حفظ بياناتك الشخصية."</string> + <string name="archive_application_text_all_users" msgid="3151229641681672580">"هل تريد أرشفة هذا التطبيق لجميع المستخدمين؟ سيتم حفظ بياناتك الشخصية."</string> + <string name="archive_application_text_current_user_work_profile" msgid="1450487362134779752">"هل تريد أرشفة هذا التطبيق في ملف العمل؟ سيتم حفظ بياناتك الشخصية."</string> + <string name="archive_application_text_user" msgid="2586558895535581451">"هل تريد أرشفة هذا التطبيق لـ \"<xliff:g id="USERNAME">%1$s</xliff:g>\"؟ سيتم حفظ بياناتك الشخصية."</string> + <string name="archive_application_text_current_user_private_profile" msgid="1958423158655599132">"هل تريد أرشفة هذا التطبيق المحفوظ في المساحة الخاصّة؟ سيتم حفظ بياناتك الشخصية."</string> <string name="uninstall_application_text_all_users" msgid="575491774380227119">"هل تريد إزالة هذا التطبيق "<b>"لكل"</b>" المستخدمين؟ ستتم إزالة التطبيق وبياناته من "<b>"كل"</b>" المستخدمين على هذا الجهاز."</string> <string name="uninstall_application_text_user" msgid="498072714173920526">"هل تريد إزالة هذا التطبيق للمستخدم <xliff:g id="USERNAME">%1$s</xliff:g>؟"</string> <string name="uninstall_application_text_current_user_work_profile" msgid="8788387739022366193">"هل تريد إزالة تثبيت هذا التطبيق من ملفك الشخصي للعمل؟"</string> @@ -108,8 +102,7 @@ <string name="anonymous_source_warning" product="tablet" msgid="3939101621438855516">"يعتبر الجهاز اللوحي والبيانات الشخصية أكثر عرضة لهجوم التطبيقات غير المعروفة. من خلال تثبيت هذا التطبيق، توافق على تحمل مسؤولية أي ضرر يحدث للجهاز اللوحي أو فقدان البيانات الذي قد ينتج عن استخدامه."</string> <string name="anonymous_source_warning" product="tv" msgid="5599483539528168566">"يعتبر جهاز التلفزيون والبيانات الشخصية أكثر عرضة لهجوم التطبيقات غير المعروفة. من خلال تثبيت هذا التطبيق، توافق على تحمل مسؤولية أي ضرر يحدث لجهاز التلفزيون أو فقدان البيانات الذي قد ينتج عن استخدامه."</string> <string name="cloned_app_label" msgid="7503612829833756160">"نسخة طبق الأصل من \"<xliff:g id="PACKAGE_LABEL">%1$s</xliff:g>\""</string> - <!-- no translation found for archiving_app_label (1127085259724124725) --> - <skip /> + <string name="archiving_app_label" msgid="1127085259724124725">"هل تريد أرشفة <xliff:g id="PACKAGE_LABEL">%1$s</xliff:g>؟"</string> <string name="anonymous_source_continue" msgid="4375745439457209366">"متابعة"</string> <string name="external_sources_settings" msgid="4046964413071713807">"الإعدادات"</string> <string name="wear_app_channel" msgid="1960809674709107850">"تثبيت / إلغاء تثبيت تطبيقات Android Wear"</string> diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java index 754437e64e78..b5af845ea0ac 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java @@ -76,7 +76,7 @@ public class UnarchiveActivity extends Activity { boolean hasRequestInstallPermission = Arrays.asList(getRequestedPermissions(callingPackage)) .contains(permission.REQUEST_INSTALL_PACKAGES); boolean hasInstallPermission = getBaseContext().checkPermission(permission.INSTALL_PACKAGES, - 0 /* random value for pid */, callingUid) != PackageManager.PERMISSION_GRANTED; + 0 /* random value for pid */, callingUid) == PackageManager.PERMISSION_GRANTED; if (!hasRequestInstallPermission && !hasInstallPermission) { Log.e(TAG, "Uid " + callingUid + " does not have " + permission.REQUEST_INSTALL_PACKAGES + " or " diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveFragment.java index 6ccbc4cb5e6b..42dd382b98bc 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveFragment.java @@ -28,12 +28,13 @@ public class UnarchiveFragment extends DialogFragment implements @Override public Dialog onCreateDialog(Bundle savedInstanceState) { String appTitle = getArguments().getString(UnarchiveActivity.APP_TITLE); + String installerTitle = getArguments().getString(UnarchiveActivity.INSTALLER_TITLE); AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity()); dialogBuilder.setTitle( String.format(getContext().getString(R.string.unarchive_application_title), - appTitle)); + appTitle, installerTitle)); dialogBuilder.setMessage(R.string.unarchive_body_text); dialogBuilder.setPositiveButton(R.string.restore, this); diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/AnonymousSourceFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/AnonymousSourceFragment.java index 679f696ff59f..b29cb2ab308c 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/AnonymousSourceFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/AnonymousSourceFragment.java @@ -34,7 +34,10 @@ import com.android.packageinstaller.v2.ui.InstallActionListener; public class AnonymousSourceFragment extends DialogFragment { public static String TAG = AnonymousSourceFragment.class.getSimpleName(); + @NonNull private InstallActionListener mInstallActionListener; + @NonNull + private AlertDialog mDialog; @Override public void onAttach(@NonNull Context context) { @@ -45,7 +48,7 @@ public class AnonymousSourceFragment extends DialogFragment { @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - return new AlertDialog.Builder(getActivity()) + mDialog = new AlertDialog.Builder(requireContext()) .setMessage(R.string.anonymous_source_warning) .setPositiveButton(R.string.anonymous_source_continue, ((dialog, which) -> mInstallActionListener.onPositiveResponse( @@ -53,6 +56,7 @@ public class AnonymousSourceFragment extends DialogFragment { .setNegativeButton(R.string.cancel, ((dialog, which) -> mInstallActionListener.onNegativeResponse( InstallStage.STAGE_USER_ACTION_REQUIRED))).create(); + return mDialog; } @Override @@ -60,4 +64,24 @@ public class AnonymousSourceFragment extends DialogFragment { super.onCancel(dialog); mInstallActionListener.onNegativeResponse(InstallStage.STAGE_USER_ACTION_REQUIRED); } + + @Override + public void onStart() { + super.onStart(); + mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true); + } + + @Override + public void onPause() { + super.onPause(); + // This prevents tapjacking since an overlay activity started in front of Pia will + // cause Pia to be paused. + mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false); + } + + @Override + public void onResume() { + super.onResume(); + mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true); + } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java index 49901de96bc4..2314d6b3b47e 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java @@ -35,8 +35,12 @@ import com.android.packageinstaller.v2.ui.InstallActionListener; public class ExternalSourcesBlockedFragment extends DialogFragment { private final String TAG = ExternalSourcesBlockedFragment.class.getSimpleName(); + @NonNull private final InstallUserActionRequired mDialogData; + @NonNull private InstallActionListener mInstallActionListener; + @NonNull + private AlertDialog mDialog; public ExternalSourcesBlockedFragment(InstallUserActionRequired dialogData) { mDialogData = dialogData; @@ -51,7 +55,7 @@ public class ExternalSourcesBlockedFragment extends DialogFragment { @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - return new AlertDialog.Builder(requireContext()) + mDialog = new AlertDialog.Builder(requireContext()) .setTitle(mDialogData.getAppLabel()) .setIcon(mDialogData.getAppIcon()) .setMessage(R.string.untrusted_external_source_warning) @@ -62,6 +66,7 @@ public class ExternalSourcesBlockedFragment extends DialogFragment { (dialog, which) -> mInstallActionListener.onNegativeResponse( mDialogData.getStageCode())) .create(); + return mDialog; } @Override @@ -69,4 +74,24 @@ public class ExternalSourcesBlockedFragment extends DialogFragment { super.onCancel(dialog); mInstallActionListener.onNegativeResponse(mDialogData.getStageCode()); } + + @Override + public void onStart() { + super.onStart(); + mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true); + } + + @Override + public void onPause() { + super.onPause(); + // This prevents tapjacking since an overlay activity started in front of Pia will + // cause Pia to be paused. + mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false); + } + + @Override + public void onResume() { + super.onResume(); + mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true); + } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java index 25363d0b5f7b..dbe32cc42d1a 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java @@ -42,6 +42,8 @@ public class InstallConfirmationFragment extends DialogFragment { private final InstallUserActionRequired mDialogData; @NonNull private InstallActionListener mInstallActionListener; + @NonNull + private AlertDialog mDialog; public InstallConfirmationFragment(@NonNull InstallUserActionRequired dialogData) { mDialogData = dialogData; @@ -58,20 +60,29 @@ public class InstallConfirmationFragment extends DialogFragment { public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { View dialogView = getLayoutInflater().inflate(R.layout.install_content_view, null); - AlertDialog dialog = new AlertDialog.Builder(requireContext()) + int positiveBtnTextRes; + if (mDialogData.isAppUpdating()) { + if (mDialogData.getDialogMessage() != null) { + positiveBtnTextRes = R.string.update_anyway; + } else { + positiveBtnTextRes = R.string.update; + } + } else { + positiveBtnTextRes = R.string.install; + } + + mDialog = new AlertDialog.Builder(requireContext()) .setIcon(mDialogData.getAppIcon()) .setTitle(mDialogData.getAppLabel()) .setView(dialogView) - .setPositiveButton(mDialogData.isAppUpdating() ? R.string.update : R.string.install, + .setPositiveButton(positiveBtnTextRes, (dialogInt, which) -> mInstallActionListener.onPositiveResponse( InstallUserActionRequired.USER_ACTION_REASON_INSTALL_CONFIRMATION)) .setNegativeButton(R.string.cancel, (dialogInt, which) -> mInstallActionListener.onNegativeResponse( mDialogData.getStageCode())) - .create(); - // TODO: Dynamically change positive button text to update anyway TextView viewToEnable; if (mDialogData.isAppUpdating()) { viewToEnable = dialogView.requireViewById(R.id.install_confirm_question_update); @@ -84,7 +95,7 @@ public class InstallConfirmationFragment extends DialogFragment { } viewToEnable.setVisibility(View.VISIBLE); - return dialog; + return mDialog; } @Override @@ -92,4 +103,24 @@ public class InstallConfirmationFragment extends DialogFragment { super.onCancel(dialog); mInstallActionListener.onNegativeResponse(mDialogData.getStageCode()); } + + @Override + public void onStart() { + super.onStart(); + mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true); + } + + @Override + public void onPause() { + super.onPause(); + // This prevents tapjacking since an overlay activity started in front of Pia will + // cause Pia to be paused. + mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false); + } + + @Override + public void onResume() { + super.onResume(); + mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true); + } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt index f4edb36b5e76..460a6f7261ae 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt @@ -24,7 +24,8 @@ import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider import com.android.settingslib.spa.gallery.card.CardPageProvider import com.android.settingslib.spa.gallery.chart.ChartPageProvider -import com.android.settingslib.spa.gallery.dialog.AlertDialogPageProvider +import com.android.settingslib.spa.gallery.dialog.DialogMainPageProvider +import com.android.settingslib.spa.gallery.dialog.NavDialogProvider import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider import com.android.settingslib.spa.gallery.editor.SettingsExposedDropdownMenuBoxPageProvider import com.android.settingslib.spa.gallery.editor.SettingsExposedDropdownMenuCheckBoxProvider @@ -91,7 +92,8 @@ class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) { ProgressBarPageProvider, LoadingBarPageProvider, ChartPageProvider, - AlertDialogPageProvider, + DialogMainPageProvider, + NavDialogProvider, ItemListPageProvider, ItemOperatePageProvider, OperateListPageProvider, diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/AlertDialogPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/DialogMainPageProvider.kt index 1545a3e886b4..4e3fcee5383e 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/AlertDialogPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/DialogMainPageProvider.kt @@ -28,10 +28,10 @@ import com.android.settingslib.spa.widget.dialog.rememberAlertDialogPresenter import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel -private const val TITLE = "AlertDialogPage" +private const val TITLE = "Category: Dialog" -object AlertDialogPageProvider : SettingsPageProvider { - override val name = "AlertDialogPage" +object DialogMainPageProvider : SettingsPageProvider { + override val name = "DialogMain" private val owner = createSettingsPage() override fun buildEntry(arguments: Bundle?): List<SettingsEntry> = listOf( @@ -47,6 +47,12 @@ object AlertDialogPageProvider : SettingsPageProvider { override val onClick = alertDialogPresenter::open }) }.build(), + SettingsEntryBuilder.create("NavDialog", owner).setUiLayoutFn { + Preference(object : PreferenceModel { + override val title = "Navigate to Dialog" + override val onClick = navigator(route = NavDialogProvider.name) + }) + }.build(), ) fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner) diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/NavDialogProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/NavDialogProvider.kt new file mode 100644 index 000000000000..6f79911225b7 --- /dev/null +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/NavDialogProvider.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.gallery.dialog + +import android.os.Bundle +import androidx.compose.runtime.Composable +import com.android.settingslib.spa.framework.common.SettingsPageProvider +import com.android.settingslib.spa.widget.dialog.SettingsDialogCard + +object NavDialogProvider : SettingsPageProvider { + override val name = "NavDialog" + override val navType = SettingsPageProvider.NavType.Dialog + + @Composable + override fun Page(arguments: Bundle?) { + SettingsDialogCard("Example Nav Dialog") {} + } +} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt index 6a2e5985a735..1f028d5e7bc9 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt @@ -30,7 +30,7 @@ import com.android.settingslib.spa.gallery.SettingsPageProviderEnum import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider import com.android.settingslib.spa.gallery.card.CardPageProvider import com.android.settingslib.spa.gallery.chart.ChartPageProvider -import com.android.settingslib.spa.gallery.dialog.AlertDialogPageProvider +import com.android.settingslib.spa.gallery.dialog.DialogMainPageProvider import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider import com.android.settingslib.spa.gallery.itemList.OperateListPageProvider import com.android.settingslib.spa.gallery.page.ArgumentPageModel @@ -71,7 +71,7 @@ object HomePageProvider : SettingsPageProvider { ProgressBarPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), LoadingBarPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), ChartPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - AlertDialogPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), + DialogMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), EditorMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), CardPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), CopyablePageProvider.buildInjectEntry().setLink(fromPage = owner).build(), 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 9f8c868f4aa4..5605485c4b84 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,7 +22,6 @@ import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.annotation.VisibleForTesting -import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -30,15 +29,19 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.core.view.WindowCompat +import androidx.navigation.NavBackStackEntry import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import androidx.navigation.compose.dialog import androidx.navigation.compose.rememberNavController import com.android.settingslib.spa.R import com.android.settingslib.spa.framework.common.LogCategory import com.android.settingslib.spa.framework.common.NullPageProvider import com.android.settingslib.spa.framework.common.SettingsPage import com.android.settingslib.spa.framework.common.SettingsPageProvider +import com.android.settingslib.spa.framework.common.SettingsPageProvider.NavType import com.android.settingslib.spa.framework.common.SettingsPageProviderRepository import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory import com.android.settingslib.spa.framework.common.createSettingsPage @@ -127,27 +130,31 @@ private fun NavControllerWrapperImpl.NavContent( allProvider: Collection<SettingsPageProvider>, content: @Composable (SettingsPage) -> Unit, ) { - // TODO(b/298520326): Remove Box after the issue is fixed. - // Wrap the top level node into a Box to workaround an issue of Compose 1.6.0-alpha03. - Box { - NavHost( - navController = navController, - startDestination = NullPageProvider.name, - ) { - composable(NullPageProvider.name) {} - for (spp in allProvider) { - animatedComposable( - route = spp.name + spp.parameter.navRoute(), - arguments = spp.parameter, - ) { navBackStackEntry -> - val page = remember { spp.createSettingsPage(navBackStackEntry.arguments) } - content(page) - } + NavHost( + navController = navController, + startDestination = NullPageProvider.name, + ) { + composable(NullPageProvider.name) {} + for (spp in allProvider) { + destination(spp) { navBackStackEntry -> + val page = remember { spp.createSettingsPage(navBackStackEntry.arguments) } + content(page) } } } } +private fun NavGraphBuilder.destination( + spp: SettingsPageProvider, + content: @Composable (NavBackStackEntry) -> Unit, +) { + val route = spp.name + spp.parameter.navRoute() + when (spp.navType) { + NavType.Page -> animatedComposable(route, spp.parameter) { content(it) } + NavType.Dialog -> dialog(route, spp.parameter) { content(it) } + } +} + @Composable private fun NavControllerWrapperImpl.InitialDestination( initialIntent: Intent?, 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 18f964ed7e6d..81bee5ec0b94 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 @@ -34,6 +34,14 @@ interface SettingsPageProvider { /** The page provider name, needs to be *unique* and *stable*. */ val name: String + enum class NavType { + Page, + Dialog, + } + + val navType: NavType + get() = NavType.Page + /** The display name of this page provider, for better readability. */ val displayName: String get() = name diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsDialog.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsDialog.kt index 8b172da08dd2..f08e7400d734 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsDialog.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsDialog.kt @@ -19,10 +19,13 @@ package com.android.settingslib.spa.widget.dialog import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.window.Dialog +import androidx.navigation.compose.NavHost import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsShape import com.android.settingslib.spa.widget.ui.SettingsTitle @@ -34,13 +37,27 @@ fun SettingsDialog( content: @Composable () -> Unit, ) { Dialog(onDismissRequest = onDismissRequest) { - Card(shape = SettingsShape.CornerExtraLarge) { - Column(modifier = Modifier.padding(vertical = SettingsDimension.itemPaddingAround)) { - Box(modifier = Modifier.padding(SettingsDimension.dialogItemPadding)) { - SettingsTitle(title = title, useMediumWeight = true) - } - content() + SettingsDialogCard(title, content) + } +} + +/** + * Card for dialog, suitable for independent dialog in the [NavHost]. + */ +@Composable +fun SettingsDialogCard( + title: String, + content: @Composable () -> Unit, +) { + Card( + shape = SettingsShape.CornerExtraLarge, + colors = CardDefaults.cardColors(containerColor = AlertDialogDefaults.containerColor), + ) { + Column(modifier = Modifier.padding(vertical = SettingsDimension.itemPaddingAround)) { + Box(modifier = Modifier.padding(SettingsDimension.dialogItemPadding)) { + SettingsTitle(title = title, useMediumWeight = true) } + content() } } } diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/BrowseActivityTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/BrowseActivityTest.kt index 92d3411b72f3..8cbd964bbd67 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/BrowseActivityTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/BrowseActivityTest.kt @@ -18,6 +18,7 @@ package com.android.settingslib.spa.framework import android.content.Context import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onNodeWithText @@ -29,12 +30,14 @@ import com.android.settingslib.spa.framework.common.LogEvent import com.android.settingslib.spa.framework.common.SettingsPage import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory import com.android.settingslib.spa.framework.common.createSettingsPage +import com.android.settingslib.spa.tests.testutils.SppDialog import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest import com.android.settingslib.spa.tests.testutils.SpaLoggerForTest import com.android.settingslib.spa.tests.testutils.SppDisabled import com.android.settingslib.spa.tests.testutils.SppHome import com.android.settingslib.spa.testutils.waitUntil -import com.google.common.truth.Truth +import com.android.settingslib.spa.testutils.waitUntilExists +import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -106,11 +109,31 @@ class BrowseActivityTest { composeTestRule.onNodeWithText(sppDisabled.getTitle(null)).assertDoesNotExist() spaLogger.verifyPageEvent(pageDisabled.id, 0, 0) } + + @Test + fun browseContent_dialog() { + val spaEnvironment = SpaEnvironmentForTest( + context = context, + rootPages = listOf(SppHome.createSettingsPage()), + logger = spaLogger, + ) + SpaEnvironmentFactory.reset(spaEnvironment) + val sppRepository by spaEnvironment.pageProviderRepository + + composeTestRule.setContent { + BrowseContent( + sppRepository = sppRepository, + isPageEnabled = SettingsPage::isEnabled, + initialIntent = null, + ) + } + composeTestRule.onNodeWithText(SppDialog.name).performClick() + + composeTestRule.waitUntilExists(hasText(SppDialog.CONTENT)) + } } private fun SpaLoggerForTest.verifyPageEvent(id: String, entryCount: Int, leaveCount: Int) { - Truth.assertThat(getEventCount(id, LogEvent.PAGE_ENTER, LogCategory.FRAMEWORK)) - .isEqualTo(entryCount) - Truth.assertThat(getEventCount(id, LogEvent.PAGE_LEAVE, LogCategory.FRAMEWORK)) - .isEqualTo(leaveCount) + assertThat(getEventCount(id, LogEvent.PAGE_ENTER, LogCategory.FRAMEWORK)).isEqualTo(entryCount) + assertThat(getEventCount(id, LogEvent.PAGE_LEAVE, LogCategory.FRAMEWORK)).isEqualTo(leaveCount) } diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryRepositoryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryRepositoryTest.kt index b139f2874a36..0a1c05f38f2d 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryRepositoryTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryRepositoryTest.kt @@ -22,6 +22,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settingslib.spa.framework.util.genEntryId import com.android.settingslib.spa.framework.util.genPageId import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest +import com.android.settingslib.spa.tests.testutils.SppDialog import com.android.settingslib.spa.tests.testutils.SppHome import com.android.settingslib.spa.tests.testutils.SppLayer1 import com.android.settingslib.spa.tests.testutils.SppLayer2 @@ -39,26 +40,21 @@ class SettingsEntryRepositoryTest { @Test fun testGetPageWithEntry() { val pageWithEntry = entryRepository.getAllPageWithEntry() - assertThat(pageWithEntry.size).isEqualTo(3) - assertThat( - entryRepository.getPageWithEntry(genPageId("SppHome")) - ?.entries?.size - ).isEqualTo(1) - assertThat( - entryRepository.getPageWithEntry(genPageId("SppLayer1")) - ?.entries?.size - ).isEqualTo(3) - assertThat( - entryRepository.getPageWithEntry(genPageId("SppLayer2")) - ?.entries?.size - ).isEqualTo(2) + + assertThat(pageWithEntry).hasSize(4) + assertThat(entryRepository.getPageWithEntry(genPageId("SppHome"))?.entries) + .hasSize(2) + assertThat(entryRepository.getPageWithEntry(genPageId("SppLayer1"))?.entries) + .hasSize(3) + assertThat(entryRepository.getPageWithEntry(genPageId("SppLayer2"))?.entries) + .hasSize(2) assertThat(entryRepository.getPageWithEntry(genPageId("SppWithParam"))).isNull() } @Test fun testGetEntry() { val entry = entryRepository.getAllEntries() - assertThat(entry.size).isEqualTo(7) + assertThat(entry).hasSize(8) assertThat( entryRepository.getEntry( genEntryId( @@ -91,6 +87,16 @@ class SettingsEntryRepositoryTest { ).isNotNull() assertThat( entryRepository.getEntry( + genEntryId( + "INJECT", + SppDialog.createSettingsPage(), + SppHome.createSettingsPage(), + SppDialog.createSettingsPage(), + ) + ) + ).isNotNull() + assertThat( + entryRepository.getEntry( genEntryId("Layer1Entry1", SppLayer1.createSettingsPage()) ) ).isNotNull() diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageProviderRepositoryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageProviderRepositoryTest.kt index 857657331126..169c541e8b5f 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageProviderRepositoryTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageProviderRepositoryTest.kt @@ -25,39 +25,55 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class SettingsPageProviderRepositoryTest { @Test - fun getStartPageTest() { - val sppRepoEmpty = SettingsPageProviderRepository(emptyList(), emptyList()) + fun rootPages_empty() { + val sppRepoEmpty = SettingsPageProviderRepository(emptyList()) + assertThat(sppRepoEmpty.getDefaultStartPage()).isEqualTo("") assertThat(sppRepoEmpty.getAllRootPages()).isEmpty() + } + @Test + fun rootPages_single() { val nullPage = NullPageProvider.createSettingsPage() - val sppRepoNull = - SettingsPageProviderRepository(emptyList(), listOf(nullPage)) + + val sppRepoNull = SettingsPageProviderRepository( + allPageProviders = emptyList(), + rootPages = listOf(nullPage), + ) + assertThat(sppRepoNull.getDefaultStartPage()).isEqualTo("NULL") - assertThat(sppRepoNull.getAllRootPages()).contains(nullPage) + assertThat(sppRepoNull.getAllRootPages()).containsExactly(nullPage) + } + @Test + fun rootPages_twoPages() { val rootPage1 = createSettingsPage(sppName = "Spp1", displayName = "Spp1") val rootPage2 = createSettingsPage(sppName = "Spp2", displayName = "Spp2") - val sppRepo = SettingsPageProviderRepository(emptyList(), listOf(rootPage1, rootPage2)) - val allRoots = sppRepo.getAllRootPages() + + val sppRepo = SettingsPageProviderRepository( + allPageProviders = emptyList(), + rootPages = listOf(rootPage1, rootPage2), + ) + assertThat(sppRepo.getDefaultStartPage()).isEqualTo("Spp1") - assertThat(allRoots.size).isEqualTo(2) - assertThat(allRoots).contains(rootPage1) - assertThat(allRoots).contains(rootPage2) + assertThat(sppRepo.getAllRootPages()).containsExactly(rootPage1, rootPage2) } @Test - fun getProviderTest() { - val sppRepoEmpty = SettingsPageProviderRepository(emptyList(), emptyList()) + fun getProviderOrNull_empty() { + val sppRepoEmpty = SettingsPageProviderRepository(emptyList()) assertThat(sppRepoEmpty.getAllProviders()).isEmpty() assertThat(sppRepoEmpty.getProviderOrNull("Spp")).isNull() + } + @Test + fun getProviderOrNull_single() { val sppRepo = SettingsPageProviderRepository(listOf( object : SettingsPageProvider { override val name = "Spp" } - ), emptyList()) - assertThat(sppRepo.getAllProviders().size).isEqualTo(1) + )) + assertThat(sppRepo.getAllProviders()).hasSize(1) assertThat(sppRepo.getProviderOrNull("Spp")).isNotNull() assertThat(sppRepo.getProviderOrNull("SppUnknown")).isNull() } diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt index 2755b4e18154..22a5ca328755 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt @@ -21,6 +21,8 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable import androidx.navigation.NavType import androidx.navigation.navArgument import com.android.settingslib.spa.framework.BrowseActivity @@ -88,6 +90,7 @@ object SppHome : SettingsPageProvider { val owner = this.createSettingsPage() return listOf( SppLayer1.buildInject().setLink(fromPage = owner).build(), + SppDialog.buildInject().setLink(fromPage = owner).build(), ) } } @@ -160,6 +163,21 @@ object SppLayer2 : SettingsPageProvider { } } +object SppDialog : SettingsPageProvider { + override val name = "SppDialog" + override val navType = SettingsPageProvider.NavType.Dialog + + const val CONTENT = "SppDialog Content" + + @Composable + override fun Page(arguments: Bundle?) { + Text(CONTENT) + } + + fun buildInject() = SettingsEntryBuilder.createInject(this.createSettingsPage()) + .setMacro { SimplePreferenceMacro(title = name, clickRoute = name) } +} + object SppForSearch : SettingsPageProvider { override val name = "SppForSearch" @@ -223,6 +241,7 @@ class SpaEnvironmentForTest( navArgument("rt_param") { type = NavType.StringType }, ) }, + SppDialog, ), rootPages ) diff --git a/packages/SettingsLib/res/values-sw/strings.xml b/packages/SettingsLib/res/values-sw/strings.xml index 1cdb69c6634b..a94c313ee817 100644 --- a/packages/SettingsLib/res/values-sw/strings.xml +++ b/packages/SettingsLib/res/values-sw/strings.xml @@ -140,8 +140,8 @@ <string name="bluetooth_hid_profile_summary_use_for" msgid="4289460627406490952">"Tumia kwa kuingiza"</string> <string name="bluetooth_hearing_aid_profile_summary_use_for" msgid="3374057355721486932">"Tumia kwa visaidizi vya kusikia"</string> <string name="bluetooth_le_audio_profile_summary_use_for" msgid="2778318636027348572">"Tumia kwa ajili ya LE_AUDIO"</string> - <string name="bluetooth_pairing_accept" msgid="2054232610815498004">"Oanisha"</string> - <string name="bluetooth_pairing_accept_all_caps" msgid="2734383073450506220">"OANISHA"</string> + <string name="bluetooth_pairing_accept" msgid="2054232610815498004">"Unganisha"</string> + <string name="bluetooth_pairing_accept_all_caps" msgid="2734383073450506220">"UNGANISHA"</string> <string name="bluetooth_pairing_decline" msgid="6483118841204885890">"Ghairi"</string> <string name="bluetooth_pairing_will_share_phonebook" msgid="3064334458659165176">"Kuoanisha hutoa ruhusa ya kufikiwa kwa unaowasiliana nao na rekodi ya simu zilizopigwa unapounganishwa."</string> <string name="bluetooth_pairing_error_message" msgid="6626399020672335565">"Haikuwezakulinganisha na <xliff:g id="DEVICE_NAME">%1$s</xliff:g>."</string> @@ -237,10 +237,10 @@ <string name="adb_wireless_error" msgid="721958772149779856">"Hitilafu"</string> <string name="adb_wireless_settings" msgid="2295017847215680229">"Utatuzi usiotumia waya"</string> <string name="adb_wireless_list_empty_off" msgid="1713707973837255490">"Ili kuangalia na kutumia vifaa vinavyopatikana, washa utatuzi usiotumia waya"</string> - <string name="adb_pair_method_qrcode_title" msgid="6982904096137468634">"Oanisha kifaa ukitumia msimbo wa QR"</string> - <string name="adb_pair_method_qrcode_summary" msgid="7130694277228970888">"Oanisha vifaa vipya ukitumia kichanganuzi cha Msimbo wa QR"</string> - <string name="adb_pair_method_code_title" msgid="1122590300445142904">"Oanisha kifaa ukitumia msimbo wa kuoanisha"</string> - <string name="adb_pair_method_code_summary" msgid="6370414511333685185">"Oanisha vifaa vipya ukitumia msimbo wa tarakimu sita"</string> + <string name="adb_pair_method_qrcode_title" msgid="6982904096137468634">"Unganisha kifaa ukitumia msimbo wa QR"</string> + <string name="adb_pair_method_qrcode_summary" msgid="7130694277228970888">"Unganisha vifaa vipya ukitumia kichanganuzi cha Msimbo wa QR"</string> + <string name="adb_pair_method_code_title" msgid="1122590300445142904">"Unganisha kifaa ukitumia msimbo wa kuunganisha"</string> + <string name="adb_pair_method_code_summary" msgid="6370414511333685185">"Unganisha vifaa vipya ukitumia msimbo wa tarakimu sita"</string> <string name="adb_paired_devices_title" msgid="5268997341526217362">"Vifaa vilivyooanishwa"</string> <string name="adb_wireless_device_connected_summary" msgid="3039660790249148713">"Vilivyounganishwa kwa sasa"</string> <string name="adb_wireless_device_details_title" msgid="7129369670526565786">"Maelezo ya kifaa"</string> @@ -248,16 +248,16 @@ <string name="adb_device_fingerprint_title_format" msgid="291504822917843701">"Alama bainifu ya kifaa: <xliff:g id="FINGERPRINT_PARAM">%1$s</xliff:g>"</string> <string name="adb_wireless_connection_failed_title" msgid="664211177427438438">"Imeshindwa kuunganisha"</string> <string name="adb_wireless_connection_failed_message" msgid="9213896700171602073">"Hakikisha kuwa <xliff:g id="DEVICE_NAME">%1$s</xliff:g> kimeunganishwa kwenye mtandao sahihi"</string> - <string name="adb_pairing_device_dialog_title" msgid="7141739231018530210">"Oanisha na kifaa"</string> + <string name="adb_pairing_device_dialog_title" msgid="7141739231018530210">"Unganisha na kifaa"</string> <string name="adb_pairing_device_dialog_pairing_code_label" msgid="3639239786669722731">"Msimbo wa kuoanisha wa Wi-Fi"</string> <string name="adb_pairing_device_dialog_failed_title" msgid="3426758947882091735">"Imeshindwa kuoanisha"</string> <string name="adb_pairing_device_dialog_failed_msg" msgid="6611097519661997148">"Hakikisha kuwa kifaa kimeunganishwa kwenye mtandao mmoja."</string> - <string name="adb_wireless_qrcode_summary" msgid="8051414549011801917">"Oanisha kifaa kupitia Wi-Fi kwa kuchanganua msimbo wa QR"</string> + <string name="adb_wireless_qrcode_summary" msgid="8051414549011801917">"Unganisha kifaa kupitia Wi-Fi kwa kuchanganua msimbo wa QR"</string> <string name="adb_wireless_verifying_qrcode_text" msgid="6123192424916029207">"Inaoanisha kifaa…"</string> <string name="adb_qrcode_pairing_device_failed_msg" msgid="6936292092592914132">"Imeshindwa kuoanisha kifaa. Huenda msimbo wa QR haukuwa sahihi au kifaa hakijaunganishwa kwenye mtandao mmoja."</string> <string name="adb_wireless_ip_addr_preference_title" msgid="8335132107715311730">"Anwani ya IP na Mlango"</string> <string name="adb_wireless_qrcode_pairing_title" msgid="1906409667944674707">"Changanua msimbo wa QR"</string> - <string name="adb_wireless_qrcode_pairing_description" msgid="6014121407143607851">"Oanisha kifaa kupitia Wi-Fi kwa kuchanganua msimbo wa QR"</string> + <string name="adb_wireless_qrcode_pairing_description" msgid="6014121407143607851">"Unganisha kifaa kupitia Wi-Fi kwa kuchanganua msimbo wa QR"</string> <string name="adb_wireless_no_network_msg" msgid="2365795244718494658">"Tafadhali unganisha kwenye mtandao wa Wi-Fi"</string> <string name="keywords_adb_wireless" msgid="6507505581882171240">"adb, tatua, dev"</string> <string name="bugreport_in_power" msgid="8664089072534638709">"Njia ya mkato ya kuripoti hitilafu"</string> diff --git a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java index 9560b8dc802e..cdb87404b016 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java @@ -19,6 +19,7 @@ import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECT import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHearingAid; import android.content.Context; import android.graphics.drawable.Drawable; import android.media.AudioManager; @@ -42,18 +43,16 @@ public class BluetoothMediaDevice extends MediaDevice { BluetoothMediaDevice( Context context, CachedBluetoothDevice device, - MediaRoute2Info info, - String packageName) { - this(context, device, info, packageName, null); + MediaRoute2Info info) { + this(context, device, info, null); } BluetoothMediaDevice( Context context, CachedBluetoothDevice device, MediaRoute2Info info, - String packageName, RouteListingPreference.Item item) { - super(context, info, packageName, item); + super(context, info, item); mCachedDevice = device; mAudioManager = context.getSystemService(AudioManager.class); initDeviceRecord(); @@ -100,7 +99,12 @@ public class BluetoothMediaDevice extends MediaDevice { @Override public String getId() { - return MediaDeviceUtils.getId(mCachedDevice); + if (mCachedDevice.isHearingAidDevice()) { + if (mCachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID) { + return Long.toString(mCachedDevice.getHiSyncId()); + } + } + return mCachedDevice.getAddress(); } /** diff --git a/packages/SettingsLib/src/com/android/settingslib/media/ComplexMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/ComplexMediaDevice.java index 4e0ebd1bdc1a..338fb872650c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/ComplexMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/ComplexMediaDevice.java @@ -34,9 +34,8 @@ public class ComplexMediaDevice extends MediaDevice { ComplexMediaDevice( Context context, MediaRoute2Info info, - String packageName, RouteListingPreference.Item item) { - super(context, info, packageName, item); + super(context, info, item); } // MediaRoute2Info.getName was made public on API 34, but exists since API 30. @@ -63,7 +62,7 @@ public class ComplexMediaDevice extends MediaDevice { @Override public String getId() { - return MediaDeviceUtils.getId(mRouteInfo); + return mRouteInfo.getId(); } public boolean isConnected() { diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java index 012cbc05b5b2..1347dd131f69 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java @@ -45,14 +45,13 @@ public class InfoMediaDevice extends MediaDevice { InfoMediaDevice( Context context, MediaRoute2Info info, - String packageName, RouteListingPreference.Item item) { - super(context, info, packageName, item); + super(context, info, item); initDeviceRecord(); } - InfoMediaDevice(Context context, MediaRoute2Info info, String packageName) { - this(context, info, packageName, null); + InfoMediaDevice(Context context, MediaRoute2Info info) { + this(context, info, null); } @Override @@ -118,7 +117,7 @@ public class InfoMediaDevice extends MediaDevice { @Override public String getId() { - return MediaDeviceUtils.getId(mRouteInfo); + return mRouteInfo.getId(); } public boolean isConnected() { diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java index e5fce5bfe02a..581c7dea002c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java @@ -383,7 +383,7 @@ public abstract class InfoMediaManager extends MediaManager { for (MediaRoute2Info route : getSelectableRoutes(info)) { deviceList.add( new InfoMediaDevice( - mContext, route, mPackageName, mPreferenceItemMap.get(route.getId()))); + mContext, route, mPreferenceItemMap.get(route.getId()))); } return deviceList; } @@ -410,7 +410,7 @@ public abstract class InfoMediaManager extends MediaManager { for (MediaRoute2Info route : getDeselectableRoutes(info)) { deviceList.add( new InfoMediaDevice( - mContext, route, mPackageName, mPreferenceItemMap.get(route.getId()))); + mContext, route, mPreferenceItemMap.get(route.getId()))); Log.d(TAG, route.getName() + " is deselectable for " + mPackageName); } return deviceList; @@ -434,7 +434,7 @@ public abstract class InfoMediaManager extends MediaManager { for (MediaRoute2Info route : getSelectedRoutes(info)) { deviceList.add( new InfoMediaDevice( - mContext, route, mPackageName, mPreferenceItemMap.get(route.getId()))); + mContext, route, mPreferenceItemMap.get(route.getId()))); } return deviceList; } @@ -633,7 +633,6 @@ public abstract class InfoMediaManager extends MediaManager { new InfoMediaDevice( mContext, route, - mPackageName, mPreferenceItemMap.get(route.getId())); break; case TYPE_BUILTIN_SPEAKER: @@ -650,7 +649,6 @@ public abstract class InfoMediaManager extends MediaManager { new PhoneMediaDevice( mContext, route, - mPackageName, mPreferenceItemMap.getOrDefault(route.getId(), null)); break; case TYPE_HEARING_AID: @@ -666,7 +664,6 @@ public abstract class InfoMediaManager extends MediaManager { mContext, cachedDevice, route, - mPackageName, mPreferenceItemMap.getOrDefault(route.getId(), null)); } break; @@ -675,7 +672,6 @@ public abstract class InfoMediaManager extends MediaManager { new ComplexMediaDevice( mContext, route, - mPackageName, mPreferenceItemMap.get(route.getId())); break; default: diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java index dbc3bf7b793e..ebcca42bb588 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java @@ -567,7 +567,7 @@ public class LocalMediaManager implements BluetoothCallback { final CachedBluetoothDevice cachedDevice = cachedDeviceManager.findDevice(device); if (isBondedMediaDevice(cachedDevice) && isMutingExpectedDevice(cachedDevice)) { - return new BluetoothMediaDevice(mContext, cachedDevice, null, mPackageName); + return new BluetoothMediaDevice(mContext, cachedDevice, null); } } return null; @@ -614,7 +614,7 @@ public class LocalMediaManager implements BluetoothCallback { mDisconnectedMediaDevices.clear(); for (CachedBluetoothDevice cachedDevice : cachedBluetoothDeviceList) { final MediaDevice mediaDevice = - new BluetoothMediaDevice(mContext, cachedDevice, null, mPackageName); + new BluetoothMediaDevice(mContext, cachedDevice, null); if (!mMediaDevices.contains(mediaDevice)) { cachedDevice.registerCallback(mDeviceAttributeChangeCallback); mDisconnectedMediaDevices.add(mediaDevice); diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java index c8e4c0c93789..f2d9d1493c74 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java @@ -121,16 +121,13 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { protected final Context mContext; protected final MediaRoute2Info mRouteInfo; protected final RouteListingPreference.Item mItem; - protected final String mPackageName; MediaDevice( Context context, MediaRoute2Info info, - String packageName, RouteListingPreference.Item item) { mContext = context; mRouteInfo = info; - mPackageName = packageName; mItem = item; setType(info); } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDeviceUtils.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDeviceUtils.java deleted file mode 100644 index b3a52b9ec788..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDeviceUtils.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.settingslib.media; - -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothHearingAid; -import android.media.MediaRoute2Info; - -import com.android.settingslib.bluetooth.CachedBluetoothDevice; - -/** - * MediaDeviceUtils provides utility function for MediaDevice - */ -public class MediaDeviceUtils { - /** - * Use CachedBluetoothDevice address to represent unique id - * - * @param cachedDevice the CachedBluetoothDevice - * @return CachedBluetoothDevice address - */ - public static String getId(CachedBluetoothDevice cachedDevice) { - if (cachedDevice.isHearingAidDevice()) { - if (cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID) { - return Long.toString(cachedDevice.getHiSyncId()); - } - } - return cachedDevice.getAddress(); - } - - /** - * Use BluetoothDevice address to represent unique id - * - * @param bluetoothDevice the BluetoothDevice - * @return BluetoothDevice address - */ - public static String getId(BluetoothDevice bluetoothDevice) { - return bluetoothDevice.getAddress(); - } - - /** - * Use MediaRoute2Info id to represent unique id - * - * @param route the MediaRoute2Info - * @return MediaRoute2Info id - */ - public static String getId(MediaRoute2Info route) { - return route.getId(); - } -} diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java index 0676ce5a65f1..d6f1eab442fa 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java @@ -117,16 +117,15 @@ public class PhoneMediaDevice extends MediaDevice { return name.toString(); } - PhoneMediaDevice(Context context, MediaRoute2Info info, String packageName) { - this(context, info, packageName, null); + PhoneMediaDevice(Context context, MediaRoute2Info info) { + this(context, info, null); } PhoneMediaDevice( Context context, MediaRoute2Info info, - String packageName, RouteListingPreference.Item item) { - super(context, info, packageName, item); + super(context, info, item); mDeviceIconUtil = new DeviceIconUtil(mContext); initDeviceRecord(); } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/BluetoothMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/BluetoothMediaDeviceTest.java index f50802a1702e..7061742a56d2 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/BluetoothMediaDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/BluetoothMediaDeviceTest.java @@ -39,6 +39,8 @@ import org.robolectric.RuntimeEnvironment; @RunWith(RobolectricTestRunner.class) public class BluetoothMediaDeviceTest { + private static final String TEST_ADDRESS = "11:22:33:44:55:66"; + @Mock private CachedBluetoothDevice mDevice; @@ -54,7 +56,7 @@ public class BluetoothMediaDeviceTest { when(mDevice.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(true); when(mDevice.isActiveDevice(BluetoothProfile.LE_AUDIO)).thenReturn(true); - mBluetoothMediaDevice = new BluetoothMediaDevice(mContext, mDevice, null, null, null); + mBluetoothMediaDevice = new BluetoothMediaDevice(mContext, mDevice, null, null); } @Test @@ -111,4 +113,10 @@ public class BluetoothMediaDeviceTest { assertThat(mBluetoothMediaDevice.getIcon() instanceof BitmapDrawable).isFalse(); } + + @Test + public void getId_returnsCachedBluetoothDeviceAddress() { + when(mDevice.getAddress()).thenReturn(TEST_ADDRESS); + assertThat(mBluetoothMediaDevice.getId()).isEqualTo(TEST_ADDRESS); + } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaDeviceTest.java index a072c1722c10..0665308fdbfb 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaDeviceTest.java @@ -65,7 +65,7 @@ public class InfoMediaDeviceTest { MockitoAnnotations.initMocks(this); mContext = RuntimeEnvironment.application; - mInfoMediaDevice = new InfoMediaDevice(mContext, mRouteInfo, TEST_PACKAGE_NAME); + mInfoMediaDevice = new InfoMediaDevice(mContext, mRouteInfo); } @Test diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java index 2252b69d61d6..f0330c46315c 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java @@ -547,7 +547,7 @@ public class InfoMediaManagerTest { @Test public void connectDeviceWithoutPackageName_noSession_returnFalse() { final MediaRoute2Info info = mock(MediaRoute2Info.class); - final MediaDevice device = new InfoMediaDevice(mContext, info, TEST_PACKAGE_NAME); + final MediaDevice device = new InfoMediaDevice(mContext, info); final List<RoutingSessionInfo> infos = new ArrayList<>(); @@ -623,7 +623,7 @@ public class InfoMediaManagerTest { routingSessionInfos.add(info); final MediaRoute2Info route2Info = mock(MediaRoute2Info.class); - final MediaDevice device = new InfoMediaDevice(mContext, route2Info, TEST_PACKAGE_NAME); + final MediaDevice device = new InfoMediaDevice(mContext, route2Info); final List<String> list = new ArrayList<>(); list.add(TEST_ID); @@ -644,7 +644,7 @@ public class InfoMediaManagerTest { routingSessionInfos.add(info); final MediaRoute2Info route2Info = mock(MediaRoute2Info.class); - final MediaDevice device = new InfoMediaDevice(mContext, route2Info, TEST_PACKAGE_NAME); + final MediaDevice device = new InfoMediaDevice(mContext, route2Info); final List<String> list = new ArrayList<>(); list.add("fake_id"); @@ -674,7 +674,7 @@ public class InfoMediaManagerTest { routingSessionInfos.add(info); final MediaRoute2Info route2Info = mock(MediaRoute2Info.class); - final MediaDevice device = new InfoMediaDevice(mContext, route2Info, TEST_PACKAGE_NAME); + final MediaDevice device = new InfoMediaDevice(mContext, route2Info); final List<String> list = new ArrayList<>(); list.add(TEST_ID); @@ -695,7 +695,7 @@ public class InfoMediaManagerTest { routingSessionInfos.add(info); final MediaRoute2Info route2Info = mock(MediaRoute2Info.class); - final MediaDevice device = new InfoMediaDevice(mContext, route2Info, TEST_PACKAGE_NAME); + final MediaDevice device = new InfoMediaDevice(mContext, route2Info); final List<String> list = new ArrayList<>(); list.add("fake_id"); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java index 926b41a0017f..999e8d508e19 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java @@ -116,8 +116,8 @@ public class LocalMediaManagerTest { when(mLocalProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile); when(mLocalProfileManager.getHearingAidProfile()).thenReturn(mHapProfile); - mInfoMediaDevice1 = spy(new InfoMediaDevice(mContext, mRouteInfo1, TEST_PACKAGE_NAME)); - mInfoMediaDevice2 = new InfoMediaDevice(mContext, mRouteInfo2, TEST_PACKAGE_NAME); + mInfoMediaDevice1 = spy(new InfoMediaDevice(mContext, mRouteInfo1)); + mInfoMediaDevice2 = new InfoMediaDevice(mContext, mRouteInfo2); mLocalMediaManager = new LocalMediaManager(mContext, mLocalBluetoothManager, mInfoMediaManager, "com.test.packagename"); mLocalMediaManager.mAudioManager = mAudioManager; diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java index 18055d97d835..098ab162c225 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java @@ -171,17 +171,17 @@ public class MediaDeviceTest { mBluetoothMediaDevice1 = new BluetoothMediaDevice( - mContext, mCachedDevice1, mBluetoothRouteInfo1, TEST_PACKAGE_NAME); + mContext, mCachedDevice1, mBluetoothRouteInfo1); mBluetoothMediaDevice2 = new BluetoothMediaDevice( - mContext, mCachedDevice2, mBluetoothRouteInfo2, TEST_PACKAGE_NAME); + mContext, mCachedDevice2, mBluetoothRouteInfo2); mBluetoothMediaDevice3 = new BluetoothMediaDevice( - mContext, mCachedDevice3, mBluetoothRouteInfo3, TEST_PACKAGE_NAME); - mInfoMediaDevice1 = new InfoMediaDevice(mContext, mRouteInfo1, TEST_PACKAGE_NAME); - mInfoMediaDevice2 = new InfoMediaDevice(mContext, mRouteInfo2, TEST_PACKAGE_NAME); - mInfoMediaDevice3 = new InfoMediaDevice(mContext, mRouteInfo3, TEST_PACKAGE_NAME); - mPhoneMediaDevice = new PhoneMediaDevice(mContext, mPhoneRouteInfo, TEST_PACKAGE_NAME); + mContext, mCachedDevice3, mBluetoothRouteInfo3); + mInfoMediaDevice1 = new InfoMediaDevice(mContext, mRouteInfo1); + mInfoMediaDevice2 = new InfoMediaDevice(mContext, mRouteInfo2); + mInfoMediaDevice3 = new InfoMediaDevice(mContext, mRouteInfo3); + mPhoneMediaDevice = new PhoneMediaDevice(mContext, mPhoneRouteInfo); } @Test @@ -316,7 +316,7 @@ public class MediaDeviceTest { when(phoneRouteInfo.getType()).thenReturn(TYPE_WIRED_HEADPHONES); final PhoneMediaDevice phoneMediaDevice = - new PhoneMediaDevice(mContext, phoneRouteInfo, TEST_PACKAGE_NAME); + new PhoneMediaDevice(mContext, phoneRouteInfo); mMediaDevices.add(mBluetoothMediaDevice1); mMediaDevices.add(phoneMediaDevice); @@ -332,7 +332,7 @@ public class MediaDeviceTest { when(phoneRouteInfo.getType()).thenReturn(TYPE_WIRED_HEADPHONES); final PhoneMediaDevice phoneMediaDevice = - new PhoneMediaDevice(mContext, phoneRouteInfo, TEST_PACKAGE_NAME); + new PhoneMediaDevice(mContext, phoneRouteInfo); mMediaDevices.add(mInfoMediaDevice1); mMediaDevices.add(phoneMediaDevice); @@ -483,7 +483,7 @@ public class MediaDeviceTest { public void getFeatures_noRouteInfo_returnEmptyList() { mBluetoothMediaDevice1 = new BluetoothMediaDevice( - mContext, mCachedDevice1, null /* MediaRoute2Info */, TEST_PACKAGE_NAME); + mContext, mCachedDevice1, /* MediaRoute2Info */ null); assertThat(mBluetoothMediaDevice1.getFeatures().size()).isEqualTo(0); } @@ -498,10 +498,9 @@ public class MediaDeviceTest { mContext, mCachedDevice1, null /* MediaRoute2Info */, - TEST_PACKAGE_NAME, mItem); mPhoneMediaDevice = - new PhoneMediaDevice(mContext, mPhoneRouteInfo, TEST_PACKAGE_NAME, mItem); + new PhoneMediaDevice(mContext, mPhoneRouteInfo, mItem); assertThat(mBluetoothMediaDevice1.getSelectionBehavior()).isEqualTo( SELECTION_BEHAVIOR_TRANSFER); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceUtilsTest.java deleted file mode 100644 index 30a6ad2576db..000000000000 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceUtilsTest.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2019 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.media; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.Mockito.when; - -import android.bluetooth.BluetoothDevice; -import android.media.MediaRoute2Info; - -import com.android.settingslib.bluetooth.CachedBluetoothDevice; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class MediaDeviceUtilsTest { - - private static final String TEST_ADDRESS = "11:22:33:44:55:66"; - private static final String TEST_ROUTE_ID = "test_route_id"; - - @Mock - private CachedBluetoothDevice mCachedDevice; - @Mock - private BluetoothDevice mBluetoothDevice; - @Mock - private MediaRoute2Info mRouteInfo; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void getId_returnCachedBluetoothDeviceAddress() { - when(mCachedDevice.getAddress()).thenReturn(TEST_ADDRESS); - - final String id = MediaDeviceUtils.getId(mCachedDevice); - - assertThat(id).isEqualTo(TEST_ADDRESS); - } - - @Test - public void getId_returnBluetoothDeviceAddress() { - when(mBluetoothDevice.getAddress()).thenReturn(TEST_ADDRESS); - - final String id = MediaDeviceUtils.getId(mBluetoothDevice); - - assertThat(id).isEqualTo(TEST_ADDRESS); - } - - @Test - public void getId_returnRouteInfoId() { - when(mRouteInfo.getId()).thenReturn(TEST_ROUTE_ID); - - final String id = MediaDeviceUtils.getId(mRouteInfo); - - assertThat(id).isEqualTo(TEST_ROUTE_ID); - } -} diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java index d6e8d26af1a0..2e39adc8f79e 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java @@ -69,6 +69,7 @@ public class GlobalSettings { Settings.Global.PRIVATE_DNS_SPECIFIER, Settings.Global.SOFT_AP_TIMEOUT_ENABLED, Settings.Global.ZEN_DURATION, + Settings.Global.MUTE_ALARM_STREAM_WITH_RINGER_MODE_USER_PREFERENCE, Settings.Global.REVERSE_CHARGING_AUTO_ON, Settings.Global.CHARGING_VIBRATION_ENABLED, Settings.Global.AWARE_ALLOWED, diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java index 59c3cd38a97d..e7d7bb01e180 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java @@ -19,93 +19,105 @@ package android.provider.settings.backup; import android.compat.annotation.UnsupportedAppUsage; import android.provider.Settings; +import com.android.server.display.feature.flags.Flags; + +import java.util.ArrayList; +import java.util.List; + /** Information about the system settings to back up */ public class SystemSettings { /** - * Settings to backup. + * Settings to back up. * * NOTE: Settings are backed up and restored in the order they appear * in this array. If you have one setting depending on another, * make sure that they are ordered appropriately. */ @UnsupportedAppUsage - public static final String[] SETTINGS_TO_BACKUP = { - Settings.System.STAY_ON_WHILE_PLUGGED_IN, // moved to global - Settings.System.WIFI_USE_STATIC_IP, - Settings.System.WIFI_STATIC_IP, - Settings.System.WIFI_STATIC_GATEWAY, - Settings.System.WIFI_STATIC_NETMASK, - Settings.System.WIFI_STATIC_DNS1, - Settings.System.WIFI_STATIC_DNS2, - Settings.System.BLUETOOTH_DISCOVERABILITY, - Settings.System.BLUETOOTH_DISCOVERABILITY_TIMEOUT, - Settings.System.FONT_SCALE, - Settings.System.DIM_SCREEN, - Settings.System.SCREEN_OFF_TIMEOUT, - Settings.System.SCREEN_BRIGHTNESS_MODE, - Settings.System.ADAPTIVE_SLEEP, // moved to secure - Settings.System.APPLY_RAMPING_RINGER, - Settings.System.VIBRATE_INPUT_DEVICES, - Settings.System.MODE_RINGER_STREAMS_AFFECTED, - Settings.System.TEXT_AUTO_REPLACE, - Settings.System.TEXT_AUTO_CAPS, - Settings.System.TEXT_AUTO_PUNCTUATE, - Settings.System.TEXT_SHOW_PASSWORD, - Settings.System.AUTO_TIME, // moved to global - Settings.System.AUTO_TIME_ZONE, // moved to global - Settings.System.TIME_12_24, - Settings.System.DTMF_TONE_WHEN_DIALING, - Settings.System.DTMF_TONE_TYPE_WHEN_DIALING, - Settings.System.HEARING_AID, - Settings.System.TTY_MODE, - Settings.System.MASTER_MONO, - Settings.System.MASTER_BALANCE, - Settings.System.FOLD_LOCK_BEHAVIOR, - Settings.System.SOUND_EFFECTS_ENABLED, - Settings.System.HAPTIC_FEEDBACK_ENABLED, - Settings.System.POWER_SOUNDS_ENABLED, // moved to global - Settings.System.DOCK_SOUNDS_ENABLED, // moved to global - Settings.System.LOCKSCREEN_SOUNDS_ENABLED, - Settings.System.SHOW_WEB_SUGGESTIONS, - Settings.System.SIP_CALL_OPTIONS, - Settings.System.SIP_RECEIVE_CALLS, - Settings.System.POINTER_SPEED, - Settings.System.VIBRATE_ON, - Settings.System.VIBRATE_WHEN_RINGING, - Settings.System.RINGTONE, - Settings.System.LOCK_TO_APP_ENABLED, - Settings.System.NOTIFICATION_SOUND, - Settings.System.ACCELEROMETER_ROTATION, - Settings.System.SHOW_BATTERY_PERCENT, - Settings.System.ALARM_VIBRATION_INTENSITY, - Settings.System.MEDIA_VIBRATION_INTENSITY, - Settings.System.NOTIFICATION_VIBRATION_INTENSITY, - Settings.System.RING_VIBRATION_INTENSITY, - Settings.System.HAPTIC_FEEDBACK_INTENSITY, - Settings.System.HARDWARE_HAPTIC_FEEDBACK_INTENSITY, - Settings.System.KEYBOARD_VIBRATION_ENABLED, - Settings.System.HAPTIC_FEEDBACK_ENABLED, - Settings.System.DISPLAY_COLOR_MODE_VENDOR_HINT, // must precede DISPLAY_COLOR_MODE - Settings.System.DISPLAY_COLOR_MODE, - Settings.System.ALARM_ALERT, - Settings.System.NOTIFICATION_LIGHT_PULSE, - Settings.System.WEAR_ACCESSIBILITY_GESTURE_ENABLED, - Settings.System.CLOCKWORK_BLUETOOTH_SETTINGS_PREF, - Settings.System.UNREAD_NOTIFICATION_DOT_INDICATOR, - Settings.System.AUTO_LAUNCH_MEDIA_CONTROLS, - Settings.System.LOCALE_PREFERENCES, - Settings.System.TOUCHPAD_POINTER_SPEED, - Settings.System.TOUCHPAD_NATURAL_SCROLLING, - Settings.System.TOUCHPAD_TAP_TO_CLICK, - Settings.System.TOUCHPAD_RIGHT_CLICK_ZONE, - Settings.System.CAMERA_FLASH_NOTIFICATION, - Settings.System.SCREEN_FLASH_NOTIFICATION, - Settings.System.SCREEN_FLASH_NOTIFICATION_COLOR, - Settings.System.PEAK_REFRESH_RATE, - Settings.System.MIN_REFRESH_RATE, - Settings.System.NOTIFICATION_COOLDOWN_ENABLED, - Settings.System.NOTIFICATION_COOLDOWN_ALL, - Settings.System.NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED, - }; + public static final String[] SETTINGS_TO_BACKUP = getSettingsToBackUp(); + + private static String[] getSettingsToBackUp() { + List<String> settings = new ArrayList<>(List.of( + Settings.System.STAY_ON_WHILE_PLUGGED_IN, // moved to global + Settings.System.WIFI_USE_STATIC_IP, + Settings.System.WIFI_STATIC_IP, + Settings.System.WIFI_STATIC_GATEWAY, + Settings.System.WIFI_STATIC_NETMASK, + Settings.System.WIFI_STATIC_DNS1, + Settings.System.WIFI_STATIC_DNS2, + Settings.System.BLUETOOTH_DISCOVERABILITY, + Settings.System.BLUETOOTH_DISCOVERABILITY_TIMEOUT, + Settings.System.FONT_SCALE, + Settings.System.DIM_SCREEN, + Settings.System.SCREEN_OFF_TIMEOUT, + Settings.System.SCREEN_BRIGHTNESS_MODE, + Settings.System.ADAPTIVE_SLEEP, // moved to secure + Settings.System.APPLY_RAMPING_RINGER, + Settings.System.VIBRATE_INPUT_DEVICES, + Settings.System.MODE_RINGER_STREAMS_AFFECTED, + Settings.System.TEXT_AUTO_REPLACE, + Settings.System.TEXT_AUTO_CAPS, + Settings.System.TEXT_AUTO_PUNCTUATE, + Settings.System.TEXT_SHOW_PASSWORD, + Settings.System.AUTO_TIME, // moved to global + Settings.System.AUTO_TIME_ZONE, // moved to global + Settings.System.TIME_12_24, + Settings.System.DTMF_TONE_WHEN_DIALING, + Settings.System.DTMF_TONE_TYPE_WHEN_DIALING, + Settings.System.HEARING_AID, + Settings.System.TTY_MODE, + Settings.System.MASTER_MONO, + Settings.System.MASTER_BALANCE, + Settings.System.FOLD_LOCK_BEHAVIOR, + Settings.System.SOUND_EFFECTS_ENABLED, + Settings.System.HAPTIC_FEEDBACK_ENABLED, + Settings.System.POWER_SOUNDS_ENABLED, // moved to global + Settings.System.DOCK_SOUNDS_ENABLED, // moved to global + Settings.System.LOCKSCREEN_SOUNDS_ENABLED, + Settings.System.SHOW_WEB_SUGGESTIONS, + Settings.System.SIP_CALL_OPTIONS, + Settings.System.SIP_RECEIVE_CALLS, + Settings.System.POINTER_SPEED, + Settings.System.VIBRATE_ON, + Settings.System.VIBRATE_WHEN_RINGING, + Settings.System.RINGTONE, + Settings.System.LOCK_TO_APP_ENABLED, + Settings.System.NOTIFICATION_SOUND, + Settings.System.ACCELEROMETER_ROTATION, + Settings.System.SHOW_BATTERY_PERCENT, + Settings.System.ALARM_VIBRATION_INTENSITY, + Settings.System.MEDIA_VIBRATION_INTENSITY, + Settings.System.NOTIFICATION_VIBRATION_INTENSITY, + Settings.System.RING_VIBRATION_INTENSITY, + Settings.System.HAPTIC_FEEDBACK_INTENSITY, + Settings.System.HARDWARE_HAPTIC_FEEDBACK_INTENSITY, + Settings.System.KEYBOARD_VIBRATION_ENABLED, + Settings.System.HAPTIC_FEEDBACK_ENABLED, + Settings.System.DISPLAY_COLOR_MODE_VENDOR_HINT, // must precede DISPLAY_COLOR_MODE + Settings.System.DISPLAY_COLOR_MODE, + Settings.System.ALARM_ALERT, + Settings.System.NOTIFICATION_LIGHT_PULSE, + Settings.System.WEAR_ACCESSIBILITY_GESTURE_ENABLED, + Settings.System.CLOCKWORK_BLUETOOTH_SETTINGS_PREF, + Settings.System.UNREAD_NOTIFICATION_DOT_INDICATOR, + Settings.System.AUTO_LAUNCH_MEDIA_CONTROLS, + Settings.System.LOCALE_PREFERENCES, + Settings.System.TOUCHPAD_POINTER_SPEED, + Settings.System.TOUCHPAD_NATURAL_SCROLLING, + Settings.System.TOUCHPAD_TAP_TO_CLICK, + Settings.System.TOUCHPAD_RIGHT_CLICK_ZONE, + Settings.System.CAMERA_FLASH_NOTIFICATION, + Settings.System.SCREEN_FLASH_NOTIFICATION, + Settings.System.SCREEN_FLASH_NOTIFICATION_COLOR, + Settings.System.NOTIFICATION_COOLDOWN_ENABLED, + Settings.System.NOTIFICATION_COOLDOWN_ALL, + Settings.System.NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED + )); + if (Flags.backUpSmoothDisplayAndForcePeakRefreshRate()) { + settings.add(Settings.System.PEAK_REFRESH_RATE); + settings.add(Settings.System.MIN_REFRESH_RATE); + } + return settings.toArray(new String[0]); + } } diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java index f8bdcf65c45d..502239513002 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java @@ -210,6 +210,8 @@ public class GlobalSettingsValidators { VALIDATORS.put(Global.POWER_BUTTON_LONG_PRESS_DURATION_MS, NONE_NEGATIVE_LONG_VALIDATOR); VALIDATORS.put(Global.STYLUS_EVER_USED, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.MUTE_ALARM_STREAM_WITH_RINGER_MODE, BOOLEAN_VALIDATOR); + VALIDATORS.put( + Global.MUTE_ALARM_STREAM_WITH_RINGER_MODE_USER_PREFERENCE, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.Wearable.HAS_PAY_TOKENS, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.Wearable.GMS_CHECKIN_TIMEOUT_MIN, ANY_INTEGER_VALIDATOR); diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 6ad10cc8cc5b..1481d97a04fa 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -33,6 +33,8 @@ import android.provider.settings.backup.SystemSettings; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.server.display.feature.flags.Flags; + import org.junit.Test; import org.junit.runner.RunWith; @@ -53,59 +55,6 @@ public class SettingsBackupTest { public static final String HYBRID_SYSUI_BATTERY_WARNING_FLAGS = "hybrid_sysui_battery_warning_flags"; - /** - * The following denylists contain settings that should *not* be backed up and restored to - * another device. As a general rule, anything that is not user configurable should be - * denied (and conversely, things that *are* user configurable *should* be backed up) - */ - private static final Set<String> BACKUP_DENY_LIST_SYSTEM_SETTINGS = - newHashSet( - Settings.System.ADVANCED_SETTINGS, // candidate for backup? - Settings.System.ALARM_ALERT_CACHE, // internal cache - Settings.System.APPEND_FOR_LAST_AUDIBLE, // suffix deprecated since API 2 - Settings.System.EGG_MODE, // I am the lolrus - Settings.System.END_BUTTON_BEHAVIOR, // bug? - Settings.System - .HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY, // candidate for backup? - Settings.System.LOCKSCREEN_DISABLED, // ? - Settings.System.MEDIA_BUTTON_RECEIVER, // candidate for backup? - Settings.System.MUTE_STREAMS_AFFECTED, // candidate for backup? - Settings.System.NOTIFICATION_SOUND_CACHE, // internal cache - Settings.System.POINTER_LOCATION, // backup candidate? - Settings.System.DEBUG_ENABLE_ENHANCED_CALL_BLOCKING, // used for testing only - Settings.System.RINGTONE_CACHE, // internal cache - Settings.System.SCREEN_BRIGHTNESS, // removed in P - Settings.System.SETUP_WIZARD_HAS_RUN, // Only used by SuW - Settings.System.SHOW_GTALK_SERVICE_STATUS, // candidate for backup? - Settings.System.SHOW_TOUCHES, - Settings.System.SHOW_KEY_PRESSES, - Settings.System.SHOW_ROTARY_INPUT, - Settings.System.SIP_ADDRESS_ONLY, // value, not a setting - Settings.System.SIP_ALWAYS, // value, not a setting - Settings.System.SYSTEM_LOCALES, // bug? - Settings.System.USER_ROTATION, // backup candidate? - Settings.System.VIBRATE_IN_SILENT, // deprecated? - Settings.System.VOLUME_ACCESSIBILITY, // used internally, changing value will - // not change volume - Settings.System.VOLUME_ALARM, // deprecated since API 2? - Settings.System.VOLUME_ASSISTANT, // candidate for backup? - Settings.System.VOLUME_BLUETOOTH_SCO, // deprecated since API 2? - Settings.System.VOLUME_MASTER, // candidate for backup? - Settings.System.VOLUME_MUSIC, // deprecated since API 2? - Settings.System.VOLUME_NOTIFICATION, // deprecated since API 2? - Settings.System.VOLUME_RING, // deprecated since API 2? - Settings.System.VOLUME_SYSTEM, // deprecated since API 2? - Settings.System.VOLUME_VOICE, // deprecated since API 2? - Settings.System.WHEN_TO_MAKE_WIFI_CALLS, // bug? - Settings.System.WINDOW_ORIENTATION_LISTENER_LOG, // used for debugging only - Settings.System.SCREEN_BRIGHTNESS_FLOAT, - Settings.System.SCREEN_BRIGHTNESS_FOR_ALS, - Settings.System.WEAR_ACCESSIBILITY_GESTURE_ENABLED_DURING_OOBE, - Settings.System.WEAR_TTS_PREWARM_ENABLED, - Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, - Settings.System.MULTI_AUDIO_FOCUS_ENABLED // form-factor/OEM specific - ); - private static final Set<String> BACKUP_DENY_LIST_GLOBAL_SETTINGS = newHashSet( Settings.Global.ACTIVITY_MANAGER_CONSTANTS, @@ -737,6 +686,7 @@ public class SettingsBackupTest { Settings.Secure.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS, Settings.Secure.CONTENT_CAPTURE_ENABLED, Settings.Secure.DEFAULT_INPUT_METHOD, + Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, Settings.Secure.DEVICE_PAIRED, Settings.Secure.DIALER_DEFAULT_APPLICATION, Settings.Secure.DISABLED_PRINT_SERVICES, @@ -862,7 +812,7 @@ public class SettingsBackupTest { checkSettingsBackedUpOrDenied( getCandidateSettings(Settings.System.class), newHashSet(SystemSettings.SETTINGS_TO_BACKUP), - BACKUP_DENY_LIST_SYSTEM_SETTINGS); + getBackUpDenyListSystemSettings()); } @Test @@ -937,6 +887,69 @@ public class SettingsBackupTest { checkSettingsBackedUpOrDenied(allSettings, keys, BACKUP_DENY_LIST_SECURE_SETTINGS); } + /** + * The following denylists contain settings that should *not* be backed up and restored to + * another device. As a general rule, anything that is not user configurable should be + * denied (and conversely, things that *are* user configurable *should* be backed up) + */ + private static Set<String> getBackUpDenyListSystemSettings() { + Set<String> settings = + newHashSet( + Settings.System.ADVANCED_SETTINGS, // candidate for backup? + Settings.System.ALARM_ALERT_CACHE, // internal cache + Settings.System.APPEND_FOR_LAST_AUDIBLE, // suffix deprecated since API 2 + Settings.System.EGG_MODE, // I am the lolrus + Settings.System.END_BUTTON_BEHAVIOR, // bug? + Settings.System + .HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY, + // candidate for backup? + Settings.System.LOCKSCREEN_DISABLED, // ? + Settings.System.MEDIA_BUTTON_RECEIVER, // candidate for backup? + Settings.System.MUTE_STREAMS_AFFECTED, // candidate for backup? + Settings.System.NOTIFICATION_SOUND_CACHE, // internal cache + Settings.System.POINTER_LOCATION, // backup candidate? + Settings.System.DEBUG_ENABLE_ENHANCED_CALL_BLOCKING, + // used for testing only + Settings.System.RINGTONE_CACHE, // internal cache + Settings.System.SCREEN_BRIGHTNESS, // removed in P + Settings.System.SETUP_WIZARD_HAS_RUN, // Only used by SuW + Settings.System.SHOW_GTALK_SERVICE_STATUS, // candidate for backup? + Settings.System.SHOW_TOUCHES, + Settings.System.SHOW_KEY_PRESSES, + Settings.System.SHOW_ROTARY_INPUT, + Settings.System.SIP_ADDRESS_ONLY, // value, not a setting + Settings.System.SIP_ALWAYS, // value, not a setting + Settings.System.SYSTEM_LOCALES, // bug? + Settings.System.USER_ROTATION, // backup candidate? + Settings.System.VIBRATE_IN_SILENT, // deprecated? + Settings.System.VOLUME_ACCESSIBILITY, + // used internally, changing value will + // not change volume + Settings.System.VOLUME_ALARM, // deprecated since API 2? + Settings.System.VOLUME_ASSISTANT, // candidate for backup? + Settings.System.VOLUME_BLUETOOTH_SCO, // deprecated since API 2? + Settings.System.VOLUME_MASTER, // candidate for backup? + Settings.System.VOLUME_MUSIC, // deprecated since API 2? + Settings.System.VOLUME_NOTIFICATION, // deprecated since API 2? + Settings.System.VOLUME_RING, // deprecated since API 2? + Settings.System.VOLUME_SYSTEM, // deprecated since API 2? + Settings.System.VOLUME_VOICE, // deprecated since API 2? + Settings.System.WHEN_TO_MAKE_WIFI_CALLS, // bug? + Settings.System.WINDOW_ORIENTATION_LISTENER_LOG, // used for debugging only + Settings.System.SCREEN_BRIGHTNESS_FLOAT, + Settings.System.SCREEN_BRIGHTNESS_FOR_ALS, + Settings.System.WEAR_ACCESSIBILITY_GESTURE_ENABLED_DURING_OOBE, + Settings.System.WEAR_TTS_PREWARM_ENABLED, + Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, + Settings.System.MULTI_AUDIO_FOCUS_ENABLED // form-factor/OEM specific + ); + if (!Flags.backUpSmoothDisplayAndForcePeakRefreshRate()) { + settings.add(Settings.System.MIN_REFRESH_RATE); + settings.add(Settings.System.PEAK_REFRESH_RATE); + } + return settings; + } + private static void checkSettingsBackedUpOrDenied( Set<String> settings, Set<String> settingsToBackup, Set<String> denylist) { Set<String> settingsNotBackedUp = difference(settings, settingsToBackup); diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index cc5dfc66cb74..42107b7a3182 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -65,6 +65,7 @@ systemui_compose_java_defaults { "androidx.compose.runtime_runtime", "androidx.compose.material3_material3", + "androidx.compose.material_material-icons-extended", "androidx.activity_activity-compose", "androidx.compose.animation_animation-graphics", ], diff --git a/packages/SystemUI/aconfig/Android.bp b/packages/SystemUI/aconfig/Android.bp index 7f16ca52d949..03f9d74a4a34 100644 --- a/packages/SystemUI/aconfig/Android.bp +++ b/packages/SystemUI/aconfig/Android.bp @@ -25,6 +25,7 @@ package { "//frameworks/base/packages/SystemUI:__subpackages__", "//frameworks/libs/systemui/tracinglib:__subpackages__", "//platform_testing:__subpackages__", + "//cts:__subpackages__", ], } diff --git a/packages/SystemUI/aconfig/predictive_back.aconfig b/packages/SystemUI/aconfig/predictive_back.aconfig new file mode 100644 index 000000000000..1ad16667f317 --- /dev/null +++ b/packages/SystemUI/aconfig/predictive_back.aconfig @@ -0,0 +1,22 @@ +package: "com.android.systemui" + +flag { + name: "predictive_back_sysui" + namespace: "systemui" + description: "Predictive Back Dispatching for SysUI" + bug: "309545085" +} + +flag { + name: "predictive_back_animate_shade" + namespace: "systemui" + description: "Enable Shade Animations" + bug: "309545085" +} + +flag { + name: "predictive_back_animate_bouncer" + namespace: "systemui" + description: "Enable Predictive Back Animation in Bouncer" + bug: "309545085" +}
\ No newline at end of file diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 558a910b22c9..5b218549de43 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -145,6 +145,16 @@ flag { } flag { + name: "enable_background_keyguard_ondrawn_callback" + namespace: "systemui" + description: "Calls the onDrawn keyguard in the background, without being blocked by main" + "thread work. This results in the screen to turn on earlier when the main thread is stuck. " + "Note that, even after this callback is called, we're waiting for all windows to finish " + " drawing." + bug: "295873557" +} + +flag { name: "qs_new_pipeline" namespace: "systemui" description: "Use the new pipeline for Quick Settings. Should have no behavior changes." @@ -155,7 +165,7 @@ flag { name: "qs_new_tiles" namespace: "systemui" description: "Use the new tiles in the Quick Settings. Should have no behavior changes." - bug: "241772429" + bug: "311147395" } flag { @@ -298,6 +308,13 @@ flag { } flag { + name: "run_fingerprint_detect_on_dismissible_keyguard" + namespace: "systemui" + description: "Run fingerprint detect instead of authenticate if the keyguard is dismissible." + bug: "311145851" +} + +flag { name: "bluetooth_qs_tile_dialog_auto_on_toggle" namespace: "systemui" description: "Displays the auto on toggle in the bluetooth QS tile dialog" diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt index 23fcb691ddb4..867bbb7d74eb 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt @@ -65,12 +65,33 @@ object ShaderUtilLibrary { return dest; } - // Return range [-1, 1]. + // Integer mod. GLSL es 1.0 doesn't have integer mod :( + int imod(int a, int b) { + return a - (b * (a / b)); + } + + ivec3 imod(ivec3 a, int b) { + return ivec3(imod(a.x, b), imod(a.y, b), imod(a.z, b)); + } + + // Integer based hash function with the return range of [-1, 1]. vec3 hash(vec3 p) { - p = fract(p * vec3(.3456, .1234, .9876)); - p += dot(p, p.yxz + 43.21); - p = (p.xxy + p.yxx) * p.zyx; - return (fract(sin(p) * 4567.1234567) - .5) * 2.; + ivec3 v = ivec3(p); + v = v * 1671731 + 10139267; + + v.x += v.y * v.z; + v.y += v.z * v.x; + v.z += v.x * v.y; + + ivec3 v2 = v / 65536; // v >> 16 + v = imod((10 - imod((v + v2), 10)), 10); // v ^ v2 + + v.x += v.y * v.z; + v.y += v.z * v.x; + v.z += v.x * v.y; + + // Use sin and cos to map the range to [-1, 1]. + return vec3(sin(float(v.x)), cos(float(v.y)), sin(float(v.z))); } // Skew factors (non-uniform). diff --git a/packages/SystemUI/compose/core/src/com/android/compose/ui/platform/DensityAwareComposeView.kt b/packages/SystemUI/compose/core/src/com/android/compose/ui/platform/DensityAwareComposeView.kt new file mode 100644 index 000000000000..dff8753fd880 --- /dev/null +++ b/packages/SystemUI/compose/core/src/com/android/compose/ui/platform/DensityAwareComposeView.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.compose.ui.platform + +import android.content.Context +import android.content.res.Configuration +import android.util.AttributeSet +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.platform.AbstractComposeView + +/** + * A ComposeView that recreates its composition if the display size or font scale was changed. + * + * TODO(b/317317814): Remove this workaround. + */ +class DensityAwareComposeView(context: Context) : OpenComposeView(context) { + private var lastDensityDpi: Int = -1 + private var lastFontScale: Float = -1f + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + + val configuration = context.resources.configuration + lastDensityDpi = configuration.densityDpi + lastFontScale = configuration.fontScale + } + + override fun dispatchConfigurationChanged(newConfig: Configuration) { + super.dispatchConfigurationChanged(newConfig) + + // If the density or font scale changed, we dispose then recreate the composition. Note that + // we do this here after dispatching the new configuration to children (instead of doing + // this in onConfigurationChanged()) because the new configuration should first be + // dispatched to the AndroidComposeView that holds the current density before we recreate + // the composition. + val densityDpi = newConfig.densityDpi + val fontScale = newConfig.fontScale + if (densityDpi != lastDensityDpi || fontScale != lastFontScale) { + lastDensityDpi = densityDpi + lastFontScale = fontScale + + disposeComposition() + if (isAttachedToWindow) { + createComposition() + } + } + } +} + +/** A fork of [androidx.compose.ui.platform.ComposeView] that is open and can be subclassed. */ +open class OpenComposeView +internal constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : + AbstractComposeView(context, attrs, defStyleAttr) { + + private val content = mutableStateOf<(@Composable () -> Unit)?>(null) + + @Suppress("RedundantVisibilityModifier") + protected override var shouldCreateCompositionOnAttachedToWindow: Boolean = false + + @Composable + override fun Content() { + content.value?.invoke() + } + + override fun getAccessibilityClassName(): CharSequence { + return javaClass.name + } + + /** + * Set the Jetpack Compose UI content for this view. Initial composition will occur when the + * view becomes attached to a window or when [createComposition] is called, whichever comes + * first. + */ + fun setContent(content: @Composable () -> Unit) { + shouldCreateCompositionOnAttachedToWindow = true + this.content.value = content + if (isAttachedToWindow) { + createComposition() + } + } +} 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 5055ee1d73f6..d31547bd0d64 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 @@ -27,6 +27,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.LifecycleOwner import com.android.compose.theme.PlatformTheme +import com.android.compose.ui.platform.DensityAwareComposeView import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation import com.android.systemui.common.ui.compose.windowinsets.DisplayCutout import com.android.systemui.common.ui.compose.windowinsets.DisplayCutoutProvider @@ -84,7 +85,7 @@ object ComposeFacade : BaseComposeFacade { viewModel: FooterActionsViewModel, qsVisibilityLifecycleOwner: LifecycleOwner, ): View { - return ComposeView(context).apply { + return DensityAwareComposeView(context).apply { setContent { PlatformTheme { FooterActions(viewModel, qsVisibilityLifecycleOwner) } } } } diff --git a/packages/SystemUI/compose/features/Android.bp b/packages/SystemUI/compose/features/Android.bp index 796abf4b52d6..5ab22357713d 100644 --- a/packages/SystemUI/compose/features/Android.bp +++ b/packages/SystemUI/compose/features/Android.bp @@ -38,6 +38,7 @@ android_library { "androidx.compose.runtime_runtime", "androidx.compose.animation_animation-graphics", "androidx.compose.material3_material3", + "androidx.compose.material_material-icons-extended", "androidx.activity_activity-compose", ], 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 17c4e022ba82..8bd5ddb060c3 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 @@ -16,6 +16,7 @@ package com.android.systemui.communal.ui.compose +import android.appwidget.AppWidgetHostView import android.os.Bundle import android.util.SizeF import android.widget.FrameLayout @@ -204,8 +205,12 @@ private fun BoxScope.CommunalHubLazyGrid( list[index].size.dp().value, ) if (viewModel.isEditMode && dragDropState != null) { - DraggableItem(dragDropState = dragDropState, enabled = true, index = index) { - isDragging -> + DraggableItem( + dragDropState = dragDropState, + enabled = true, + index = index, + size = size + ) { isDragging -> val elevation by animateDpAsState(if (isDragging) 4.dp else 1.dp) CommunalContent( modifier = cardModifier, @@ -325,7 +330,7 @@ private fun CommunalContent( elevation: Dp = 0.dp, ) { when (model) { - is CommunalContentModel.Widget -> WidgetContent(model, size, elevation, modifier) + is CommunalContentModel.Widget -> WidgetContent(viewModel, model, size, elevation, modifier) is CommunalContentModel.WidgetPlaceholder -> WidgetPlaceholderContent(size) is CommunalContentModel.Smartspace -> SmartspaceContent(model, modifier) is CommunalContentModel.Tutorial -> TutorialContent(modifier) @@ -346,6 +351,7 @@ fun WidgetPlaceholderContent(size: SizeF) { @Composable private fun WidgetContent( + viewModel: BaseCommunalViewModel, model: CommunalContentModel.Widget, size: SizeF, elevation: Dp, @@ -358,9 +364,18 @@ private fun WidgetContent( AndroidView( modifier = modifier, factory = { context -> - model.appWidgetHost - .createView(context, model.appWidgetId, model.providerInfo) - .apply { updateAppWidgetSize(Bundle.EMPTY, listOf(size)) } + // The AppWidgetHostView will inherit the interaction handler from the + // AppWidgetHost. So set the interaction handler here before creating the view, and + // then clear it after the view is created. This is a workaround due to the fact + // that the interaction handler cannot be specified when creating the view, + // and there are race conditions if it is set after the view is created. + model.appWidgetHost.setInteractionHandler(viewModel.getInteractionHandler()) + val view = + model.appWidgetHost + .createView(context, model.appWidgetId, model.providerInfo) + .apply { updateAppWidgetSize(Bundle.EMPTY, listOf(size)) } + model.appWidgetHost.setInteractionHandler(null) + view }, // For reusing composition in lazy lists. onReset = {}, @@ -376,7 +391,7 @@ private fun SmartspaceContent( AndroidView( modifier = modifier, factory = { context -> - FrameLayout(context).apply { addView(model.remoteViews.apply(context, this)) } + AppWidgetHostView(context).apply { updateAppWidget(model.remoteViews) } }, // For reusing composition in lazy lists. onReset = {}, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt index 0d460aa8d464..1b40de4ef5df 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal.ui.compose +import android.util.SizeF import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress import androidx.compose.foundation.gestures.scrollBy @@ -233,6 +234,7 @@ fun LazyGridItemScope.DraggableItem( dragDropState: GridDragDropState, index: Int, enabled: Boolean, + size: SizeF, modifier: Modifier = Modifier, content: @Composable (isDragging: Boolean) -> Unit ) { @@ -250,7 +252,11 @@ fun LazyGridItemScope.DraggableItem( } else { Modifier.animateItemPlacement() } - Box(modifier = modifier.then(draggingModifier), propagateMinConstraints = true) { - content(dragging) + + Box(modifier) { + if (dragging) { + WidgetPlaceholderContent(size) + } + Box(modifier = draggingModifier, propagateMinConstraints = true) { content(dragging) } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt index 67a68200f269..ff53ff256931 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt @@ -37,9 +37,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -/** Set this to `true` to use the LockscreenContent replacement of KeyguardRootView. */ -private val UseLockscreenContent = false - /** The lock screen scene shows when the device is locked. */ @SysUISingleton class LockscreenScene @@ -48,7 +45,6 @@ constructor( @Application private val applicationScope: CoroutineScope, private val viewModel: LockscreenSceneViewModel, private val lockscreenContent: Lazy<LockscreenContent>, - private val viewBasedLockscreenContent: Lazy<ViewBasedLockscreenContent>, ) : ComposableScene { override val key = SceneKey.Lockscreen @@ -73,7 +69,6 @@ constructor( ) { LockscreenScene( lockscreenContent = lockscreenContent, - viewBasedLockscreenContent = viewBasedLockscreenContent, modifier = modifier, ) } @@ -93,22 +88,13 @@ constructor( } @Composable -private fun SceneScope.LockscreenScene( +private fun LockscreenScene( lockscreenContent: Lazy<LockscreenContent>, - viewBasedLockscreenContent: Lazy<ViewBasedLockscreenContent>, modifier: Modifier = Modifier, ) { - if (UseLockscreenContent) { - lockscreenContent - .get() - .Content( - modifier = modifier.fillMaxSize(), - ) - } else { - with(viewBasedLockscreenContent.get()) { - Content( - modifier = modifier.fillMaxSize(), - ) - } - } + lockscreenContent + .get() + .Content( + modifier = modifier.fillMaxSize(), + ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt index 9abb50c35ccf..3677cab890f5 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt @@ -20,6 +20,7 @@ import com.android.systemui.keyguard.ui.composable.blueprint.CommunalBlueprintMo import com.android.systemui.keyguard.ui.composable.blueprint.DefaultBlueprintModule import com.android.systemui.keyguard.ui.composable.blueprint.ShortcutsBesideUdfpsBlueprintModule import com.android.systemui.keyguard.ui.composable.blueprint.SplitShadeBlueprintModule +import com.android.systemui.keyguard.ui.composable.section.OptionalSectionModule import dagger.Module @Module( @@ -27,6 +28,7 @@ import dagger.Module [ CommunalBlueprintModule::class, DefaultBlueprintModule::class, + OptionalSectionModule::class, ShortcutsBesideUdfpsBlueprintModule::class, SplitShadeBlueprintModule::class, ], diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/ViewBasedLockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/ViewBasedLockscreenContent.kt deleted file mode 100644 index 8119d2a119ca..000000000000 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/ViewBasedLockscreenContent.kt +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) 2023 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.keyguard.ui.composable - -import android.graphics.Rect -import android.view.View -import android.view.ViewGroup -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.toComposeRect -import androidx.compose.ui.layout.Layout -import androidx.compose.ui.layout.onPlaced -import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.view.isVisible -import com.android.compose.animation.scene.SceneScope -import com.android.systemui.keyguard.qualifiers.KeyguardRootView -import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel -import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel -import com.android.systemui.notifications.ui.composable.NotificationStack -import com.android.systemui.res.R -import javax.inject.Inject - -/** - * Renders the content of the lockscreen. - * - * This is different from [LockscreenContent] (which is pure compose) and uses a view-based - * implementation of the lockscreen scene content that relies on [KeyguardRootView]. - * - * TODO(b/316211368): remove this once [LockscreenContent] is feature complete. - */ -class ViewBasedLockscreenContent -@Inject -constructor( - private val lockscreenSceneViewModel: LockscreenSceneViewModel, - @KeyguardRootView private val viewProvider: () -> @JvmSuppressWildcards View, - private val keyguardRootViewModel: KeyguardRootViewModel, -) { - @Composable - fun SceneScope.Content( - modifier: Modifier = Modifier, - ) { - fun findSettingsMenu(): View { - return viewProvider().requireViewById(R.id.keyguard_settings_button) - } - - LockscreenLongPress( - viewModel = lockscreenSceneViewModel.longPress, - modifier = modifier, - ) { onSettingsMenuPlaced -> - AndroidView( - factory = { _ -> - val keyguardRootView = viewProvider() - // Remove the KeyguardRootView from any parent it might already have in legacy - // code just in case (a view can't have two parents). - (keyguardRootView.parent as? ViewGroup)?.removeView(keyguardRootView) - keyguardRootView - }, - modifier = Modifier.fillMaxSize(), - ) - - val notificationStackPosition by - keyguardRootViewModel.notificationBounds.collectAsState() - - Layout( - modifier = - Modifier.fillMaxSize().onPlaced { - val settingsMenuView = findSettingsMenu() - onSettingsMenuPlaced( - if (settingsMenuView.isVisible) { - val bounds = Rect() - settingsMenuView.getHitRect(bounds) - bounds.toComposeRect() - } else { - null - } - ) - }, - content = { - NotificationStack( - viewModel = lockscreenSceneViewModel.notifications, - isScrimVisible = false, - ) - } - ) { measurables, constraints -> - check(measurables.size == 1) - val height = notificationStackPosition.height.toInt() - val childConstraints = constraints.copy(minHeight = height, maxHeight = height) - val placeable = measurables[0].measure(childConstraints) - layout(constraints.maxWidth, constraints.maxHeight) { - val start = (constraints.maxWidth - placeable.measuredWidth) / 2 - placeable.placeRelative(x = start, y = notificationStackPosition.top.toInt()) - } - } - } - } -} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt index 7385a251200e..84d42463913c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt @@ -38,6 +38,7 @@ import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel import dagger.Binds import dagger.Module import dagger.multibindings.IntoSet +import java.util.Optional import javax.inject.Inject /** @@ -53,7 +54,7 @@ constructor( private val smartSpaceSection: SmartSpaceSection, private val notificationSection: NotificationSection, private val lockSection: LockSection, - private val ambientIndicationSection: AmbientIndicationSection, + private val ambientIndicationSectionOptional: Optional<AmbientIndicationSection>, private val bottomAreaSection: BottomAreaSection, private val settingsMenuSection: SettingsMenuSection, private val clockInteractor: KeyguardClockInteractor, @@ -94,8 +95,8 @@ constructor( with(notificationSection) { Notifications(modifier = Modifier.fillMaxWidth().weight(1f)) } - if (!isUdfpsVisible) { - with(ambientIndicationSection) { + if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) { + with(ambientIndicationSectionOptional.get()) { AmbientIndication(modifier = Modifier.fillMaxWidth()) } } @@ -105,8 +106,8 @@ constructor( // Aligned to bottom and constrained to below the lock icon. Column(modifier = Modifier.fillMaxWidth()) { - if (isUdfpsVisible) { - with(ambientIndicationSection) { + if (isUdfpsVisible && ambientIndicationSectionOptional.isPresent) { + with(ambientIndicationSectionOptional.get()) { AmbientIndication(modifier = Modifier.fillMaxWidth()) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt index acd47797baca..414846276b2a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt @@ -38,6 +38,7 @@ import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel import dagger.Binds import dagger.Module import dagger.multibindings.IntoSet +import java.util.Optional import javax.inject.Inject /** @@ -53,7 +54,7 @@ constructor( private val smartSpaceSection: SmartSpaceSection, private val notificationSection: NotificationSection, private val lockSection: LockSection, - private val ambientIndicationSection: AmbientIndicationSection, + private val ambientIndicationSectionOptional: Optional<AmbientIndicationSection>, private val bottomAreaSection: BottomAreaSection, private val settingsMenuSection: SettingsMenuSection, private val clockInteractor: KeyguardClockInteractor, @@ -94,8 +95,8 @@ constructor( with(notificationSection) { Notifications(modifier = Modifier.fillMaxWidth().weight(1f)) } - if (!isUdfpsVisible) { - with(ambientIndicationSection) { + if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) { + with(ambientIndicationSectionOptional.get()) { AmbientIndication(modifier = Modifier.fillMaxWidth()) } } @@ -111,8 +112,8 @@ constructor( // Aligned to bottom and constrained to below the lock icon. Column(modifier = Modifier.fillMaxWidth()) { - if (isUdfpsVisible) { - with(ambientIndicationSection) { + if (isUdfpsVisible && ambientIndicationSectionOptional.isPresent) { + with(ambientIndicationSectionOptional.get()) { AmbientIndication(modifier = Modifier.fillMaxWidth()) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/AmbientIndicationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/AmbientIndicationSection.kt index 0e7ac5ec046a..af9a195c4575 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/AmbientIndicationSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/AmbientIndicationSection.kt @@ -16,36 +16,11 @@ package com.android.systemui.keyguard.ui.composable.section -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.SceneScope -import javax.inject.Inject -class AmbientIndicationSection @Inject constructor() { - @Composable - fun SceneScope.AmbientIndication(modifier: Modifier = Modifier) { - MovableElement( - key = AmbientIndicationElementKey, - modifier = modifier, - ) { - Box( - modifier = Modifier.fillMaxWidth().background(Color.Green), - ) { - Text( - text = "TODO(b/316211368): Ambient indication", - color = Color.White, - modifier = Modifier.align(Alignment.Center), - ) - } - } - } +/** Defines interface for classes that can render the ambient indication area. */ +interface AmbientIndicationSection { + @Composable fun SceneScope.AmbientIndication(modifier: Modifier) } - -private val AmbientIndicationElementKey = ElementKey("AmbientIndication") diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt index 4f3498e8dff5..8bd0d45920f4 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt @@ -74,20 +74,22 @@ constructor( key = if (isStart) StartButtonElementKey else EndButtonElementKey, modifier = modifier, ) { - Shortcut( - viewId = if (isStart) R.id.start_button else R.id.end_button, - viewModel = if (isStart) viewModel.startButton else viewModel.endButton, - transitionAlpha = viewModel.transitionAlpha, - falsingManager = falsingManager, - vibratorHelper = vibratorHelper, - indicationController = indicationController, - modifier = - if (applyPadding) { - Modifier.shortcutPadding() - } else { - Modifier - } - ) + content { + Shortcut( + viewId = if (isStart) R.id.start_button else R.id.end_button, + viewModel = if (isStart) viewModel.startButton else viewModel.endButton, + transitionAlpha = viewModel.transitionAlpha, + falsingManager = falsingManager, + vibratorHelper = vibratorHelper, + indicationController = indicationController, + modifier = + if (applyPadding) { + Modifier.shortcutPadding() + } else { + Modifier + } + ) + } } } @@ -99,11 +101,13 @@ constructor( key = IndicationAreaElementKey, modifier = modifier.shortcutPadding(), ) { - IndicationArea( - indicationAreaViewModel = indicationAreaViewModel, - alphaViewModel = alphaViewModel, - indicationController = indicationController, - ) + content { + IndicationArea( + indicationAreaViewModel = indicationAreaViewModel, + alphaViewModel = alphaViewModel, + indicationController = indicationController, + ) + } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt index 0b49922a8412..f021bb6743c4 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt @@ -16,49 +16,73 @@ package com.android.systemui.keyguard.ui.composable.section -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.viewinterop.AndroidView import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.SceneScope +import com.android.compose.modifiers.padding +import com.android.keyguard.KeyguardClockSwitch +import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.composable.modifier.onTopPlacementChanged import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel +import com.android.systemui.res.R import javax.inject.Inject class ClockSection @Inject constructor( private val viewModel: KeyguardClockViewModel, + private val clockInteractor: KeyguardClockInteractor, ) { + @Composable fun SceneScope.SmallClock( onTopChanged: (top: Float?) -> Unit, modifier: Modifier = Modifier, ) { - if (viewModel.useLargeClock) { + val clockSize by viewModel.clockSize.collectAsState() + val currentClock by viewModel.currentClock.collectAsState() + viewModel.clock = currentClock + + if (clockSize != KeyguardClockSwitch.SMALL) { onTopChanged(null) return } + if (currentClock?.smallClock?.view == null) { + return + } + + val view = LocalView.current + + DisposableEffect(view) { + clockInteractor.clockEventController.registerListeners(view) + + onDispose { clockInteractor.clockEventController.unregisterListeners() } + } + MovableElement( key = ClockElementKey, modifier = modifier, ) { - Box( - modifier = - Modifier.fillMaxWidth() - .background(Color.Magenta) - .onTopPlacementChanged(onTopChanged) - ) { - Text( - text = "TODO(b/316211368): Small clock", - color = Color.White, - modifier = Modifier.align(Alignment.Center), + content { + AndroidView( + factory = { checkNotNull(currentClock).smallClock.view }, + modifier = + Modifier.padding( + horizontal = + dimensionResource(R.dimen.keyguard_affordance_horizontal_offset) + ) + .padding(top = { viewModel.getSmallClockTopMargin(view.context) }) + .onTopPlacementChanged(onTopChanged), ) } } @@ -66,21 +90,36 @@ constructor( @Composable fun SceneScope.LargeClock(modifier: Modifier = Modifier) { - if (!viewModel.useLargeClock) { + val clockSize by viewModel.clockSize.collectAsState() + val currentClock by viewModel.currentClock.collectAsState() + viewModel.clock = currentClock + + if (clockSize != KeyguardClockSwitch.LARGE) { + return + } + + if (currentClock?.largeClock?.view == null) { return } + val view = LocalView.current + + DisposableEffect(view) { + clockInteractor.clockEventController.registerListeners(view) + + onDispose { clockInteractor.clockEventController.unregisterListeners() } + } + MovableElement( key = ClockElementKey, modifier = modifier, ) { - Box( - modifier = Modifier.fillMaxWidth().background(Color.Blue), - ) { - Text( - text = "TODO(b/316211368): Large clock", - color = Color.White, - modifier = Modifier.align(Alignment.Center), + content { + AndroidView( + factory = { checkNotNull(currentClock).largeClock.view }, + modifier = + Modifier.fillMaxWidth() + .padding(top = { viewModel.getLargeClockTopMargin(view.context) }) ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt index c547e2b93158..900616f6af89 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt @@ -22,7 +22,6 @@ import com.android.compose.animation.scene.SceneScope import com.android.systemui.notifications.ui.composable.NotificationStack import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher class NotificationSection @Inject diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/OptionalSectionModule.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/OptionalSectionModule.kt new file mode 100644 index 000000000000..5b7a8e6eb52f --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/OptionalSectionModule.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 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.keyguard.ui.composable.section + +import dagger.BindsOptionalOf +import dagger.Module + +/** + * Dagger module for providing placeholders for optional lockscreen scene sections that don't exist + * in AOSP but may be provided by OEMs. + */ +@Module +interface OptionalSectionModule { + @BindsOptionalOf fun ambientIndicationSection(): AmbientIndicationSection +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt index 5727e3436126..ddc12ff22d50 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt @@ -53,37 +53,43 @@ constructor( key = StatusBarElementKey, modifier = modifier, ) { - AndroidView( - factory = { - notificationPanelView.get().findViewById<View>(R.id.keyguard_header)?.let { - (it.parent as ViewGroup).removeView(it) - } + content { + AndroidView( + factory = { + notificationPanelView.get().findViewById<View>(R.id.keyguard_header)?.let { + (it.parent as ViewGroup).removeView(it) + } + + val provider = + object : ShadeViewStateProvider { + override val lockscreenShadeDragProgress: Float = 0f + override val panelViewExpandedHeight: Float = 0f - val provider = - object : ShadeViewStateProvider { - override val lockscreenShadeDragProgress: Float = 0f - override val panelViewExpandedHeight: Float = 0f - override fun shouldHeadsUpBeVisible(): Boolean { - return false + override fun shouldHeadsUpBeVisible(): Boolean { + return false + } } - } - @SuppressLint("InflateParams") - val view = - LayoutInflater.from(context) - .inflate( - R.layout.keyguard_status_bar, - null, - false, - ) as KeyguardStatusBarView - componentFactory.build(view, provider).keyguardStatusBarViewController.init() - view - }, - modifier = - Modifier.fillMaxWidth().padding(horizontal = 16.dp).height { - Utils.getStatusBarHeaderHeightKeyguard(context) + @SuppressLint("InflateParams") + val view = + LayoutInflater.from(context) + .inflate( + R.layout.keyguard_status_bar, + null, + false, + ) as KeyguardStatusBarView + componentFactory + .build(view, provider) + .keyguardStatusBarViewController + .init() + view }, - ) + modifier = + Modifier.fillMaxWidth().padding(horizontal = 16.dp).height { + Utils.getStatusBarHeaderHeightKeyguard(context) + }, + ) + } } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index 12f1b301c836..0eec024d3c81 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -44,7 +44,7 @@ import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.ValueKey -import com.android.compose.animation.scene.animateSharedFloatAsState +import com.android.compose.animation.scene.animateElementFloatAsState import com.android.systemui.notifications.ui.composable.Notifications.Form import com.android.systemui.notifications.ui.composable.Notifications.SharedValues.SharedExpansionValue import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel @@ -157,10 +157,10 @@ private fun SceneScope.NotificationPlaceholder( modifier: Modifier = Modifier, ) { val elementKey = Notifications.Elements.NotificationPlaceholder - Box( + Element( + elementKey, modifier = modifier - .element(elementKey) .debugBackground(viewModel) .onSizeChanged { size: IntSize -> debugLog(viewModel) { "STACK onSizeChanged: size=$size" } @@ -182,19 +182,23 @@ private fun SceneScope.NotificationPlaceholder( } ) { val animatedExpansion by - animateSharedFloatAsState( + animateElementFloatAsState( value = if (form == Form.HunFromTop) 0f else 1f, - key = SharedExpansionValue, - element = elementKey + key = SharedExpansionValue ) debugLog(viewModel) { "STACK composed: expansion=$animatedExpansion" } - if (viewModel.isPlaceholderTextVisible) { - Text( - text = "Notifications", - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.align(Alignment.Center), - ) + + content { + if (viewModel.isPlaceholderTextVisible) { + Box(Modifier.fillMaxSize()) { + Text( + text = "Notifications", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.align(Alignment.Center), + ) + } + } } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt index f3cde534ebaa..9778e53d8f69 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt @@ -40,6 +40,7 @@ import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Companion.Collaps import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Expanding import com.android.systemui.res.R import com.android.systemui.scene.ui.composable.Gone +import com.android.systemui.scene.ui.composable.Lockscreen import com.android.systemui.scene.ui.composable.QuickSettings as QuickSettingsSceneKey import com.android.systemui.scene.ui.composable.Shade @@ -77,7 +78,12 @@ private fun SceneScope.stateForQuickSettingsContent(): QSSceneAdapter.State { toScene == Shade -> QSSceneAdapter.State.QQS toScene == QuickSettingsSceneKey -> QSSceneAdapter.State.QS toScene == Gone -> QSSceneAdapter.State.CLOSED - else -> error("Bad transition for QuickSettings: $transitionState") + toScene == Lockscreen -> QSSceneAdapter.State.CLOSED + else -> + error( + "Bad transition for QuickSettings: fromScene=$fromScene," + + " toScene=$toScene" + ) } } } @@ -98,7 +104,7 @@ fun SceneScope.QuickSettings( key = QuickSettings.Elements.Content, modifier = modifier.fillMaxWidth().defaultMinSize(minHeight = 300.dp) ) { - QuickSettingsContent(qsSceneAdapter = qsSceneAdapter, contentState) + content { QuickSettingsContent(qsSceneAdapter = qsSceneAdapter, contentState) } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt index 4bbb78b69392..e2beaeea6402 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt @@ -50,7 +50,7 @@ import androidx.compose.ui.viewinterop.AndroidView import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.ValueKey -import com.android.compose.animation.scene.animateSharedFloatAsState +import com.android.compose.animation.scene.animateSceneFloatAsState import com.android.compose.windowsizeclass.LocalWindowSizeClass import com.android.settingslib.Utils import com.android.systemui.battery.BatteryMeterView @@ -69,7 +69,6 @@ import com.android.systemui.statusbar.policy.Clock object ShadeHeader { object Elements { - val FormatPlaceholder = ElementKey("ShadeHeaderFormatPlaceholder") val ExpandedContent = ElementKey("ShadeHeaderExpandedContent") val CollapsedContent = ElementKey("ShadeHeaderCollapsedContent") } @@ -92,19 +91,19 @@ fun SceneScope.CollapsedShadeHeader( statusBarIconController: StatusBarIconController, modifier: Modifier = Modifier, ) { - // TODO(b/298153892): Remove this once animateSharedFloatAsState.element can be null. - Spacer(Modifier.element(ShadeHeader.Elements.FormatPlaceholder)) val formatProgress = - animateSharedFloatAsState( - 0.0f, - ShadeHeader.Keys.transitionProgress, - ShadeHeader.Elements.FormatPlaceholder - ) + animateSceneFloatAsState(0f, ShadeHeader.Keys.transitionProgress) + .unsafeCompositionState(initialValue = 0f) val cutoutWidth = LocalDisplayCutout.current.width() val cutoutLocation = LocalDisplayCutout.current.location - val useExpandedFormat = formatProgress.value > 0.5f || cutoutLocation != CutoutLocation.CENTER + val useExpandedFormat by + remember(formatProgress) { + derivedStateOf { + cutoutLocation != CutoutLocation.CENTER || formatProgress.value > 0.5f + } + } // This layout assumes it is globally positioned at (0, 0) and is the // same size as the screen. @@ -217,14 +216,9 @@ fun SceneScope.ExpandedShadeHeader( statusBarIconController: StatusBarIconController, modifier: Modifier = Modifier, ) { - // TODO(b/298153892): Remove this once animateSharedFloatAsState.element can be null. - Spacer(Modifier.element(ShadeHeader.Elements.FormatPlaceholder)) val formatProgress = - animateSharedFloatAsState( - 1.0f, - ShadeHeader.Keys.transitionProgress, - ShadeHeader.Elements.FormatPlaceholder - ) + animateSceneFloatAsState(1f, ShadeHeader.Keys.transitionProgress) + .unsafeCompositionState(initialValue = 1f) val useExpandedFormat by remember(formatProgress) { derivedStateOf { formatProgress.value > 0.5f } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt index 2944bd9f9a8e..b26194f2397b 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt @@ -17,10 +17,15 @@ package com.android.compose.animation.scene import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.snapshots.Snapshot +import androidx.compose.runtime.snapshotFlow +import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.lerp import androidx.compose.ui.unit.Dp @@ -28,180 +33,263 @@ import androidx.compose.ui.unit.lerp import com.android.compose.ui.util.lerp /** - * Animate a shared Int value. + * A [State] whose [value] is animated. * - * @see SceneScope.animateSharedValueAsState + * Important: This animated value should always be ready *after* composition, e.g. during layout, + * drawing or inside a LaunchedEffect. If you read [value] during composition, it will probably + * throw an exception, for 2 important reasons: + * 1. You should never read animated values during composition, because this will probably lead to + * bad performance. + * 2. Given that this value depends on the target value in different scenes, its current value + * (depending on the current transition state) can only be computed once the full tree has been + * composed. + * + * If you don't have the choice and *have to* get the value during composition, for instance because + * a Modifier or Composable reading this value does not have a lazy/lambda-based API, then you can + * access [unsafeCompositionState] and use a fallback value for the first frame where this animated + * value can not be computed yet. Note however that doing so will be bad for performance and might + * lead to late-by-one-frame flickers. + */ +@Stable +interface AnimatedState<T> : State<T> { + /** + * Return a [State] that can be read during composition. + * + * Important: You should avoid using this as much as possible and instead read [value] during + * layout/drawing, otherwise you will probably end up with a few frames that have a value that + * is not correctly interpolated. + */ + @Composable fun unsafeCompositionState(initialValue: T): State<T> +} + +/** + * Animate a scene Int value. + * + * @see SceneScope.animateSceneValueAsState */ @Composable -fun SceneScope.animateSharedIntAsState( +fun SceneScope.animateSceneIntAsState( value: Int, key: ValueKey, - element: ElementKey?, canOverflow: Boolean = true, -): State<Int> { - return animateSharedValueAsState(value, key, element, ::lerp, canOverflow) +): AnimatedState<Int> { + return animateSceneValueAsState(value, key, ::lerp, canOverflow) } /** - * Animate a shared Int value. + * Animate a shared element Int value. * - * @see MovableElementScope.animateSharedValueAsState + * @see ElementScope.animateElementValueAsState */ @Composable -fun MovableElementScope.animateSharedIntAsState( +fun ElementScope<*>.animateElementIntAsState( value: Int, - debugName: String, + key: ValueKey, canOverflow: Boolean = true, -): State<Int> { - return animateSharedValueAsState(value, debugName, ::lerp, canOverflow) +): AnimatedState<Int> { + return animateElementValueAsState(value, key, ::lerp, canOverflow) } /** - * Animate a shared Float value. + * Animate a scene Float value. * - * @see SceneScope.animateSharedValueAsState + * @see SceneScope.animateSceneValueAsState */ @Composable -fun SceneScope.animateSharedFloatAsState( +fun SceneScope.animateSceneFloatAsState( value: Float, key: ValueKey, - element: ElementKey?, canOverflow: Boolean = true, -): State<Float> { - return animateSharedValueAsState(value, key, element, ::lerp, canOverflow) +): AnimatedState<Float> { + return animateSceneValueAsState(value, key, ::lerp, canOverflow) } /** - * Animate a shared Float value. + * Animate a shared element Float value. * - * @see MovableElementScope.animateSharedValueAsState + * @see ElementScope.animateElementValueAsState */ @Composable -fun MovableElementScope.animateSharedFloatAsState( +fun ElementScope<*>.animateElementFloatAsState( value: Float, - debugName: String, + key: ValueKey, canOverflow: Boolean = true, -): State<Float> { - return animateSharedValueAsState(value, debugName, ::lerp, canOverflow) +): AnimatedState<Float> { + return animateElementValueAsState(value, key, ::lerp, canOverflow) } /** - * Animate a shared Dp value. + * Animate a scene Dp value. * - * @see SceneScope.animateSharedValueAsState + * @see SceneScope.animateSceneValueAsState */ @Composable -fun SceneScope.animateSharedDpAsState( +fun SceneScope.animateSceneDpAsState( value: Dp, key: ValueKey, - element: ElementKey?, canOverflow: Boolean = true, -): State<Dp> { - return animateSharedValueAsState(value, key, element, ::lerp, canOverflow) +): AnimatedState<Dp> { + return animateSceneValueAsState(value, key, ::lerp, canOverflow) } /** - * Animate a shared Dp value. + * Animate a shared element Dp value. * - * @see MovableElementScope.animateSharedValueAsState + * @see ElementScope.animateElementValueAsState */ @Composable -fun MovableElementScope.animateSharedDpAsState( +fun ElementScope<*>.animateElementDpAsState( value: Dp, - debugName: String, + key: ValueKey, canOverflow: Boolean = true, -): State<Dp> { - return animateSharedValueAsState(value, debugName, ::lerp, canOverflow) +): AnimatedState<Dp> { + return animateElementValueAsState(value, key, ::lerp, canOverflow) } /** - * Animate a shared Color value. + * Animate a scene Color value. * - * @see SceneScope.animateSharedValueAsState + * @see SceneScope.animateSceneValueAsState */ @Composable -fun SceneScope.animateSharedColorAsState( +fun SceneScope.animateSceneColorAsState( value: Color, key: ValueKey, - element: ElementKey?, -): State<Color> { - return animateSharedValueAsState(value, key, element, ::lerp, canOverflow = false) +): AnimatedState<Color> { + return animateSceneValueAsState(value, key, ::lerp, canOverflow = false) } /** - * Animate a shared Color value. + * Animate a shared element Color value. * - * @see MovableElementScope.animateSharedValueAsState + * @see ElementScope.animateElementValueAsState */ @Composable -fun MovableElementScope.animateSharedColorAsState( +fun ElementScope<*>.animateElementColorAsState( value: Color, - debugName: String, -): State<Color> { - return animateSharedValueAsState(value, debugName, ::lerp, canOverflow = false) + key: ValueKey, +): AnimatedState<Color> { + return animateElementValueAsState(value, key, ::lerp, canOverflow = false) } @Composable internal fun <T> animateSharedValueAsState( layoutImpl: SceneTransitionLayoutImpl, - scene: Scene, - element: Element?, + scene: SceneKey, + element: ElementKey?, key: ValueKey, value: T, lerp: (T, T, Float) -> T, canOverflow: Boolean, -): State<T> { - val sharedValue = - Snapshot.withoutReadObservation { - val sharedValues = - element?.sceneValues?.getValue(scene.key)?.sharedValues ?: scene.sharedValues - sharedValues.getOrPut(key) { Element.SharedValue(key, value) } as Element.SharedValue<T> - } +): AnimatedState<T> { + DisposableEffect(layoutImpl, scene, element, key) { + // Create the associated maps that hold the current value for each (element, scene) pair. + val valueMap = layoutImpl.sharedValues.getOrPut(key) { mutableMapOf() } + val sceneToValueMap = + valueMap.getOrPut(element) { SnapshotStateMap<SceneKey, Any>() } + as SnapshotStateMap<SceneKey, T> + sceneToValueMap[scene] = value + + onDispose { + // Remove the value associated to the current scene, and eventually remove the maps if + // they are empty. + sceneToValueMap.remove(scene) - if (value != sharedValue.value) { - sharedValue.value = value + if (sceneToValueMap.isEmpty() && valueMap[element] === sceneToValueMap) { + valueMap.remove(element) + + if (valueMap.isEmpty() && layoutImpl.sharedValues[key] === valueMap) { + layoutImpl.sharedValues.remove(key) + } + } + } } - return remember(layoutImpl, element, sharedValue, lerp, canOverflow) { - derivedStateOf { computeValue(layoutImpl, element, sharedValue, lerp, canOverflow) } + // Update the current value. Note that side effects run after disposable effects, so we know + // that the associated maps were created at this point. + SideEffect { sceneToValueMap<T>(layoutImpl, key, element)[scene] = value } + + return remember(layoutImpl, scene, element, lerp, canOverflow) { + object : AnimatedState<T> { + override val value: T + get() = value(layoutImpl, scene, element, key, lerp, canOverflow) + + @Composable + override fun unsafeCompositionState(initialValue: T): State<T> { + val state = remember { mutableStateOf(initialValue) } + + val animatedState = this + LaunchedEffect(animatedState) { + snapshotFlow { animatedState.value }.collect { state.value = it } + } + + return state + } + } } } -private fun <T> computeValue( +private fun <T> sceneToValueMap( layoutImpl: SceneTransitionLayoutImpl, - element: Element?, - sharedValue: Element.SharedValue<T>, + key: ValueKey, + element: ElementKey? +): MutableMap<SceneKey, T> { + return layoutImpl.sharedValues[key]?.get(element)?.let { it as SnapshotStateMap<SceneKey, T> } + ?: error(valueReadTooEarlyMessage(key)) +} + +private fun valueReadTooEarlyMessage(key: ValueKey) = + "Animated value $key was read before its target values were set. This probably " + + "means that you are reading it during composition, which you should not do. See the " + + "documentation of AnimatedState for more information." + +private fun <T> value( + layoutImpl: SceneTransitionLayoutImpl, + scene: SceneKey, + element: ElementKey?, + key: ValueKey, lerp: (T, T, Float) -> T, canOverflow: Boolean, ): T { - val transition = layoutImpl.state.currentTransition - if (transition == null || !layoutImpl.isTransitionReady(transition)) { - return sharedValue.value - } + return valueOrNull(layoutImpl, scene, element, key, lerp, canOverflow) + ?: error(valueReadTooEarlyMessage(key)) +} - fun sceneValue(scene: SceneKey): Element.SharedValue<T>? { - val sharedValues = - if (element == null) { - layoutImpl.scene(scene).sharedValues - } else { - element.sceneValues[scene]?.sharedValues - } - ?: return null - val value = sharedValues[sharedValue.key] ?: return null - return value as Element.SharedValue<T> - } +private fun <T> valueOrNull( + layoutImpl: SceneTransitionLayoutImpl, + scene: SceneKey, + element: ElementKey?, + key: ValueKey, + lerp: (T, T, Float) -> T, + canOverflow: Boolean, +): T? { + val sceneToValueMap = sceneToValueMap<T>(layoutImpl, key, element) + fun sceneValue(scene: SceneKey): T? = sceneToValueMap[scene] - val fromValue = sceneValue(transition.fromScene) - val toValue = sceneValue(transition.toScene) - return if (fromValue != null && toValue != null) { - val progress = - if (canOverflow) transition.progress else transition.progress.coerceIn(0f, 1f) - lerp(fromValue.value, toValue.value, progress) - } else if (fromValue != null) { - fromValue.value - } else if (toValue != null) { - toValue.value - } else { - sharedValue.value + return when (val transition = layoutImpl.state.transitionState) { + is TransitionState.Idle -> sceneValue(transition.currentScene) + is TransitionState.Transition -> { + // Note: no need to check for transition ready here given that all target values are + // defined during composition, we should already have the correct values to interpolate + // between here. + val fromValue = sceneValue(transition.fromScene) + val toValue = sceneValue(transition.toScene) + if (fromValue != null && toValue != null) { + if (fromValue == toValue) { + // Optimization: avoid reading progress if the values are the same, so we don't + // relayout/redraw for nothing. + fromValue + } else { + val progress = + if (canOverflow) transition.progress + else transition.progress.coerceIn(0f, 1f) + lerp(fromValue, toValue, progress) + } + } else fromValue ?: toValue + } } + // TODO(b/311600838): Remove this. We should not have to fallback to the current scene value, + // but we have to because code of removed nodes can still run if they are placed with a graphics + // layer. + ?: sceneValue(scene) } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt index a85d9bff283e..280fbfb7d3d3 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt @@ -16,15 +16,10 @@ package com.android.compose.animation.scene -import android.graphics.Picture -import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue -import androidx.compose.runtime.movableContentOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.Snapshot -import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset @@ -52,43 +47,22 @@ import kotlinx.coroutines.launch @Stable internal class Element(val key: ElementKey) { /** - * The last values of this element, coming from any scene. Note that this value will be unstable + * The last state of this element, coming from any scene. Note that this state will be unstable * if this element is present in multiple scenes but the shared element animation is disabled, - * given that multiple instances of the element with different states will write to these - * values. You should prefer using [TargetValues.lastValues] in the current scene if it is - * defined. + * given that multiple instances of the element with different states will write to this state. + * You should prefer using [SceneState.lastState] in the current scene when it is defined. */ - val lastSharedValues = Values() + val lastSharedState = State() - /** The mapping between a scene and the values/state this element has in that scene, if any. */ - val sceneValues = SnapshotStateMap<SceneKey, TargetValues>() - - /** - * The movable content of this element, if this element is composed using - * [SceneScope.MovableElement]. - */ - private var _movableContent: (@Composable (@Composable () -> Unit) -> Unit)? = null - val movableContent: @Composable (@Composable () -> Unit) -> Unit - get() = - _movableContent - ?: movableContentOf { content: @Composable () -> Unit -> content() } - .also { _movableContent = it } - - /** - * The [Picture] to which we save the last drawing commands of this element, if it is movable. - * This is necessary because the content of this element might not be composed in the scene it - * should currently be drawn. - */ - private var _picture: Picture? = null - val picture: Picture - get() = _picture ?: Picture().also { _picture = it } + /** The mapping between a scene and the state this element has in that scene, if any. */ + val sceneStates = mutableMapOf<SceneKey, SceneState>() override fun toString(): String { return "Element(key=$key)" } - /** The current values of this element, either in a specific scene or in a shared context. */ - class Values { + /** The state of this element, either in a specific scene or in a shared context. */ + class State { /** The offset of the element, relative to the SceneTransitionLayout containing it. */ var offset = Offset.Unspecified @@ -102,16 +76,14 @@ internal class Element(val key: ElementKey) { var alpha = AlphaUnspecified } - /** The target values of this element in a given scene. */ + /** The last and target state of this element in a given scene. */ @Stable - class TargetValues(val scene: SceneKey) { - val lastValues = Values() + class SceneState(val scene: SceneKey) { + val lastState = State() var targetSize by mutableStateOf(SizeUnspecified) var targetOffset by mutableStateOf(Offset.Unspecified) - val sharedValues = SnapshotStateMap<ValueKey, SharedValue<*>>() - /** * The attached [ElementNode] a Modifier.element() for a given element and scene. During * composition, this set could have 0 to 2 elements. After composition and after all @@ -120,12 +92,6 @@ internal class Element(val key: ElementKey) { val nodes = mutableSetOf<ElementNode>() } - /** A shared value of this element. */ - @Stable - class SharedValue<T>(val key: ValueKey, initialValue: T) { - var value by mutableStateOf(initialValue) - } - companion object { val SizeUnspecified = IntSize(Int.MAX_VALUE, Int.MAX_VALUE) val AlphaUnspecified = Float.MIN_VALUE @@ -147,27 +113,18 @@ internal fun Modifier.element( scene: Scene, key: ElementKey, ): Modifier { - val element: Element - val sceneValues: Element.TargetValues - - // Get the element associated to [key] if it was already composed in another scene, - // otherwise create it and add it to our Map<ElementKey, Element>. This is done inside a - // withoutReadObservation() because there is no need to recompose when that map is mutated. - Snapshot.withoutReadObservation { - element = layoutImpl.elements[key] ?: Element(key).also { layoutImpl.elements[key] = it } - sceneValues = - element.sceneValues[scene.key] - ?: Element.TargetValues(scene.key).also { element.sceneValues[scene.key] = it } - } - - return this.then(ElementModifier(layoutImpl, scene, element, sceneValues)) + return this.then(ElementModifier(layoutImpl, scene, key)) // TODO(b/311132415): Move this into ElementNode once we can create a delegate // IntermediateLayoutModifierNode. .intermediateLayout { measurable, constraints -> - val placeable = - measure(layoutImpl, scene, element, sceneValues, measurable, constraints) + // TODO(b/311132415): No need to fetch the element and sceneState from the map anymore + // once this is merged into ElementNode. + val element = layoutImpl.elements.getValue(key) + val sceneState = element.sceneStates.getValue(scene.key) + + val placeable = measure(layoutImpl, scene, element, sceneState, measurable, constraints) layout(placeable.width, placeable.height) { - place(layoutImpl, scene, element, sceneValues, placeable, placementScope = this) + place(layoutImpl, scene, element, sceneState, placeable, placementScope = this) } } .testTag(key.testTag) @@ -180,72 +137,89 @@ internal fun Modifier.element( private data class ElementModifier( private val layoutImpl: SceneTransitionLayoutImpl, private val scene: Scene, - private val element: Element, - private val sceneValues: Element.TargetValues, + private val key: ElementKey, ) : ModifierNodeElement<ElementNode>() { - override fun create(): ElementNode = ElementNode(layoutImpl, scene, element, sceneValues) + override fun create(): ElementNode = ElementNode(layoutImpl, scene, key) override fun update(node: ElementNode) { - node.update(layoutImpl, scene, element, sceneValues) + node.update(layoutImpl, scene, key) } } internal class ElementNode( private var layoutImpl: SceneTransitionLayoutImpl, private var scene: Scene, - private var element: Element, - private var sceneValues: Element.TargetValues, + private var key: ElementKey, ) : Modifier.Node(), DrawModifierNode { + private var _element: Element? = null + private val element: Element + get() = _element!! + + private var _sceneState: Element.SceneState? = null + private val sceneState: Element.SceneState + get() = _sceneState!! override fun onAttach() { super.onAttach() - addNodeToSceneValues() + updateElementAndSceneValues() + addNodeToSceneState() } - private fun addNodeToSceneValues() { - sceneValues.nodes.add(this) + private fun updateElementAndSceneValues() { + val element = + layoutImpl.elements[key] ?: Element(key).also { layoutImpl.elements[key] = it } + _element = element + _sceneState = + element.sceneStates[scene.key] + ?: Element.SceneState(scene.key).also { element.sceneStates[scene.key] = it } + } + + private fun addNodeToSceneState() { + sceneState.nodes.add(this) coroutineScope.launch { // At this point all [CodeLocationNode] have been attached or detached, which means that - // [sceneValues.codeLocations] should have exactly 1 element, otherwise this means that + // [sceneState.codeLocations] should have exactly 1 element, otherwise this means that // this element was composed multiple times in the same scene. - val nCodeLocations = sceneValues.nodes.size - if (nCodeLocations != 1 || !sceneValues.nodes.contains(this@ElementNode)) { - error("${element.key} was composed $nCodeLocations times in ${sceneValues.scene}") + val nCodeLocations = sceneState.nodes.size + if (nCodeLocations != 1 || !sceneState.nodes.contains(this@ElementNode)) { + error("$key was composed $nCodeLocations times in ${sceneState.scene}") } } } override fun onDetach() { super.onDetach() - removeNodeFromSceneValues() - maybePruneMaps(layoutImpl, element, sceneValues) + removeNodeFromSceneState() + maybePruneMaps(layoutImpl, element, sceneState) + + _element = null + _sceneState = null } - private fun removeNodeFromSceneValues() { - sceneValues.nodes.remove(this) + private fun removeNodeFromSceneState() { + sceneState.nodes.remove(this) } fun update( layoutImpl: SceneTransitionLayoutImpl, scene: Scene, - element: Element, - sceneValues: Element.TargetValues, + key: ElementKey, ) { check(layoutImpl == this.layoutImpl && scene == this.scene) - removeNodeFromSceneValues() + removeNodeFromSceneState() val prevElement = this.element - val prevSceneValues = this.sceneValues - this.element = element - this.sceneValues = sceneValues + val prevSceneState = this.sceneState + this.key = key + updateElementAndSceneValues() - addNodeToSceneValues() - maybePruneMaps(layoutImpl, prevElement, prevSceneValues) + addNodeToSceneState() + maybePruneMaps(layoutImpl, prevElement, prevSceneState) } override fun ContentDrawScope.draw() { - val drawScale = getDrawScale(layoutImpl, element, scene, sceneValues) + val drawScale = getDrawScale(layoutImpl, element, scene, sceneState) if (drawScale == Scale.Default) { drawContent() } else { @@ -263,18 +237,16 @@ internal class ElementNode( private fun maybePruneMaps( layoutImpl: SceneTransitionLayoutImpl, element: Element, - sceneValues: Element.TargetValues, + sceneState: Element.SceneState, ) { // If element is not composed from this scene anymore, remove the scene values. This // works because [onAttach] is called before [onDetach], so if an element is moved from // the UI tree we will first add the new code location then remove the old one. - if ( - sceneValues.nodes.isEmpty() && element.sceneValues[sceneValues.scene] == sceneValues - ) { - element.sceneValues.remove(sceneValues.scene) + if (sceneState.nodes.isEmpty() && element.sceneStates[sceneState.scene] == sceneState) { + element.sceneStates.remove(sceneState.scene) // If the element is not composed in any scene, remove it from the elements map. - if (element.sceneValues.isEmpty() && layoutImpl.elements[element.key] == element) { + if (element.sceneStates.isEmpty() && layoutImpl.elements[element.key] == element) { layoutImpl.elements.remove(element.key) } } @@ -293,8 +265,8 @@ private fun shouldDrawElement( if ( transition == null || !layoutImpl.isTransitionReady(transition) || - transition.fromScene !in element.sceneValues || - transition.toScene !in element.sceneValues + transition.fromScene !in element.sceneStates || + transition.toScene !in element.sceneStates ) { return true } @@ -310,7 +282,6 @@ private fun shouldDrawElement( transition, scene.key, element.key, - sharedTransformation, ) } @@ -319,17 +290,14 @@ internal fun shouldDrawOrComposeSharedElement( transition: TransitionState.Transition, scene: SceneKey, element: ElementKey, - sharedTransformation: SharedElementTransformation? ): Boolean { - val scenePicker = sharedTransformation?.scenePicker ?: DefaultSharedElementScenePicker + val scenePicker = element.scenePicker val fromScene = transition.fromScene val toScene = transition.toScene return scenePicker.sceneDuringTransition( element = element, - fromScene = fromScene, - toScene = toScene, - progress = transition::progress, + transition = transition, fromSceneZIndex = layoutImpl.scenes.getValue(fromScene).zIndex, toSceneZIndex = layoutImpl.scenes.getValue(toScene).zIndex, ) == scene @@ -374,28 +342,28 @@ private fun isElementOpaque( layoutImpl: SceneTransitionLayoutImpl, element: Element, scene: Scene, - sceneValues: Element.TargetValues, + sceneState: Element.SceneState, ): Boolean { val transition = layoutImpl.state.currentTransition ?: return true if (!layoutImpl.isTransitionReady(transition)) { val lastValue = - sceneValues.lastValues.alpha.takeIf { it != Element.AlphaUnspecified } - ?: element.lastSharedValues.alpha.takeIf { it != Element.AlphaUnspecified } ?: 1f + sceneState.lastState.alpha.takeIf { it != Element.AlphaUnspecified } + ?: element.lastSharedState.alpha.takeIf { it != Element.AlphaUnspecified } ?: 1f return lastValue == 1f } val fromScene = transition.fromScene val toScene = transition.toScene - val fromValues = element.sceneValues[fromScene] - val toValues = element.sceneValues[toScene] + val fromState = element.sceneStates[fromScene] + val toState = element.sceneStates[toScene] - if (fromValues == null && toValues == null) { + if (fromState == null && toState == null) { error("This should not happen, element $element is neither in $fromScene or $toScene") } - val isSharedElement = fromValues != null && toValues != null + val isSharedElement = fromState != null && toState != null if (isSharedElement && isSharedElementEnabled(layoutImpl.state, transition, element.key)) { return true } @@ -415,7 +383,7 @@ private fun elementAlpha( layoutImpl: SceneTransitionLayoutImpl, element: Element, scene: Scene, - sceneValues: Element.TargetValues, + sceneState: Element.SceneState, ): Float { return computeValue( layoutImpl, @@ -426,9 +394,8 @@ private fun elementAlpha( idleValue = 1f, currentValue = { 1f }, lastValue = { - sceneValues.lastValues.alpha.takeIf { it != Element.AlphaUnspecified } - ?: element.lastSharedValues.alpha.takeIf { it != Element.AlphaUnspecified } - ?: 1f + sceneState.lastState.alpha.takeIf { it != Element.AlphaUnspecified } + ?: element.lastSharedState.alpha.takeIf { it != Element.AlphaUnspecified } ?: 1f }, ::lerp, ) @@ -440,15 +407,15 @@ private fun IntermediateMeasureScope.measure( layoutImpl: SceneTransitionLayoutImpl, scene: Scene, element: Element, - sceneValues: Element.TargetValues, + sceneState: Element.SceneState, measurable: Measurable, constraints: Constraints, ): Placeable { // Update the size this element has in this scene when idle. val targetSizeInScene = lookaheadSize - if (targetSizeInScene != sceneValues.targetSize) { + if (targetSizeInScene != sceneState.targetSize) { // TODO(b/290930950): Better handle when this changes to avoid instant size jumps. - sceneValues.targetSize = targetSizeInScene + sceneState.targetSize = targetSizeInScene } // Some lambdas called (max once) by computeValue() will need to measure [measurable], in which @@ -468,8 +435,8 @@ private fun IntermediateMeasureScope.measure( idleValue = lookaheadSize, currentValue = { measurable.measure(constraints).also { maybePlaceable = it }.size() }, lastValue = { - sceneValues.lastValues.size.takeIf { it != Element.SizeUnspecified } - ?: element.lastSharedValues.size.takeIf { it != Element.SizeUnspecified } + sceneState.lastState.size.takeIf { it != Element.SizeUnspecified } + ?: element.lastSharedState.size.takeIf { it != Element.SizeUnspecified } ?: measurable.measure(constraints).also { maybePlaceable = it }.size() }, ::lerp, @@ -485,8 +452,8 @@ private fun IntermediateMeasureScope.measure( ) val size = placeable.size() - element.lastSharedValues.size = size - sceneValues.lastValues.size = size + element.lastSharedState.size = size + sceneState.lastState.size = size return placeable } @@ -494,7 +461,7 @@ private fun getDrawScale( layoutImpl: SceneTransitionLayoutImpl, element: Element, scene: Scene, - sceneValues: Element.TargetValues + sceneState: Element.SceneState ): Scale { return computeValue( layoutImpl, @@ -505,8 +472,8 @@ private fun getDrawScale( idleValue = Scale.Default, currentValue = { Scale.Default }, lastValue = { - sceneValues.lastValues.drawScale.takeIf { it != Scale.Default } - ?: element.lastSharedValues.drawScale + sceneState.lastState.drawScale.takeIf { it != Scale.Default } + ?: element.lastSharedState.drawScale }, ::lerp, ) @@ -517,7 +484,7 @@ private fun IntermediateMeasureScope.place( layoutImpl: SceneTransitionLayoutImpl, scene: Scene, element: Element, - sceneValues: Element.TargetValues, + sceneState: Element.SceneState, placeable: Placeable, placementScope: Placeable.PlacementScope, ) { @@ -526,14 +493,14 @@ private fun IntermediateMeasureScope.place( // when idle. val coords = coordinates ?: error("Element ${element.key} does not have any coordinates") val targetOffsetInScene = lookaheadScopeCoordinates.localLookaheadPositionOf(coords) - if (targetOffsetInScene != sceneValues.targetOffset) { + if (targetOffsetInScene != sceneState.targetOffset) { // TODO(b/290930950): Better handle when this changes to avoid instant offset jumps. - sceneValues.targetOffset = targetOffsetInScene + sceneState.targetOffset = targetOffsetInScene } val currentOffset = lookaheadScopeCoordinates.localPositionOf(coords, Offset.Zero) - val lastSharedValues = element.lastSharedValues - val lastValues = sceneValues.lastValues + val lastSharedState = element.lastSharedState + val lastSceneState = sceneState.lastState val targetOffset = computeValue( layoutImpl, @@ -544,36 +511,36 @@ private fun IntermediateMeasureScope.place( idleValue = targetOffsetInScene, currentValue = { currentOffset }, lastValue = { - lastValues.offset.takeIf { it.isSpecified } - ?: lastSharedValues.offset.takeIf { it.isSpecified } ?: currentOffset + lastSceneState.offset.takeIf { it.isSpecified } + ?: lastSharedState.offset.takeIf { it.isSpecified } ?: currentOffset }, ::lerp, ) - lastSharedValues.offset = targetOffset - lastValues.offset = targetOffset + lastSharedState.offset = targetOffset + lastSceneState.offset = targetOffset // No need to place the element in this scene if we don't want to draw it anyways. Note that - // it's still important to compute the target offset and update lastValues, otherwise it - // will be out of date. + // it's still important to compute the target offset and update last(Shared|Scene)State, + // otherwise they will be out of date. if (!shouldDrawElement(layoutImpl, scene, element)) { return } val offset = (targetOffset - currentOffset).round() - if (isElementOpaque(layoutImpl, element, scene, sceneValues)) { + if (isElementOpaque(layoutImpl, element, scene, sceneState)) { // TODO(b/291071158): Call placeWithLayer() if offset != IntOffset.Zero and size is not // animated once b/305195729 is fixed. Test that drawing is not invalidated in that // case. placeable.place(offset) - lastSharedValues.alpha = 1f - lastValues.alpha = 1f + lastSharedState.alpha = 1f + lastSceneState.alpha = 1f } else { placeable.placeWithLayer(offset) { - val alpha = elementAlpha(layoutImpl, element, scene, sceneValues) + val alpha = elementAlpha(layoutImpl, element, scene, sceneState) this.alpha = alpha - lastSharedValues.alpha = alpha - lastValues.alpha = alpha + lastSharedState.alpha = alpha + lastSceneState.alpha = alpha } } } @@ -605,7 +572,7 @@ private inline fun <T> computeValue( layoutImpl: SceneTransitionLayoutImpl, scene: Scene, element: Element, - sceneValue: (Element.TargetValues) -> T, + sceneValue: (Element.SceneState) -> T, transformation: (ElementTransformations) -> PropertyTransformation<T>?, idleValue: T, currentValue: () -> T, @@ -628,10 +595,10 @@ private inline fun <T> computeValue( val fromScene = transition.fromScene val toScene = transition.toScene - val fromValues = element.sceneValues[fromScene] - val toValues = element.sceneValues[toScene] + val fromState = element.sceneStates[fromScene] + val toState = element.sceneStates[toScene] - if (fromValues == null && toValues == null) { + if (fromState == null && toState == null) { // TODO(b/311600838): Throw an exception instead once layers of disposed elements are not // run anymore. return lastValue() @@ -640,10 +607,10 @@ private inline fun <T> computeValue( // The element is shared: interpolate between the value in fromScene and the value in toScene. // TODO(b/290184746): Support non linear shared paths as well as a way to make sure that shared // elements follow the finger direction. - val isSharedElement = fromValues != null && toValues != null + val isSharedElement = fromState != null && toState != null if (isSharedElement && isSharedElementEnabled(layoutImpl.state, transition, element.key)) { - val start = sceneValue(fromValues!!) - val end = sceneValue(toValues!!) + val start = sceneValue(fromState!!) + val end = sceneValue(toState!!) // Make sure we don't read progress if values are the same and we don't need to interpolate, // so we don't invalidate the phase where this is read. @@ -659,12 +626,12 @@ private inline fun <T> computeValue( // Get the transformed value, i.e. the target value at the beginning (for entering elements) or // end (for leaving elements) of the transition. - val sceneValues = + val sceneState = checkNotNull( when { - isSharedElement && scene.key == fromScene -> fromValues - isSharedElement -> toValues - else -> fromValues ?: toValues + isSharedElement && scene.key == fromScene -> fromState + isSharedElement -> toState + else -> fromState ?: toState } ) @@ -673,7 +640,7 @@ private inline fun <T> computeValue( layoutImpl, scene, element, - sceneValues, + sceneState, transition, idleValue, ) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt index 84d3b8647d6c..90f46bd4dcaa 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt @@ -64,10 +64,10 @@ class ElementKey( identity: Any = Object(), /** - * Whether this element is a background and usually drawn below other elements. This should be - * set to true to make sure that shared backgrounds are drawn below elements of other scenes. + * The [ElementScenePicker] to use when deciding in which scene we should draw shared Elements + * or compose MovableElements. */ - val isBackground: Boolean = false, + val scenePicker: ElementScenePicker = DefaultElementScenePicker, ) : Key(name, identity), ElementMatcher { @VisibleForTesting // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt index 49df2f6b6062..af3c0999c97b 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt @@ -16,27 +16,36 @@ package com.android.compose.animation.scene -import android.util.Log import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.BoxScope import androidx.compose.runtime.Composable -import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.movableContentOf import androidx.compose.runtime.remember -import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.graphics.Canvas -import androidx.compose.ui.graphics.drawscope.draw -import androidx.compose.ui.graphics.drawscope.drawIntoCanvas -import androidx.compose.ui.graphics.nativeCanvas -import androidx.compose.ui.layout.layout -import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.layout.Layout import androidx.compose.ui.unit.IntSize -private const val TAG = "MovableElement" +@Composable +internal fun Element( + layoutImpl: SceneTransitionLayoutImpl, + scene: Scene, + key: ElementKey, + modifier: Modifier, + content: @Composable ElementScope<ElementContentScope>.() -> Unit, +) { + Box(modifier.element(layoutImpl, scene, key)) { + val sceneScope = scene.scope + val boxScope = this + val elementScope = + remember(layoutImpl, key, scene, sceneScope, boxScope) { + ElementScopeImpl(layoutImpl, key, scene, sceneScope, boxScope) + } + + content(elementScope) + } +} @Composable internal fun MovableElement( @@ -44,72 +53,113 @@ internal fun MovableElement( scene: Scene, key: ElementKey, modifier: Modifier, - content: @Composable MovableElementScope.() -> Unit, + content: @Composable ElementScope<MovableElementContentScope>.() -> Unit, ) { Box(modifier.element(layoutImpl, scene, key)) { - // Get the Element from the map. It will always be the same and we don't want to recompose - // every time an element is added/removed from SceneTransitionLayoutImpl.elements, so we - // disable read observation during the look-up in that map. - val element = Snapshot.withoutReadObservation { layoutImpl.elements.getValue(key) } - val movableElementScope = - remember(layoutImpl, element, scene) { - MovableElementScopeImpl(layoutImpl, element, scene) + val sceneScope = scene.scope + val boxScope = this + val elementScope = + remember(layoutImpl, key, scene, sceneScope, boxScope) { + MovableElementScopeImpl(layoutImpl, key, scene, sceneScope, boxScope) } - // The [Picture] to which we save the last drawing commands of this element. This is - // necessary because the content of this element might not be composed in this scene, in - // which case we still need to draw it. - val picture = element.picture + content(elementScope) + } +} + +private abstract class BaseElementScope<ContentScope>( + private val layoutImpl: SceneTransitionLayoutImpl, + private val element: ElementKey, + private val scene: Scene, +) : ElementScope<ContentScope> { + @Composable + override fun <T> animateElementValueAsState( + value: T, + key: ValueKey, + lerp: (start: T, stop: T, fraction: Float) -> T, + canOverflow: Boolean + ): AnimatedState<T> { + return animateSharedValueAsState( + layoutImpl, + scene.key, + element, + key, + value, + lerp, + canOverflow, + ) + } +} + +private class ElementScopeImpl( + layoutImpl: SceneTransitionLayoutImpl, + element: ElementKey, + scene: Scene, + private val sceneScope: SceneScope, + private val boxScope: BoxScope, +) : BaseElementScope<ElementContentScope>(layoutImpl, element, scene) { + private val contentScope = + object : ElementContentScope, SceneScope by sceneScope, BoxScope by boxScope {} + @Composable + override fun content(content: @Composable ElementContentScope.() -> Unit) { + contentScope.content() + } +} + +private class MovableElementScopeImpl( + private val layoutImpl: SceneTransitionLayoutImpl, + private val element: ElementKey, + private val scene: Scene, + private val sceneScope: BaseSceneScope, + private val boxScope: BoxScope, +) : BaseElementScope<MovableElementContentScope>(layoutImpl, element, scene) { + private val contentScope = + object : MovableElementContentScope, BaseSceneScope by sceneScope, BoxScope by boxScope {} + + @Composable + override fun content(content: @Composable MovableElementContentScope.() -> Unit) { // Whether we should compose the movable element here. The scene picker logic to know in // which scene we should compose/draw a movable element might depend on the current // transition progress, so we put this in a derivedStateOf to prevent many recompositions // during the transition. + // TODO(b/317026105): Use derivedStateOf only if the scene picker reads the progress in its + // logic. val shouldComposeMovableElement by remember(layoutImpl, scene.key, element) { derivedStateOf { shouldComposeMovableElement(layoutImpl, scene.key, element) } } if (shouldComposeMovableElement) { - Box( - Modifier.drawWithCache { - val width = size.width.toInt() - val height = size.height.toInt() - - onDrawWithContent { - // Save the draw commands into [picture] for later to draw the last content - // even when this movable content is not composed. - val pictureCanvas = Canvas(picture.beginRecording(width, height)) - draw(this, this.layoutDirection, pictureCanvas, this.size) { - this@onDrawWithContent.drawContent() + val movableContent: MovableElementContent = + layoutImpl.movableContents[element] + ?: movableContentOf { + contentScope: MovableElementContentScope, + content: @Composable MovableElementContentScope.() -> Unit -> + contentScope.content() } - picture.endRecording() - - // Draw the content. - drawIntoCanvas { canvas -> canvas.nativeCanvas.drawPicture(picture) } - } - } - ) { - element.movableContent { movableElementScope.content() } - } + .also { layoutImpl.movableContents[element] = it } + + // Important: Don't introduce any parent Box or other layout here, because contentScope + // delegates its BoxScope implementation to the Box where this content() function is + // called, so it's important that this movableContent is composed directly under that + // Box. + movableContent(contentScope, content) } else { - // If we are not composed, we draw the previous drawing commands at the same size as the - // movable content when it was composed in this scene. - val sceneValues = element.sceneValues.getValue(scene.key) - - Spacer( - Modifier.layout { measurable, _ -> - val size = - sceneValues.targetSize.takeIf { it != Element.SizeUnspecified } - ?: IntSize.Zero - val placeable = - measurable.measure(Constraints.fixed(size.width, size.height)) - layout(size.width, size.height) { placeable.place(0, 0) } - } - .drawBehind { - drawIntoCanvas { canvas -> canvas.nativeCanvas.drawPicture(picture) } - } - ) + // If we are not composed, we still need to lay out an empty space with the same *target + // size* as its movable content, i.e. the same *size when idle*. During transitions, + // this size will be used to interpolate the transition size, during the intermediate + // layout pass. + Layout { _, _ -> + // No need to measure or place anything. + val size = + placeholderContentSize( + layoutImpl, + scene.key, + layoutImpl.elements.getValue(element), + ) + layout(size.width, size.height) {} + } } } } @@ -117,7 +167,7 @@ internal fun MovableElement( private fun shouldComposeMovableElement( layoutImpl: SceneTransitionLayoutImpl, scene: SceneKey, - element: Element, + element: ElementKey, ): Boolean { val transition = layoutImpl.state.currentTransition @@ -130,72 +180,55 @@ private fun shouldComposeMovableElement( val fromReady = layoutImpl.isSceneReady(fromScene) val toReady = layoutImpl.isSceneReady(toScene) - val otherScene = - when (scene) { - fromScene -> toScene - toScene -> fromScene - else -> - error( - "shouldComposeMovableElement(scene=$scene) called with fromScene=$fromScene " + - "and toScene=$toScene" - ) - } - - val isShared = otherScene in element.sceneValues - - if (isShared && !toReady && !fromReady) { - // This should usually not happen given that fromScene should be ready, but let's log a - // warning here in case it does so it helps debugging flicker issues caused by this part of - // the code. - Log.w( - TAG, - "MovableElement $element might have to be composed for the first time in both " + - "fromScene=$fromScene and toScene=$toScene. This will probably lead to a flicker " + - "where the size of the element will jump from IntSize.Zero to its actual size " + - "during the transition." - ) - } - - // Element is not shared in this transition. - if (!isShared) { - return true - } - - // toScene is not ready (because we are composing it for the first time), so we compose it there - // first. This is the most common scenario when starting a transition that has a shared movable - // element. - if (!toReady) { + if (!fromReady && !toReady) { + // Neither of the scenes will be drawn, so where we compose it doesn't really matter. Note + // that we could have slightly more complicated logic here to optimize for this case, but + // it's not worth it given that readyScenes should disappear soon (b/316901148). return scene == toScene } - // This should usually not happen, but if we are also composing for the first time in fromScene - // then we should compose it there only. - if (!fromReady) { - return scene == fromScene - } + // If one of the scenes is not ready, compose it in the other one to make sure it is drawn. + if (!fromReady) return scene == toScene + if (!toReady) return scene == fromScene + // Always compose movable elements in the scene picked by their scene picker. return shouldDrawOrComposeSharedElement( layoutImpl, transition, scene, - element.key, - sharedElementTransformation(layoutImpl.state, transition, element.key), + element, ) } -private class MovableElementScopeImpl( - private val layoutImpl: SceneTransitionLayoutImpl, - private val element: Element, - private val scene: Scene, -) : MovableElementScope { - @Composable - override fun <T> animateSharedValueAsState( - value: T, - debugName: String, - lerp: (start: T, stop: T, fraction: Float) -> T, - canOverflow: Boolean, - ): State<T> { - val key = remember { ValueKey(debugName) } - return animateSharedValueAsState(layoutImpl, scene, element, key, value, lerp, canOverflow) +/** + * Return the size of the placeholder/space that is composed when the movable content is not + * composed in a scene. + */ +private fun placeholderContentSize( + layoutImpl: SceneTransitionLayoutImpl, + scene: SceneKey, + element: Element, +): IntSize { + // If the content of the movable element was already composed in this scene before, use that + // target size. + val targetValueInScene = element.sceneStates.getValue(scene).targetSize + if (targetValueInScene != Element.SizeUnspecified) { + return targetValueInScene } + + // This code is only run during transitions (otherwise the content would be composed and the + // placeholder would not), so it's ok to cast the state into a Transition directly. + val transition = layoutImpl.state.transitionState as TransitionState.Transition + + // If the content was already composed in the other scene, we use that target size assuming it + // doesn't change between scenes. + // TODO(b/317026105): Provide a way to give a hint size/content for cases where this is not + // true. + val otherScene = if (transition.fromScene == scene) transition.toScene else transition.fromScene + val targetValueInOtherScene = element.sceneStates[otherScene]?.targetSize + if (targetValueInOtherScene != null && targetValueInOtherScene != Element.SizeUnspecified) { + return targetValueInOtherScene + } + + return IntSize.Zero } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt index 560e92becba5..454c0ecf8ac5 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt @@ -75,8 +75,8 @@ private class PunchHoleNode( if ( bounds == null || - bounds.lastSharedValues.size == Element.SizeUnspecified || - bounds.lastSharedValues.offset == Offset.Unspecified + bounds.lastSharedState.size == Element.SizeUnspecified || + bounds.lastSharedState.offset == Offset.Unspecified ) { drawContent() return @@ -87,14 +87,14 @@ private class PunchHoleNode( canvas.withSaveLayer(size.toRect(), Paint()) { drawContent() - val offset = bounds.lastSharedValues.offset - element.lastSharedValues.offset + val offset = bounds.lastSharedState.offset - element.lastSharedState.offset translate(offset.x, offset.y) { drawHole(bounds) } } } } private fun DrawScope.drawHole(bounds: Element) { - val boundsSize = bounds.lastSharedValues.size.toSize() + val boundsSize = bounds.lastSharedState.size.toSize() if (shape == RectangleShape) { drawRect(Color.Black, size = boundsSize, blendMode = BlendMode.DstOut) return diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt index 30e50a972230..3537b7989ed5 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt @@ -20,13 +20,10 @@ import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable -import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.Snapshot -import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape @@ -45,16 +42,13 @@ internal class Scene( actions: Map<UserAction, SceneKey>, zIndex: Float, ) { - private val scope = SceneScopeImpl(layoutImpl, this) + internal val scope = SceneScopeImpl(layoutImpl, this) var content by mutableStateOf(content) var userActions by mutableStateOf(actions) var zIndex by mutableFloatStateOf(zIndex) var targetSize by mutableStateOf(IntSize.Zero) - /** The shared values in this scene that are not tied to a specific element. */ - val sharedValues = SnapshotStateMap<ValueKey, Element.SharedValue<*>>() - @Composable @OptIn(ExperimentalComposeUiApi::class) fun Content(modifier: Modifier = Modifier) { @@ -77,7 +71,7 @@ internal class Scene( } } -private class SceneScopeImpl( +internal class SceneScopeImpl( private val layoutImpl: SceneTransitionLayoutImpl, private val scene: Scene, ) : SceneScope { @@ -87,6 +81,42 @@ private class SceneScopeImpl( return element(layoutImpl, scene, key) } + @Composable + override fun Element( + key: ElementKey, + modifier: Modifier, + content: @Composable (ElementScope<ElementContentScope>.() -> Unit) + ) { + Element(layoutImpl, scene, key, modifier, content) + } + + @Composable + override fun MovableElement( + key: ElementKey, + modifier: Modifier, + content: @Composable (ElementScope<MovableElementContentScope>.() -> Unit) + ) { + MovableElement(layoutImpl, scene, key, modifier, content) + } + + @Composable + override fun <T> animateSceneValueAsState( + value: T, + key: ValueKey, + lerp: (T, T, Float) -> T, + canOverflow: Boolean + ): AnimatedState<T> { + return animateSharedValueAsState( + layoutImpl = layoutImpl, + scene = scene.key, + element = null, + key = key, + value = value, + lerp = lerp, + canOverflow = canOverflow, + ) + } + override fun Modifier.horizontalNestedScrollToScene( leftBehavior: NestedScrollBehavior, rightBehavior: NestedScrollBehavior, @@ -109,45 +139,6 @@ private class SceneScopeImpl( bottomOrRightBehavior = bottomBehavior, ) - @Composable - override fun <T> animateSharedValueAsState( - value: T, - key: ValueKey, - element: ElementKey?, - lerp: (T, T, Float) -> T, - canOverflow: Boolean - ): State<T> { - val element = - element?.let { key -> - Snapshot.withoutReadObservation { - layoutImpl.elements[key] - ?: error( - "Element $key is not composed. Make sure to call " + - "animateSharedXAsState *after* Modifier.element(key)." - ) - } - } - - return animateSharedValueAsState( - layoutImpl, - scene, - element, - key, - value, - lerp, - canOverflow, - ) - } - - @Composable - override fun MovableElement( - key: ElementKey, - modifier: Modifier, - content: @Composable MovableElementScope.() -> Unit, - ) { - MovableElement(layoutImpl, scene, key, modifier, content) - } - override fun Modifier.punchHole( element: ElementKey, bounds: ElementKey, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index 5eb339e4a5e4..84fade8937ff 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -22,9 +22,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.Stable -import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape import androidx.compose.ui.input.nestedscroll.NestedScrollConnection @@ -98,9 +98,9 @@ interface SceneTransitionLayoutScope { */ @DslMarker annotation class ElementDsl -@ElementDsl @Stable -interface SceneScope { +@ElementDsl +interface BaseSceneScope { /** The state of the [SceneTransitionLayout] in which this scene is contained. */ val layoutState: SceneTransitionLayoutState @@ -111,21 +111,74 @@ interface SceneScope { * that the element can be transformed and animated when the scene transitions in or out. * * Additionally, this [key] will be used to detect elements that are shared between scenes to - * automatically interpolate their size, offset and [shared values][animateSharedValueAsState]. + * automatically interpolate their size and offset. If you need to animate shared element values + * (i.e. values associated to this element that change depending on which scene it is composed + * in), use [Element] instead. * * Note that shared elements tagged using this function will be duplicated in each scene they * are part of, so any **internal** state (e.g. state created using `remember { * mutableStateOf(...) }`) will be lost. If you need to preserve internal state, you should use * [MovableElement] instead. * + * @see Element * @see MovableElement - * - * TODO(b/291566282): Migrate this to the new Modifier Node API and remove the @Composable - * constraint. */ fun Modifier.element(key: ElementKey): Modifier /** + * Create an element identified by [key]. + * + * Similar to [element], this creates an element that will be automatically shared when present + * in multiple scenes and that can be transformed during transitions, the same way that + * [element] does. + * + * The only difference with [element] is that the provided [ElementScope] allows you to + * [animate element values][ElementScope.animateElementValueAsState] or specify its + * [movable content][Element.movableContent] that will be "moved" and composed only once during + * transitions (as opposed to [element] that duplicates shared elements) so that any internal + * state is preserved during and after the transition. + * + * @see element + * @see MovableElement + */ + @Composable + fun Element( + key: ElementKey, + modifier: Modifier, + + // TODO(b/317026105): As discussed in http://shortn/_gJVdltF8Si, remove the @Composable + // scope here to make sure that callers specify the content in ElementScope.content {} or + // ElementScope.movableContent {}. + content: @Composable ElementScope<ElementContentScope>.() -> Unit, + ) + + /** + * Create a *movable* element identified by [key]. + * + * Similar to [Element], this creates an element that will be automatically shared when present + * in multiple scenes and that can be transformed during transitions, and you can also use the + * provided [ElementScope] to [animate element values][ElementScope.animateElementValueAsState]. + * + * The important difference with [element] and [Element] is that this element + * [content][ElementScope.content] will be "moved" and composed only once during transitions, as + * opposed to [element] and [Element] that duplicates shared elements, so that any internal + * state is preserved during and after the transition. + * + * @see element + * @see Element + */ + @Composable + fun MovableElement( + key: ElementKey, + modifier: Modifier, + + // TODO(b/317026105): As discussed in http://shortn/_gJVdltF8Si, remove the @Composable + // scope here to make sure that callers specify the content in ElementScope.content {} or + // ElementScope.movableContent {}. + content: @Composable ElementScope<MovableElementContentScope>.() -> Unit, + ) + + /** * Adds a [NestedScrollConnection] to intercept scroll events not handled by the scrollable * component. * @@ -150,82 +203,114 @@ interface SceneScope { ): Modifier /** - * Create a *movable* element identified by [key]. - * - * This creates an element that will be automatically shared when present in multiple scenes and - * that can be transformed during transitions, the same way that [element] does. The major - * difference with [element] is that elements created with [MovableElement] will be "moved" and - * composed only once during transitions (as opposed to [element] that duplicates shared - * elements) so that any internal state is preserved during and after the transition. + * Punch a hole in this [element] using the bounds of [bounds] in [scene] and the given [shape]. * - * @see element + * Punching a hole in an element will "remove" any pixel drawn by that element in the hole area. + * This can be used to make content drawn below an opaque element visible. For example, if we + * have [this lockscreen scene](http://shortn/_VYySFnJDhN) drawn below + * [this shade scene](http://shortn/_fpxGUk0Rg7) and punch a hole in the latter using the big + * clock time bounds and a RoundedCornerShape(10dp), [this](http://shortn/_qt80IvORFj) would be + * the result. */ - @Composable - fun MovableElement( - key: ElementKey, - modifier: Modifier, - content: @Composable MovableElementScope.() -> Unit, - ) + fun Modifier.punchHole(element: ElementKey, bounds: ElementKey, shape: Shape): Modifier + + /** + * Don't resize during transitions. This can for instance be used to make sure that scrollable + * lists keep a constant size during transitions even if its elements are growing/shrinking. + */ + fun Modifier.noResizeDuringTransitions(): Modifier +} +@Stable +@ElementDsl +interface SceneScope : BaseSceneScope { /** - * Animate some value of a shared element. + * Animate some value at the scene level. * * @param value the value of this shared value in the current scene. * @param key the key of this shared value. - * @param element the element associated with this value. If `null`, this value will be - * associated at the scene level, which means that [key] should be used maximum once in the - * same scene. * @param lerp the *linear* interpolation function that should be used to interpolate between * two different values. Note that it has to be linear because the [fraction] passed to this * interpolator is already interpolated. * @param canOverflow whether this value can overflow past the values it is interpolated * between, for instance because the transition is animated using a bouncy spring. - * @see animateSharedIntAsState - * @see animateSharedFloatAsState - * @see animateSharedDpAsState - * @see animateSharedColorAsState + * @see animateSceneIntAsState + * @see animateSceneFloatAsState + * @see animateSceneDpAsState + * @see animateSceneColorAsState */ @Composable - fun <T> animateSharedValueAsState( + fun <T> animateSceneValueAsState( value: T, key: ValueKey, - element: ElementKey?, lerp: (start: T, stop: T, fraction: Float) -> T, canOverflow: Boolean, - ): State<T> + ): AnimatedState<T> +} +@Stable +@ElementDsl +interface ElementScope<ContentScope> { /** - * Punch a hole in this [element] using the bounds of [bounds] in [scene] and the given [shape]. + * Animate some value associated to this element. * - * Punching a hole in an element will "remove" any pixel drawn by that element in the hole area. - * This can be used to make content drawn below an opaque element visible. For example, if we - * have [this lockscreen scene](http://shortn/_VYySFnJDhN) drawn below - * [this shade scene](http://shortn/_fpxGUk0Rg7) and punch a hole in the latter using the big - * clock time bounds and a RoundedCornerShape(10dp), [this](http://shortn/_qt80IvORFj) would be - * the result. + * @param value the value of this shared value in the current scene. + * @param key the key of this shared value. + * @param lerp the *linear* interpolation function that should be used to interpolate between + * two different values. Note that it has to be linear because the [fraction] passed to this + * interpolator is already interpolated. + * @param canOverflow whether this value can overflow past the values it is interpolated + * between, for instance because the transition is animated using a bouncy spring. + * @see animateElementIntAsState + * @see animateElementFloatAsState + * @see animateElementDpAsState + * @see animateElementColorAsState */ - fun Modifier.punchHole(element: ElementKey, bounds: ElementKey, shape: Shape): Modifier + @Composable + fun <T> animateElementValueAsState( + value: T, + key: ValueKey, + lerp: (start: T, stop: T, fraction: Float) -> T, + canOverflow: Boolean, + ): AnimatedState<T> /** - * Don't resize during transitions. This can for instance be used to make sure that scrollable - * lists keep a constant size during transitions even if its elements are growing/shrinking. + * The content of this element. + * + * Important: This must be called exactly once, after all calls to [animateElementValueAsState]. */ - fun Modifier.noResizeDuringTransitions(): Modifier + @Composable fun content(content: @Composable ContentScope.() -> Unit) } -// TODO(b/291053742): Add animateSharedValueAsState(targetValue) without any ValueKey and ElementKey -// arguments to allow sharing values inside a movable element. +/** + * The exact same scope as [androidx.compose.foundation.layout.BoxScope]. + * + * We can't reuse BoxScope directly because of the @LayoutScopeMarker annotation on it, which would + * prevent us from calling Modifier.element() and other methods of [SceneScope] inside any Box {} in + * the [content][ElementScope.content] of a [SceneScope.Element] or a [SceneScope.MovableElement]. + */ +@Stable @ElementDsl -interface MovableElementScope { - @Composable - fun <T> animateSharedValueAsState( - value: T, - debugName: String, - lerp: (start: T, stop: T, fraction: Float) -> T, - canOverflow: Boolean, - ): State<T> +interface ElementBoxScope { + /** @see [androidx.compose.foundation.layout.BoxScope.align]. */ + @Stable fun Modifier.align(alignment: Alignment): Modifier + + /** @see [androidx.compose.foundation.layout.BoxScope.matchParentSize]. */ + @Stable fun Modifier.matchParentSize(): Modifier } +/** The scope for "normal" (not movable) elements. */ +@Stable @ElementDsl interface ElementContentScope : SceneScope, ElementBoxScope + +/** + * The scope for the content of movable elements. + * + * Note that it extends [BaseSceneScope] and not [SceneScope] because movable elements should not + * call [SceneScope.animateSceneValueAsState], given that their content is not composed in all + * scenes. + */ +@Stable @ElementDsl interface MovableElementContentScope : BaseSceneScope, ElementBoxScope + /** An action performed by the user. */ sealed interface UserAction diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index 45e1a0fa8f77..0227aba94b53 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -36,6 +36,16 @@ import androidx.compose.ui.util.fastForEach import com.android.compose.ui.util.lerp import kotlinx.coroutines.CoroutineScope +/** + * The type for the content of movable elements. + * + * TODO(b/317972419): Revert back to make this movable content have a single @Composable lambda + * parameter. + */ +internal typealias MovableElementContent = + @Composable + (MovableElementContentScope, @Composable MovableElementContentScope.() -> Unit) -> Unit + @Stable internal class SceneTransitionLayoutImpl( internal val state: SceneTransitionLayoutStateImpl, @@ -56,16 +66,47 @@ internal class SceneTransitionLayoutImpl( /** * The map of [Element]s. * - * Note that this map is *mutated* directly during composition, so it is a [SnapshotStateMap] to - * make sure that mutations are reverted if composition is cancelled. + * Important: [Element]s from this map should never be accessed during composition because the + * Elements are added when the associated Modifier.element() node is attached to the Modifier + * tree, i.e. after composition. */ - internal val elements = SnapshotStateMap<ElementKey, Element>() + internal val elements = mutableMapOf<ElementKey, Element>() + + /** + * The map of contents of movable elements. + * + * Note that given that this map is mutated directly during a composition, it has to be a + * [SnapshotStateMap] to make sure that mutations are reverted if composition is cancelled. + */ + private var _movableContents: SnapshotStateMap<ElementKey, MovableElementContent>? = null + val movableContents: SnapshotStateMap<ElementKey, MovableElementContent> + get() = + _movableContents + ?: SnapshotStateMap<ElementKey, MovableElementContent>().also { + _movableContents = it + } + + /** + * The different values of a shared value keyed by a a [ValueKey] and the different elements and + * scenes it is associated to. + */ + private var _sharedValues: + MutableMap<ValueKey, MutableMap<ElementKey?, SnapshotStateMap<SceneKey, *>>>? = + null + internal val sharedValues: + MutableMap<ValueKey, MutableMap<ElementKey?, SnapshotStateMap<SceneKey, *>>> + get() = + _sharedValues + ?: mutableMapOf<ValueKey, MutableMap<ElementKey?, SnapshotStateMap<SceneKey, *>>>() + .also { _sharedValues = it } /** * The scenes that are "ready", i.e. they were composed and fully laid-out at least once. * * Note that this map is *read* during composition, so it is a [SnapshotStateMap] to make sure * that we recompose when modifications are made to this map. + * + * TODO(b/316901148): Remove this map. */ private val readyScenes = SnapshotStateMap<SceneKey, Boolean>() diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt index d1ba582d6c23..0607aa148157 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt @@ -92,6 +92,20 @@ sealed interface TransitionState { /** Whether user input is currently driving the transition. */ abstract val isUserInputOngoing: Boolean + + /** + * Whether we are transitioning. If [from] or [to] is empty, we will also check that they + * match the scenes we are animating from and/or to. + */ + fun isTransitioning(from: SceneKey? = null, to: SceneKey? = null): Boolean { + return (from == null || fromScene == from) && (to == null || toScene == to) + } + + /** Whether we are transitioning from [scene] to [other], or from [other] to [scene]. */ + fun isTransitioningBetween(scene: SceneKey, other: SceneKey): Boolean { + return isTransitioning(from = scene, to = other) || + isTransitioning(from = other, to = scene) + } } } @@ -111,13 +125,12 @@ internal class SceneTransitionLayoutStateImpl( override fun isTransitioning(from: SceneKey?, to: SceneKey?): Boolean { val transition = currentTransition ?: return false - return (from == null || transition.fromScene == from) && - (to == null || transition.toScene == to) + return transition.isTransitioning(from, to) } override fun isTransitioningBetween(scene: SceneKey, other: SceneKey): Boolean { - return isTransitioning(from = scene, to = other) || - isTransitioning(from = other, to = scene) + val transition = currentTransition ?: return false + return transition.isTransitioningBetween(scene, other) } /** Start a new [transition], instantly interrupting any ongoing transition if there was one. */ diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt index dfa2a9a18e91..dc8505c43889 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt @@ -119,14 +119,8 @@ interface TransitionBuilder : PropertyTransformationBuilder { * * @param enabled whether the matched element(s) should actually be shared in this transition. * Defaults to true. - * @param scenePicker the [SharedElementScenePicker] to use when deciding in which scene we - * should draw or compose this shared element. */ - fun sharedElement( - matcher: ElementMatcher, - enabled: Boolean = true, - scenePicker: SharedElementScenePicker = DefaultSharedElementScenePicker, - ) + fun sharedElement(matcher: ElementMatcher, enabled: Boolean = true) /** * Adds the transformations in [builder] but in reversed order. This allows you to partially @@ -136,44 +130,132 @@ interface TransitionBuilder : PropertyTransformationBuilder { fun reversed(builder: TransitionBuilder.() -> Unit) } -interface SharedElementScenePicker { +/** + * An interface to decide where we should draw shared Elements or compose MovableElements. + * + * @see DefaultElementScenePicker + * @see HighestZIndexScenePicker + * @see LowestZIndexScenePicker + * @see MovableElementScenePicker + */ +interface ElementScenePicker { /** * Return the scene in which [element] should be drawn (when using `Modifier.element(key)`) or - * composed (when using `MovableElement(key)`) during the transition from [fromScene] to - * [toScene]. + * composed (when using `MovableElement(key)`) during the given [transition]. + * + * Important: For [MovableElements][SceneScope.MovableElement], this scene picker will *always* + * be used during transitions to decide whether we should compose that element in a given scene + * or not. Therefore, you should make sure that the returned [SceneKey] contains the movable + * element, otherwise that element will not be composed in any scene during the transition. */ fun sceneDuringTransition( element: ElementKey, - fromScene: SceneKey, - toScene: SceneKey, - progress: () -> Float, + transition: TransitionState.Transition, fromSceneZIndex: Float, toSceneZIndex: Float, ): SceneKey + + /** + * Return [transition.fromScene] if it is in [scenes] and [transition.toScene] is not, or return + * [transition.toScene] if it is in [scenes] and [transition.fromScene] is not, otherwise throw + * an exception (i.e. if neither or both of fromScene and toScene are in [scenes]). + * + * This function can be useful when computing the scene in which a movable element should be + * composed. + */ + fun pickSingleSceneIn( + scenes: Set<SceneKey>, + transition: TransitionState.Transition, + element: ElementKey, + ): SceneKey { + val fromScene = transition.fromScene + val toScene = transition.toScene + val fromSceneInScenes = scenes.contains(fromScene) + val toSceneInScenes = scenes.contains(toScene) + if (fromSceneInScenes && toSceneInScenes) { + error( + "Element $element can be in both $fromScene and $toScene. You should add a " + + "special case for this transition before calling pickSingleSceneIn()." + ) + } + + if (!fromSceneInScenes && !toSceneInScenes) { + error( + "Element $element can be neither in $fromScene and $toScene. This either means " + + "that you should add one of them in the scenes set passed to " + + "pickSingleSceneIn(), or there is an internal error and this element was " + + "composed when it shouldn't be." + ) + } + + return if (fromSceneInScenes) { + fromScene + } else { + toScene + } + } } -object DefaultSharedElementScenePicker : SharedElementScenePicker { +/** An [ElementScenePicker] that draws/composes elements in the scene with the highest z-order. */ +object HighestZIndexScenePicker : ElementScenePicker { override fun sceneDuringTransition( element: ElementKey, - fromScene: SceneKey, - toScene: SceneKey, - progress: () -> Float, + transition: TransitionState.Transition, fromSceneZIndex: Float, toSceneZIndex: Float ): SceneKey { - // By default shared elements are drawn in the highest scene possible, unless it is a - // background. - return if ( - (fromSceneZIndex > toSceneZIndex && !element.isBackground) || - (fromSceneZIndex < toSceneZIndex && element.isBackground) - ) { - fromScene + return if (fromSceneZIndex > toSceneZIndex) { + transition.fromScene } else { - toScene + transition.toScene } } } +/** An [ElementScenePicker] that draws/composes elements in the scene with the lowest z-order. */ +object LowestZIndexScenePicker : ElementScenePicker { + override fun sceneDuringTransition( + element: ElementKey, + transition: TransitionState.Transition, + fromSceneZIndex: Float, + toSceneZIndex: Float + ): SceneKey { + return if (fromSceneZIndex < toSceneZIndex) { + transition.fromScene + } else { + transition.toScene + } + } +} + +/** + * An [ElementScenePicker] that draws/composes elements in the scene we are transitioning to, iff + * that scene is in [scenes]. + * + * This picker can be useful for movable elements whose content size depends on its content (because + * it wraps it) in at least one scene. That way, the target size of the MovableElement will be + * computed in the scene we are going to and, given that this element was probably already composed + * in the scene we are going from before starting the transition, the interpolated size of the + * movable element during the transition should be correct. + * + * The downside of this picker is that the zIndex of the element when going from scene A to scene B + * is not the same as when going from scene B to scene A, so it's not usable in situations where + * z-ordering during the transition matters. + */ +class MovableElementScenePicker(private val scenes: Set<SceneKey>) : ElementScenePicker { + override fun sceneDuringTransition( + element: ElementKey, + transition: TransitionState.Transition, + fromSceneZIndex: Float, + toSceneZIndex: Float, + ): SceneKey { + return if (scenes.contains(transition.toScene)) transition.toScene else transition.fromScene + } +} + +/** The default [ElementScenePicker]. */ +val DefaultElementScenePicker = HighestZIndexScenePicker + @TransitionDsl interface PropertyTransformationBuilder { /** diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt index 70468669297c..b96f9bebb08b 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt @@ -108,12 +108,8 @@ internal class TransitionBuilderImpl : TransitionBuilder { range = null } - override fun sharedElement( - matcher: ElementMatcher, - enabled: Boolean, - scenePicker: SharedElementScenePicker, - ) { - transformations.add(SharedElementTransformation(matcher, enabled, scenePicker)) + override fun sharedElement(matcher: ElementMatcher, enabled: Boolean) { + transformations.add(SharedElementTransformation(matcher, enabled)) } override fun timestampRange( diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt index 40c814e0f25c..124ec290f42a 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt @@ -36,12 +36,12 @@ internal class AnchoredSize( layoutImpl: SceneTransitionLayoutImpl, scene: Scene, element: Element, - sceneValues: Element.TargetValues, + sceneState: Element.SceneState, transition: TransitionState.Transition, value: IntSize, ): IntSize { fun anchorSizeIn(scene: SceneKey): IntSize { - val size = layoutImpl.elements[anchor]?.sceneValues?.get(scene)?.targetSize + val size = layoutImpl.elements[anchor]?.sceneStates?.get(scene)?.targetSize return if (size != null && size != Element.SizeUnspecified) { IntSize( width = if (anchorWidth) size.width else value.width, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt index a1d63193bc73..7aa702b0bbd2 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt @@ -35,13 +35,13 @@ internal class AnchoredTranslate( layoutImpl: SceneTransitionLayoutImpl, scene: Scene, element: Element, - sceneValues: Element.TargetValues, + sceneState: Element.SceneState, transition: TransitionState.Transition, value: Offset, ): Offset { val anchor = layoutImpl.elements[anchor] ?: return value fun anchorOffsetIn(scene: SceneKey): Offset? { - return anchor.sceneValues[scene]?.targetOffset?.takeIf { it.isSpecified } + return anchor.sceneStates[scene]?.targetOffset?.takeIf { it.isSpecified } } // [element] will move the same amount as [anchor] does. diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt index d1cf8ee6ad4a..6704a3bbeff2 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt @@ -39,7 +39,7 @@ internal class DrawScale( layoutImpl: SceneTransitionLayoutImpl, scene: Scene, element: Element, - sceneValues: Element.TargetValues, + sceneState: Element.SceneState, transition: TransitionState.Transition, value: Scale, ): Scale { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt index 70534dde4f6f..191a8fbcd009 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt @@ -34,12 +34,12 @@ internal class EdgeTranslate( layoutImpl: SceneTransitionLayoutImpl, scene: Scene, element: Element, - sceneValues: Element.TargetValues, + sceneState: Element.SceneState, transition: TransitionState.Transition, value: Offset ): Offset { val sceneSize = scene.targetSize - val elementSize = sceneValues.targetSize + val elementSize = sceneState.targetSize if (elementSize == Element.SizeUnspecified) { return value } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt index 17032dc288e0..41f626e24e79 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt @@ -30,7 +30,7 @@ internal class Fade( layoutImpl: SceneTransitionLayoutImpl, scene: Scene, element: Element, - sceneValues: Element.TargetValues, + sceneState: Element.SceneState, transition: TransitionState.Transition, value: Float ): Float { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt index 233ae597090b..f5207dc4d345 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt @@ -37,7 +37,7 @@ internal class ScaleSize( layoutImpl: SceneTransitionLayoutImpl, scene: Scene, element: Element, - sceneValues: Element.TargetValues, + sceneState: Element.SceneState, transition: TransitionState.Transition, value: IntSize, ): IntSize { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt index 0cd11b9914c9..04254fbb588b 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt @@ -20,7 +20,6 @@ import com.android.compose.animation.scene.Element import com.android.compose.animation.scene.ElementMatcher import com.android.compose.animation.scene.Scene import com.android.compose.animation.scene.SceneTransitionLayoutImpl -import com.android.compose.animation.scene.SharedElementScenePicker import com.android.compose.animation.scene.TransitionState /** A transformation applied to one or more elements during a transition. */ @@ -48,7 +47,6 @@ sealed interface Transformation { internal class SharedElementTransformation( override val matcher: ElementMatcher, internal val enabled: Boolean, - internal val scenePicker: SharedElementScenePicker, ) : Transformation /** A transformation that changes the value of an element property, like its size or offset. */ @@ -62,7 +60,7 @@ internal sealed interface PropertyTransformation<T> : Transformation { layoutImpl: SceneTransitionLayoutImpl, scene: Scene, element: Element, - sceneValues: Element.TargetValues, + sceneState: Element.SceneState, transition: TransitionState.Transition, value: T, ): T diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt index 864b937a3fe0..04d5914bff69 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt @@ -35,7 +35,7 @@ internal class Translate( layoutImpl: SceneTransitionLayoutImpl, scene: Scene, element: Element, - sceneValues: Element.TargetValues, + sceneState: Element.SceneState, transition: TransitionState.Transition, value: Offset, ): Offset { diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt index 5473186c14ec..a116501a298c 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt @@ -18,10 +18,11 @@ package com.android.compose.animation.scene import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.lerp @@ -32,6 +33,7 @@ import androidx.compose.ui.unit.lerp import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.compose.ui.util.lerp import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -62,17 +64,17 @@ class AnimatedSharedAsStateTest { onCurrentValueChanged: (Values) -> Unit, ) { val key = TestElements.Foo - Box(Modifier.element(key)) { - val int by animateSharedIntAsState(targetValues.int, TestValues.Value1, key) - val float by animateSharedFloatAsState(targetValues.float, TestValues.Value2, key) - val dp by animateSharedDpAsState(targetValues.dp, TestValues.Value3, key) - val color by - animateSharedColorAsState(targetValues.color, TestValues.Value4, element = null) + Element(key, Modifier) { + val int by animateElementIntAsState(targetValues.int, key = TestValues.Value1) + val float by animateElementFloatAsState(targetValues.float, key = TestValues.Value2) + val dp by animateElementDpAsState(targetValues.dp, key = TestValues.Value3) + val color by animateElementColorAsState(targetValues.color, key = TestValues.Value4) - // Make sure we read the values during composition, so that we recompose and call - // onCurrentValueChanged() with the latest values. - val currentValues = Values(int, float, dp, color) - SideEffect { onCurrentValueChanged(currentValues) } + content { + LaunchedEffect(Unit) { + snapshotFlow { Values(int, float, dp, color) }.collect(onCurrentValueChanged) + } + } } } @@ -83,30 +85,34 @@ class AnimatedSharedAsStateTest { ) { val key = TestElements.Foo MovableElement(key = key, Modifier) { - val int by - animateSharedIntAsState(targetValues.int, debugName = TestValues.Value1.debugName) - val float by - animateSharedFloatAsState( - targetValues.float, - debugName = TestValues.Value2.debugName - ) - val dp by - animateSharedDpAsState(targetValues.dp, debugName = TestValues.Value3.debugName) - val color by - animateSharedColorAsState( - targetValues.color, - debugName = TestValues.Value4.debugName - ) + val int by animateElementIntAsState(targetValues.int, key = TestValues.Value1) + val float by animateElementFloatAsState(targetValues.float, key = TestValues.Value2) + val dp by animateElementDpAsState(targetValues.dp, key = TestValues.Value3) + val color by animateElementColorAsState(targetValues.color, key = TestValues.Value4) + + LaunchedEffect(Unit) { + snapshotFlow { Values(int, float, dp, color) }.collect(onCurrentValueChanged) + } + } + } - // Make sure we read the values during composition, so that we recompose and call - // onCurrentValueChanged() with the latest values. - val currentValues = Values(int, float, dp, color) - SideEffect { onCurrentValueChanged(currentValues) } + @Composable + private fun SceneScope.SceneValues( + targetValues: Values, + onCurrentValueChanged: (Values) -> Unit, + ) { + val int by animateSceneIntAsState(targetValues.int, key = TestValues.Value1) + val float by animateSceneFloatAsState(targetValues.float, key = TestValues.Value2) + val dp by animateSceneDpAsState(targetValues.dp, key = TestValues.Value3) + val color by animateSceneColorAsState(targetValues.color, key = TestValues.Value4) + + LaunchedEffect(Unit) { + snapshotFlow { Values(int, float, dp, color) }.collect(onCurrentValueChanged) } } @Test - fun animateSharedValues() { + fun animateElementValues() { val fromValues = Values(int = 0, float = 0f, dp = 0.dp, color = Color.Red) val toValues = Values(int = 100, float = 100f, dp = 100.dp, color = Color.Blue) @@ -194,24 +200,183 @@ class AnimatedSharedAsStateTest { } at(16) { - // Given that we use MovableElement here, animateSharedXAsState is composed only - // once, in the highest scene (in this case, in toScene). - assertThat(lastValueInFrom).isEqualTo(fromValues) + assertThat(lastValueInFrom).isEqualTo(lerp(fromValues, toValues, fraction = 0.25f)) assertThat(lastValueInTo).isEqualTo(lerp(fromValues, toValues, fraction = 0.25f)) } at(32) { - assertThat(lastValueInFrom).isEqualTo(fromValues) + assertThat(lastValueInFrom).isEqualTo(lerp(fromValues, toValues, fraction = 0.5f)) assertThat(lastValueInTo).isEqualTo(lerp(fromValues, toValues, fraction = 0.5f)) } at(48) { - assertThat(lastValueInFrom).isEqualTo(fromValues) + assertThat(lastValueInFrom).isEqualTo(lerp(fromValues, toValues, fraction = 0.75f)) assertThat(lastValueInTo).isEqualTo(lerp(fromValues, toValues, fraction = 0.75f)) } after { + assertThat(lastValueInFrom).isEqualTo(toValues) + assertThat(lastValueInTo).isEqualTo(toValues) + } + } + } + + @Test + fun animateSceneValues() { + val fromValues = Values(int = 0, float = 0f, dp = 0.dp, color = Color.Red) + val toValues = Values(int = 100, float = 100f, dp = 100.dp, color = Color.Blue) + + var lastValueInFrom = fromValues + var lastValueInTo = toValues + + rule.testTransition( + fromSceneContent = { + SceneValues( + targetValues = fromValues, + onCurrentValueChanged = { lastValueInFrom = it } + ) + }, + toSceneContent = { + SceneValues(targetValues = toValues, onCurrentValueChanged = { lastValueInTo = it }) + }, + transition = { + // The transition lasts 64ms = 4 frames. + spec = tween(durationMillis = 16 * 4, easing = LinearEasing) + }, + fromScene = TestScenes.SceneA, + toScene = TestScenes.SceneB, + ) { + before { assertThat(lastValueInFrom).isEqualTo(fromValues) + + // to was not composed yet, so lastValueInTo was not set yet. + assertThat(lastValueInTo).isEqualTo(toValues) + } + + at(16) { + // Given that we use scene values here, animateSceneXAsState is composed in both + // scenes and values should be interpolated with the transition fraction. + val expectedValues = lerp(fromValues, toValues, fraction = 0.25f) + assertThat(lastValueInFrom).isEqualTo(expectedValues) + assertThat(lastValueInTo).isEqualTo(expectedValues) + } + + at(32) { + val expectedValues = lerp(fromValues, toValues, fraction = 0.5f) + assertThat(lastValueInFrom).isEqualTo(expectedValues) + assertThat(lastValueInTo).isEqualTo(expectedValues) + } + + at(48) { + val expectedValues = lerp(fromValues, toValues, fraction = 0.75f) + assertThat(lastValueInFrom).isEqualTo(expectedValues) + assertThat(lastValueInTo).isEqualTo(expectedValues) + } + + after { + assertThat(lastValueInFrom).isEqualTo(toValues) + assertThat(lastValueInTo).isEqualTo(toValues) + } + } + } + + @Test + fun readingAnimatedStateValueDuringCompositionThrows() { + assertThrows(IllegalStateException::class.java) { + rule.testTransition( + fromSceneContent = { animateSceneIntAsState(0, TestValues.Value1).value }, + toSceneContent = {}, + transition = {}, + ) {} + } + } + + @Test + fun readingAnimatedStateValueDuringCompositionIsStillPossible() { + @Composable + fun SceneScope.SceneValuesDuringComposition( + targetValues: Values, + onCurrentValueChanged: (Values) -> Unit, + ) { + val int by + animateSceneIntAsState(targetValues.int, key = TestValues.Value1) + .unsafeCompositionState(targetValues.int) + val float by + animateSceneFloatAsState(targetValues.float, key = TestValues.Value2) + .unsafeCompositionState(targetValues.float) + val dp by + animateSceneDpAsState(targetValues.dp, key = TestValues.Value3) + .unsafeCompositionState(targetValues.dp) + val color by + animateSceneColorAsState(targetValues.color, key = TestValues.Value4) + .unsafeCompositionState(targetValues.color) + + val values = Values(int, float, dp, color) + SideEffect { onCurrentValueChanged(values) } + } + + val fromValues = Values(int = 0, float = 0f, dp = 0.dp, color = Color.Red) + val toValues = Values(int = 100, float = 100f, dp = 100.dp, color = Color.Blue) + + var lastValueInFrom = fromValues + var lastValueInTo = toValues + + rule.testTransition( + fromSceneContent = { + SceneValuesDuringComposition( + targetValues = fromValues, + onCurrentValueChanged = { lastValueInFrom = it }, + ) + }, + toSceneContent = { + SceneValuesDuringComposition( + targetValues = toValues, + onCurrentValueChanged = { lastValueInTo = it }, + ) + }, + transition = { + // The transition lasts 64ms = 4 frames. + spec = tween(durationMillis = 16 * 4, easing = LinearEasing) + }, + ) { + before { + assertThat(lastValueInFrom).isEqualTo(fromValues) + + // to was not composed yet, so lastValueInTo was not set yet. + assertThat(lastValueInTo).isEqualTo(toValues) + } + + at(16) { + // Because we are using unsafeCompositionState(), values are one frame behind their + // expected progress so at this first frame we are at progress = 0% instead of 25%. + val expectedValues = lerp(fromValues, toValues, fraction = 0f) + assertThat(lastValueInFrom).isEqualTo(expectedValues) + assertThat(lastValueInTo).isEqualTo(expectedValues) + } + + at(32) { + // One frame behind, so 25% instead of 50%. + val expectedValues = lerp(fromValues, toValues, fraction = 0.25f) + assertThat(lastValueInFrom).isEqualTo(expectedValues) + assertThat(lastValueInTo).isEqualTo(expectedValues) + } + + at(48) { + // One frame behind, so 50% instead of 75%. + val expectedValues = lerp(fromValues, toValues, fraction = 0.5f) + assertThat(lastValueInFrom).isEqualTo(expectedValues) + assertThat(lastValueInTo).isEqualTo(expectedValues) + } + + after { + // from should have been last composed at progress = 100% before it is removed from + // composition, but given that we are one frame behind the last values are stuck at + // 75%. + assertThat(lastValueInFrom).isEqualTo(lerp(fromValues, toValues, fraction = 0.75f)) + + // The after {} block resumes the clock and will run as many frames as necessary so + // that the application is idle, so the toScene settle to the idle state and to the + // final values. assertThat(lastValueInTo).isEqualTo(toValues) } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementScenePickerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementScenePickerTest.kt new file mode 100644 index 000000000000..3b022e8adc72 --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementScenePickerTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 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.compose.animation.scene + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ElementScenePickerTest { + @get:Rule val rule = createComposeRule() + + @Test + fun highestZIndexPicker() { + val key = ElementKey("TestElement", scenePicker = HighestZIndexScenePicker) + rule.testTransition( + fromSceneContent = { Box(Modifier.element(key).size(10.dp)) }, + toSceneContent = { Box(Modifier.element(key).size(10.dp)) }, + transition = { spec = tween(4 * 16, easing = LinearEasing) }, + fromScene = TestScenes.SceneA, + toScene = TestScenes.SceneB, + ) { + before { + onElement(key, TestScenes.SceneA).assertIsDisplayed() + onElement(key, TestScenes.SceneB).assertDoesNotExist() + } + at(32) { + // Scene B has the highest index, so the element is placed only there. + onElement(key, TestScenes.SceneA).assertExists().assertIsNotDisplayed() + onElement(key, TestScenes.SceneB).assertIsDisplayed() + } + after { + onElement(key, TestScenes.SceneA).assertDoesNotExist() + onElement(key, TestScenes.SceneB).assertIsDisplayed() + } + } + } + + @Test + fun lowestZIndexPicker() { + val key = ElementKey("TestElement", scenePicker = LowestZIndexScenePicker) + rule.testTransition( + fromSceneContent = { Box(Modifier.element(key).size(10.dp)) }, + toSceneContent = { Box(Modifier.element(key).size(10.dp)) }, + transition = { spec = tween(4 * 16, easing = LinearEasing) }, + fromScene = TestScenes.SceneA, + toScene = TestScenes.SceneB, + ) { + before { + onElement(key, TestScenes.SceneA).assertIsDisplayed() + onElement(key, TestScenes.SceneB).assertDoesNotExist() + } + at(32) { + // Scene A has the lowest index, so the element is placed only there. + onElement(key, TestScenes.SceneA).assertIsDisplayed() + onElement(key, TestScenes.SceneB).assertExists().assertIsNotDisplayed() + } + after { + onElement(key, TestScenes.SceneA).assertDoesNotExist() + onElement(key, TestScenes.SceneB).assertIsDisplayed() + } + } + } +} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index da5a0a04ed63..54c5de710f77 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -306,7 +306,7 @@ class ElementTest { assertThat(layoutImpl.elements.keys).containsExactly(key) val element = layoutImpl.elements.getValue(key) - assertThat(element.sceneValues.keys).containsExactly(TestScenes.SceneB) + assertThat(element.sceneStates.keys).containsExactly(TestScenes.SceneB) // Scene C, state 0: the same element is reused. currentScene = TestScenes.SceneC @@ -315,7 +315,7 @@ class ElementTest { assertThat(layoutImpl.elements.keys).containsExactly(key) assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element) - assertThat(element.sceneValues.keys).containsExactly(TestScenes.SceneC) + assertThat(element.sceneStates.keys).containsExactly(TestScenes.SceneC) // Scene C, state 1: the same element is reused. sceneCState = 1 @@ -323,7 +323,7 @@ class ElementTest { assertThat(layoutImpl.elements.keys).containsExactly(key) assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element) - assertThat(element.sceneValues.keys).containsExactly(TestScenes.SceneC) + assertThat(element.sceneStates.keys).containsExactly(TestScenes.SceneC) // Scene D, state 0: the same element is reused. currentScene = TestScenes.SceneD @@ -332,7 +332,7 @@ class ElementTest { assertThat(layoutImpl.elements.keys).containsExactly(key) assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element) - assertThat(element.sceneValues.keys).containsExactly(TestScenes.SceneD) + assertThat(element.sceneStates.keys).containsExactly(TestScenes.SceneD) // Scene D, state 1: the same element is reused. sceneDState = 1 @@ -340,13 +340,13 @@ class ElementTest { assertThat(layoutImpl.elements.keys).containsExactly(key) assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element) - assertThat(element.sceneValues.keys).containsExactly(TestScenes.SceneD) + assertThat(element.sceneStates.keys).containsExactly(TestScenes.SceneD) // Scene D, state 2: the element is removed from the map. sceneDState = 2 rule.waitForIdle() - assertThat(element.sceneValues).isEmpty() + assertThat(element.sceneStates).isEmpty() assertThat(layoutImpl.elements).isEmpty() } @@ -442,7 +442,7 @@ class ElementTest { // There is only Foo in the elements map. assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Foo) val fooElement = layoutImpl.elements.getValue(TestElements.Foo) - assertThat(fooElement.sceneValues.keys).containsExactly(TestScenes.SceneA) + assertThat(fooElement.sceneStates.keys).containsExactly(TestScenes.SceneA) key = TestElements.Bar @@ -450,8 +450,8 @@ class ElementTest { rule.waitForIdle() assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Bar) val barElement = layoutImpl.elements.getValue(TestElements.Bar) - assertThat(barElement.sceneValues.keys).containsExactly(TestScenes.SceneA) - assertThat(fooElement.sceneValues).isEmpty() + assertThat(barElement.sceneStates.keys).containsExactly(TestScenes.SceneA) + assertThat(fooElement.sceneStates).isEmpty() } @Test @@ -505,7 +505,7 @@ class ElementTest { // There is only Foo in the elements map. assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Foo) val element = layoutImpl.elements.getValue(TestElements.Foo) - val sceneValues = element.sceneValues + val sceneValues = element.sceneStates assertThat(sceneValues.keys).containsExactly(TestScenes.SceneA) // Get the ElementModifier node that should be reused later on when coming back to this @@ -528,7 +528,7 @@ class ElementTest { assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Foo) val newElement = layoutImpl.elements.getValue(TestElements.Foo) - val newSceneValues = newElement.sceneValues + val newSceneValues = newElement.sceneStates assertThat(newElement).isNotEqualTo(element) assertThat(newSceneValues).isNotEqualTo(sceneValues) assertThat(newSceneValues.keys).containsExactly(TestScenes.SceneA) @@ -579,11 +579,11 @@ class ElementTest { fun foo() = layoutImpl().elements[TestElements.Foo] ?: error("Foo not in elements map") - fun Element.lastSharedOffset() = lastSharedValues.offset.toDpOffset() + fun Element.lastSharedOffset() = lastSharedState.offset.toDpOffset() fun Element.lastOffsetIn(scene: SceneKey) = - (sceneValues[scene] ?: error("$scene not in sceneValues map")) - .lastValues + (sceneStates[scene] ?: error("$scene not in sceneValues map")) + .lastState .offset .toDpOffset() diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementScenePickerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementScenePickerTest.kt new file mode 100644 index 000000000000..fb46a34e3cab --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementScenePickerTest.kt @@ -0,0 +1,53 @@ +/* + * 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.compose.animation.scene + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class MovableElementScenePickerTest { + @Test + fun toSceneInScenes() { + val picker = MovableElementScenePicker(scenes = setOf(TestScenes.SceneA, TestScenes.SceneB)) + assertThat( + picker.sceneDuringTransition( + TestElements.Foo, + transition(from = TestScenes.SceneA, to = TestScenes.SceneB), + fromSceneZIndex = 0f, + toSceneZIndex = 1f, + ) + ) + .isEqualTo(TestScenes.SceneB) + } + + @Test + fun toSceneNotInScenes() { + val picker = MovableElementScenePicker(scenes = emptySet()) + assertThat( + picker.sceneDuringTransition( + TestElements.Foo, + transition(from = TestScenes.SceneA, to = TestScenes.SceneB), + fromSceneZIndex = 0f, + toSceneZIndex = 1f, + ) + ) + .isEqualTo(TestScenes.SceneA) + } +} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt index 3cd65cde274e..35cb691e6e37 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt @@ -28,19 +28,24 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.assertPositionInRootIsEqualTo import androidx.compose.ui.test.hasParent import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.compose.test.assertSizeIsEqualTo import com.google.common.truth.Truth.assertThat +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -58,7 +63,7 @@ class MovableElementTest { @Composable private fun SceneScope.MovableCounter(key: ElementKey, modifier: Modifier) { - MovableElement(key, modifier) { Counter() } + MovableElement(key, modifier) { content { Counter() } } } @Test @@ -142,39 +147,37 @@ class MovableElementTest { @Test fun movableElementIsMovedAndComposedOnlyOnce() { - rule.testTransition( - fromSceneContent = { MovableCounter(TestElements.Foo, Modifier.size(50.dp)) }, - toSceneContent = { MovableCounter(TestElements.Foo, Modifier.size(100.dp)) }, - transition = { - spec = tween(durationMillis = 16 * 4, easing = LinearEasing) - sharedElement( - TestElements.Foo, - scenePicker = - object : SharedElementScenePicker { - override fun sceneDuringTransition( - element: ElementKey, - fromScene: SceneKey, - toScene: SceneKey, - progress: () -> Float, - fromSceneZIndex: Float, - toSceneZIndex: Float - ): SceneKey { - assertThat(fromScene).isEqualTo(TestScenes.SceneA) - assertThat(toScene).isEqualTo(TestScenes.SceneB) - assertThat(fromSceneZIndex).isEqualTo(0) - assertThat(toSceneZIndex).isEqualTo(1) + val key = + ElementKey( + "Foo", + scenePicker = + object : ElementScenePicker { + override fun sceneDuringTransition( + element: ElementKey, + transition: TransitionState.Transition, + fromSceneZIndex: Float, + toSceneZIndex: Float + ): SceneKey { + assertThat(transition.fromScene).isEqualTo(TestScenes.SceneA) + assertThat(transition.toScene).isEqualTo(TestScenes.SceneB) + assertThat(fromSceneZIndex).isEqualTo(0) + assertThat(toSceneZIndex).isEqualTo(1) - // Compose Foo in Scene A if progress < 0.65f, otherwise compose it - // in Scene B. - return if (progress() < 0.65f) { - TestScenes.SceneA - } else { - TestScenes.SceneB - } + // Compose Foo in Scene A if progress < 0.65f, otherwise compose it + // in Scene B. + return if (transition.progress < 0.65f) { + TestScenes.SceneA + } else { + TestScenes.SceneB } } - ) - }, + } + ) + + rule.testTransition( + fromSceneContent = { MovableCounter(key, Modifier.size(50.dp)) }, + toSceneContent = { MovableCounter(key, Modifier.size(100.dp)) }, + transition = { spec = tween(durationMillis = 16 * 4, easing = LinearEasing) }, fromScene = TestScenes.SceneA, toScene = TestScenes.SceneB, ) { @@ -257,4 +260,73 @@ class MovableElementTest { } } } + + @Test + @Ignore("b/317972419#comment2") + fun movableElementContentIsRecomposedIfContentParametersChange() { + @Composable + fun SceneScope.MovableFoo(text: String, modifier: Modifier = Modifier) { + MovableElement(TestElements.Foo, modifier) { content { Text(text) } } + } + + rule.testTransition( + fromSceneContent = { MovableFoo(text = "fromScene") }, + toSceneContent = { MovableFoo(text = "toScene") }, + transition = { spec = tween(durationMillis = 16 * 4, easing = LinearEasing) }, + fromScene = TestScenes.SceneA, + toScene = TestScenes.SceneB, + ) { + // Before the transition, only fromScene is composed. + before { + rule.onNodeWithText("fromScene").assertIsDisplayed() + rule.onNodeWithText("toScene").assertDoesNotExist() + } + + // During the transition, the element is composed in toScene. + at(32) { + rule.onNodeWithText("fromScene").assertDoesNotExist() + rule.onNodeWithText("toScene").assertIsDisplayed() + } + + // At the end of the transition, the element is composed in toScene. + after { + rule.onNodeWithText("fromScene").assertDoesNotExist() + rule.onNodeWithText("toScene").assertIsDisplayed() + } + } + } + + @Test + fun elementScopeExtendsBoxScope() { + rule.setContent { + TestSceneScope { + Element(TestElements.Foo, Modifier.size(200.dp)) { + content { + Box(Modifier.testTag("bottomEnd").align(Alignment.BottomEnd)) + Box(Modifier.testTag("matchParentSize").matchParentSize()) + } + } + } + } + + rule.onNodeWithTag("bottomEnd").assertPositionInRootIsEqualTo(200.dp, 200.dp) + rule.onNodeWithTag("matchParentSize").assertSizeIsEqualTo(200.dp, 200.dp) + } + + @Test + fun movableElementScopeExtendsBoxScope() { + rule.setContent { + TestSceneScope { + MovableElement(TestElements.Foo, Modifier.size(200.dp)) { + content { + Box(Modifier.testTag("bottomEnd").align(Alignment.BottomEnd)) + Box(Modifier.testTag("matchParentSize").matchParentSize()) + } + } + } + } + + rule.onNodeWithTag("bottomEnd").assertPositionInRootIsEqualTo(200.dp, 200.dp) + rule.onNodeWithTag("matchParentSize").assertSizeIsEqualTo(200.dp, 200.dp) + } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt index c5b8d9ae0d10..75dee47a91cd 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt @@ -50,13 +50,4 @@ class SceneTransitionLayoutStateTest { assertThat(state.isTransitioning(to = TestScenes.SceneA)).isFalse() assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue() } - - private fun transition(from: SceneKey, to: SceneKey): TransitionState.Transition { - return object : TransitionState.Transition(from, to) { - override val currentScene: SceneKey = from - override val progress: Float = 0f - override val isInitiatedByUserInput: Boolean = false - override val isUserInputOngoing: Boolean = false - } - } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt index ebbd5006be55..649e4991434e 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt @@ -113,25 +113,21 @@ class SceneTransitionLayoutTest { @Composable private fun SceneScope.SharedFoo(size: Dp, childOffset: Dp, modifier: Modifier = Modifier) { - Box( - modifier - .size(size) - .background(Color.Red) - .element(TestElements.Foo) - .testTag(TestElements.Foo.debugName) - ) { + Element(TestElements.Foo, modifier.size(size).background(Color.Red)) { // Offset the single child of Foo by some animated shared offset. - val offset by animateSharedDpAsState(childOffset, TestValues.Value1, TestElements.Foo) - - Box( - Modifier.offset { - val pxOffset = offset.roundToPx() - IntOffset(pxOffset, pxOffset) - } - .size(30.dp) - .background(Color.Blue) - .testTag(TestElements.Bar.debugName) - ) + val offset by animateElementDpAsState(childOffset, TestValues.Value1) + + content { + Box( + Modifier.offset { + val pxOffset = offset.roundToPx() + IntOffset(pxOffset, pxOffset) + } + .size(30.dp) + .background(Color.Blue) + .testTag(TestElements.Bar.debugName) + ) + } } } diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt new file mode 100644 index 000000000000..238b21e1ea37 --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt @@ -0,0 +1,33 @@ +/* + * 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.compose.animation.scene + +/** A utility to easily create a [TransitionState.Transition] in tests. */ +fun transition( + from: SceneKey, + to: SceneKey, + progress: () -> Float = { 0f }, + isInitiatedByUserInput: Boolean = false, + isUserInputOngoing: Boolean = false, +): TransitionState.Transition { + return object : TransitionState.Transition(from, to) { + override val currentScene: SceneKey = from + override val progress: Float = progress() + override val isInitiatedByUserInput: Boolean = isInitiatedByUserInput + override val isUserInputOngoing: Boolean = isUserInputOngoing + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index 1fc2843b1cb8..c8461d2c5415 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt @@ -45,10 +45,10 @@ 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.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory import com.android.systemui.log.SessionTracker @@ -140,7 +140,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { @Mock private lateinit var viewMediatorCallback: ViewMediatorCallback @Mock private lateinit var audioManager: AudioManager @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor - @Mock private lateinit var faceAuthInteractor: KeyguardFaceAuthInteractor + @Mock private lateinit var faceAuthInteractor: DeviceEntryFaceAuthInteractor @Mock private lateinit var faceAuthAccessibilityDelegate: FaceAuthAccessibilityDelegate @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController @Mock private lateinit var postureController: DevicePostureController diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/SysuiTestCaseSelfTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/SysuiTestCaseSelfTest.kt new file mode 100644 index 000000000000..be6bb9c39299 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/SysuiTestCaseSelfTest.kt @@ -0,0 +1,35 @@ +/* + * 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 + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SysuiTestCaseSelfTest : SysuiTestCase() { + private val contextBeforeSetup = context + + // cf b/311612168 + @Test + fun captureCorrectContextBeforeSetupRuns() { + Truth.assertThat(contextBeforeSetup).isEqualTo(context) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index e5da1f86a841..36aa4416f292 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -85,10 +85,10 @@ import com.android.systemui.biometrics.ui.viewmodel.DefaultUdfpsTouchOverlayView import com.android.systemui.biometrics.ui.viewmodel.DeviceEntryUdfpsTouchOverlayViewModel; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor; +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.ScreenLifecycle; -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.FalsingManager; @@ -336,7 +336,7 @@ public class UdfpsControllerTest extends SysuiTestCase { mSessionTracker, mAlternateBouncerInteractor, mInputManager, - mock(KeyguardFaceAuthInteractor.class), + mock(DeviceEntryFaceAuthInteractor.class), mUdfpsKeyguardAccessibilityDelegate, mSelectedUserInteractor, mFpsUnlockTracker, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt index 1f8854ffc1c8..335ac9d42e77 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt @@ -32,11 +32,11 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants import com.android.systemui.bouncer.ui.BouncerView import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.FakeTrustRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState @@ -111,7 +111,7 @@ class UdfpsKeyguardViewLegacyControllerWithCoroutinesTest : FakeTrustRepository(), testScope.backgroundScope, mSelectedUserInteractor, - mock(KeyguardFaceAuthInteractor::class.java), + mock(DeviceEntryFaceAuthInteractor::class.java), ) mAlternateBouncerInteractor = AlternateBouncerInteractor( diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt index 640807b110d2..8d6d052b8769 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt @@ -24,6 +24,7 @@ import android.view.DisplayInfo import android.view.WindowInsets import android.view.WindowManager import android.view.WindowMetrics +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider @@ -36,15 +37,24 @@ import com.android.systemui.biometrics.shared.model.DisplayRotation.ROTATION_90 import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.KeyguardState.OFF +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.kosmos.testScope import com.android.systemui.log.SideFpsLogger import com.android.systemui.log.logcatLogBuffer import com.android.systemui.res.R +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -52,7 +62,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.junit.runners.JUnit4 import org.mockito.Mock import org.mockito.Mockito.mock import org.mockito.Mockito.spy @@ -60,11 +69,12 @@ import org.mockito.junit.MockitoJUnit @OptIn(ExperimentalCoroutinesApi::class) @SmallTest -@RunWith(JUnit4::class) +@RunWith(AndroidJUnit4::class) class SideFpsSensorInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() @JvmField @Rule var mockitoRule = MockitoJUnit.rule() - private val testScope = TestScope(StandardTestDispatcher()) + private val testScope = kosmos.testScope private val fingerprintRepository = FakeFingerprintPropertyRepository() @@ -94,6 +104,7 @@ class SideFpsSensorInteractorTest : SysuiTestCase() { contextDisplayInfo.uniqueId = "current-display" whenever(fingerprintInteractiveToAuthProvider.enabledForCurrentUser) .thenReturn(isRestToUnlockEnabled) + overrideResource(R.bool.config_restToUnlockSupported, true) underTest = SideFpsSensorInteractor( mContext, @@ -101,6 +112,7 @@ class SideFpsSensorInteractorTest : SysuiTestCase() { windowManager, displayStateInteractor, Optional.of(fingerprintInteractiveToAuthProvider), + kosmos.keyguardTransitionInteractor, SideFpsLogger(logcatLogBuffer("SfpsLogger")) ) } @@ -129,11 +141,62 @@ class SideFpsSensorInteractorTest : SysuiTestCase() { assertThat(isAvailable).isFalse() } + private suspend fun sendTransition(from: KeyguardState, to: KeyguardState) { + kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( + listOf( + TransitionStep( + from = from, + to = to, + transitionState = TransitionState.STARTED, + ), + TransitionStep( + from = from, + to = to, + transitionState = TransitionState.FINISHED, + value = 1.0f + ) + ), + testScope + ) + } + @Test - fun authenticationDurationIsAvailableWhenSFPSSensorIsAvailable() = + fun authenticationDurationIsLongerIfScreenIsOff() = testScope.runTest { - assertThat(underTest.authenticationDuration) - .isEqualTo(context.resources.getInteger(R.integer.config_restToUnlockDuration)) + val authenticationDuration by collectLastValue(underTest.authenticationDuration) + val longDuration = + context.resources.getInteger(R.integer.config_restToUnlockDurationScreenOff) + sendTransition(LOCKSCREEN, OFF) + + runCurrent() + assertThat(authenticationDuration).isEqualTo(longDuration) + } + + @Test + fun authenticationDurationIsLongerIfScreenIsDozing() = + testScope.runTest { + val authenticationDuration by collectLastValue(underTest.authenticationDuration) + val longDuration = + context.resources.getInteger(R.integer.config_restToUnlockDurationScreenOff) + sendTransition(LOCKSCREEN, DOZING) + runCurrent() + assertThat(authenticationDuration).isEqualTo(longDuration) + } + + @Test + fun authenticationDurationIsShorterIfScreenIsNotDozingOrOff() = + testScope.runTest { + val authenticationDuration by collectLastValue(underTest.authenticationDuration) + val shortDuration = + context.resources.getInteger(R.integer.config_restToUnlockDurationDefault) + val allOtherKeyguardStates = KeyguardState.entries.filter { it != OFF && it != DOZING } + + allOtherKeyguardStates.forEach { destinationState -> + sendTransition(OFF, destinationState) + + runCurrent() + assertThat(authenticationDuration).isEqualTo(shortDuration) + } } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt index 99c18744f9b4..93ba6a48f3dd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt @@ -25,7 +25,7 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.res.R import com.android.systemui.scene.SceneTestUtils import com.google.common.truth.Truth.assertThat @@ -46,7 +46,7 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) class BouncerInteractorTest : SysuiTestCase() { - @Mock private lateinit var keyguardFaceAuthInteractor: KeyguardFaceAuthInteractor + @Mock private lateinit var mDeviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor private val utils = SceneTestUtils(this) private val testScope = utils.testScope @@ -67,7 +67,7 @@ class BouncerInteractorTest : SysuiTestCase() { underTest = utils.bouncerInteractor( authenticationInteractor = authenticationInteractor, - keyguardFaceAuthInteractor = keyguardFaceAuthInteractor, + deviceEntryFaceAuthInteractor = mDeviceEntryFaceAuthInteractor, ) } @@ -306,7 +306,7 @@ class BouncerInteractorTest : SysuiTestCase() { fun intentionalUserInputEvent_notifiesFaceAuthInteractor() = testScope.runTest { underTest.onIntentionalUserInput() - verify(keyguardFaceAuthInteractor).onPrimaryBouncerUserInput() + verify(mDeviceEntryFaceAuthInteractor).onPrimaryBouncerUserInput() } companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt index bdf5041f8a38..c8560c31cdf1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt @@ -26,9 +26,9 @@ import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepositor import com.android.systemui.bouncer.ui.BouncerView import com.android.systemui.classifier.FalsingCollector import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.repository.TrustRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.utils.os.FakeHandler @@ -54,7 +54,7 @@ class PrimaryBouncerInteractorWithCoroutinesTest : SysuiTestCase() { @Mock private lateinit var dismissCallbackRegistry: DismissCallbackRegistry @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor - @Mock private lateinit var faceAuthInteractor: KeyguardFaceAuthInteractor + @Mock private lateinit var faceAuthInteractor: DeviceEntryFaceAuthInteractor private val mainHandler = FakeHandler(Looper.getMainLooper()) private lateinit var underTest: PrimaryBouncerInteractor diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt index a3bf3f492e6e..a0c2acc31589 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt @@ -30,9 +30,9 @@ import com.android.systemui.bouncer.ui.BouncerView import com.android.systemui.classifier.FalsingCollector import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.repository.TrustRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor import com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.user.domain.interactor.SelectedUserInteractor @@ -63,7 +63,7 @@ class KeyguardBouncerViewModelTest : SysuiTestCase() { @Mock private lateinit var dismissCallbackRegistry: DismissCallbackRegistry @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor - @Mock private lateinit var faceAuthInteractor: KeyguardFaceAuthInteractor + @Mock private lateinit var faceAuthInteractor: DeviceEntryFaceAuthInteractor lateinit var bouncerInteractor: PrimaryBouncerInteractor private val mainHandler = FakeHandler(Looper.getMainLooper()) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt new file mode 100644 index 000000000000..6380ace7ba4f --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2023 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.common.data.repository + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Handler +import android.os.UserHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.data.shared.model.PackageChangeModel +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.coroutines.collectValues +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.log.logcatLogBuffer +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +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 + +@SmallTest +@RunWith(AndroidJUnit4::class) +class PackageChangeRepositoryTest : SysuiTestCase() { + + private val kosmos = testKosmos() + + @Mock private lateinit var context: Context + @Mock private lateinit var packageManager: PackageManager + @Mock private lateinit var handler: Handler + + private lateinit var repository: PackageChangeRepository + private lateinit var updateMonitor: PackageUpdateMonitor + + @Before + fun setUp() = + with(kosmos) { + MockitoAnnotations.initMocks(this@PackageChangeRepositoryTest) + whenever(context.packageManager).thenReturn(packageManager) + + repository = PackageChangeRepositoryImpl { user -> + updateMonitor = + PackageUpdateMonitor( + user = user, + bgDispatcher = testDispatcher, + scope = applicationCoroutineScope, + context = context, + bgHandler = handler, + logger = PackageUpdateLogger(logcatLogBuffer()) + ) + updateMonitor + } + } + + @Test + fun packageUninstalled() = + with(kosmos) { + testScope.runTest { + val packageChange by collectLastValue(repository.packageChanged(USER_100)) + assertThat(packageChange).isNull() + + updateMonitor.onPackageRemoved( + packageName = TEST_PACKAGE, + uid = UserHandle.getUid(/* userId = */ 100, /* appId = */ 10) + ) + + assertThat(packageChange).isInstanceOf(PackageChangeModel.Uninstalled::class.java) + assertThat(packageChange?.packageName).isEqualTo(TEST_PACKAGE) + } + } + + @Test + fun packageUpdateStarted() = + with(kosmos) { + testScope.runTest { + val packageChange by collectLastValue(repository.packageChanged(USER_100)) + assertThat(packageChange).isNull() + + updateMonitor.onPackageUpdateStarted( + packageName = TEST_PACKAGE, + uid = UserHandle.getUid(/* userId = */ 100, /* appId = */ 10) + ) + + assertThat(packageChange).isInstanceOf(PackageChangeModel.UpdateStarted::class.java) + assertThat(packageChange?.packageName).isEqualTo(TEST_PACKAGE) + } + } + + @Test + fun packageUpdateFinished() = + with(kosmos) { + testScope.runTest { + val packageChange by collectLastValue(repository.packageChanged(USER_100)) + assertThat(packageChange).isNull() + + updateMonitor.onPackageUpdateFinished( + packageName = TEST_PACKAGE, + uid = UserHandle.getUid(/* userId = */ 100, /* appId = */ 10) + ) + + assertThat(packageChange) + .isInstanceOf(PackageChangeModel.UpdateFinished::class.java) + assertThat(packageChange?.packageName).isEqualTo(TEST_PACKAGE) + } + } + + @Test + fun packageInstalled() = + with(kosmos) { + testScope.runTest { + val packageChange by collectLastValue(repository.packageChanged(UserHandle.ALL)) + assertThat(packageChange).isNull() + + updateMonitor.onPackageAdded( + packageName = TEST_PACKAGE, + uid = UserHandle.getUid(/* userId = */ 100, /* appId = */ 10) + ) + + assertThat(packageChange).isInstanceOf(PackageChangeModel.Installed::class.java) + assertThat(packageChange?.packageName).isEqualTo(TEST_PACKAGE) + } + } + + @Test + fun packageIsChanged() = + with(kosmos) { + testScope.runTest { + val packageChange by collectLastValue(repository.packageChanged(USER_100)) + assertThat(packageChange).isNull() + + updateMonitor.onPackageChanged( + packageName = TEST_PACKAGE, + uid = UserHandle.getUid(/* userId = */ 100, /* appId = */ 10), + components = emptyArray() + ) + + assertThat(packageChange).isInstanceOf(PackageChangeModel.Changed::class.java) + assertThat(packageChange?.packageName).isEqualTo(TEST_PACKAGE) + } + } + + @Test + fun filtersOutUpdatesFromOtherUsers() = + with(kosmos) { + testScope.runTest { + val packageChange by collectLastValue(repository.packageChanged(USER_100)) + assertThat(packageChange).isNull() + + updateMonitor.onPackageUpdateFinished( + packageName = TEST_PACKAGE, + uid = UserHandle.getUid(/* userId = */ 101, /* appId = */ 10) + ) + + updateMonitor.onPackageAdded( + packageName = TEST_PACKAGE, + uid = UserHandle.getUid(/* userId = */ 99, /* appId = */ 10) + ) + + assertThat(packageChange).isNull() + } + } + + @Test + fun listenToUpdatesFromAllUsers() = + with(kosmos) { + testScope.runTest { + val packageChanges by collectValues(repository.packageChanged(UserHandle.ALL)) + assertThat(packageChanges).isEmpty() + + updateMonitor.onPackageUpdateFinished( + packageName = TEST_PACKAGE, + uid = UserHandle.getUid(/* userId = */ 101, /* appId = */ 10) + ) + + updateMonitor.onPackageAdded( + packageName = TEST_PACKAGE, + uid = UserHandle.getUid(/* userId = */ 99, /* appId = */ 10) + ) + + assertThat(packageChanges).hasSize(2) + } + } + + private companion object { + val USER_100 = UserHandle.of(100) + const val TEST_PACKAGE = "pkg.test" + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageUpdateMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageUpdateMonitorTest.kt new file mode 100644 index 000000000000..d610925edd8a --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageUpdateMonitorTest.kt @@ -0,0 +1,210 @@ +/* + * 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.common.data.repository + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Handler +import android.os.UserHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.data.shared.model.PackageChangeModel +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.coroutines.collectValues +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.log.logcatLogBuffer +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +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.Mock +import org.mockito.MockitoAnnotations + +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(AndroidJUnit4::class) +class PackageUpdateMonitorTest : SysuiTestCase() { + private val kosmos = testKosmos() + + @Mock private lateinit var context: Context + @Mock private lateinit var packageManager: PackageManager + @Mock private lateinit var handler: Handler + + private lateinit var monitor: PackageUpdateMonitor + + @Before + fun setUp() = + with(kosmos) { + MockitoAnnotations.initMocks(this@PackageUpdateMonitorTest) + whenever(context.packageManager).thenReturn(packageManager) + + monitor = + PackageUpdateMonitor( + user = USER_100, + bgDispatcher = testDispatcher, + bgHandler = handler, + context = context, + scope = applicationCoroutineScope, + logger = PackageUpdateLogger(logcatLogBuffer()) + ) + } + + @Test + fun becomesActiveWhenFlowCollected() = + with(kosmos) { + testScope.runTest { + assertThat(monitor.isActive).isFalse() + val job = monitor.packageChanged.launchIn(this) + runCurrent() + assertThat(monitor.isActive).isTrue() + job.cancel() + runCurrent() + assertThat(monitor.isActive).isFalse() + } + } + + @Test + fun packageAdded() = + with(kosmos) { + testScope.runTest { + val packageChange by collectLastValue(monitor.packageChanged) + assertThat(packageChange).isNull() + + monitor.onPackageAdded(TEST_PACKAGE, 123) + + assertThat(packageChange) + .isEqualTo( + PackageChangeModel.Installed(packageName = TEST_PACKAGE, packageUid = 123) + ) + } + } + + @Test + fun packageRemoved() = + with(kosmos) { + testScope.runTest { + val packageChange by collectLastValue(monitor.packageChanged) + assertThat(packageChange).isNull() + + monitor.onPackageRemoved(TEST_PACKAGE, 123) + + assertThat(packageChange) + .isEqualTo( + PackageChangeModel.Uninstalled(packageName = TEST_PACKAGE, packageUid = 123) + ) + } + } + + @Test + fun packageChanged() = + with(kosmos) { + testScope.runTest { + val packageChange by collectLastValue(monitor.packageChanged) + assertThat(packageChange).isNull() + + monitor.onPackageChanged(TEST_PACKAGE, 123, emptyArray()) + + assertThat(packageChange) + .isEqualTo( + PackageChangeModel.Changed(packageName = TEST_PACKAGE, packageUid = 123) + ) + } + } + + @Test + fun packageUpdateStarted() = + with(kosmos) { + testScope.runTest { + val packageChange by collectLastValue(monitor.packageChanged) + assertThat(packageChange).isNull() + + monitor.onPackageUpdateStarted(TEST_PACKAGE, 123) + + assertThat(packageChange) + .isEqualTo( + PackageChangeModel.UpdateStarted( + packageName = TEST_PACKAGE, + packageUid = 123 + ) + ) + } + } + + @Test + fun packageUpdateFinished() = + with(kosmos) { + testScope.runTest { + val packageChange by collectLastValue(monitor.packageChanged) + assertThat(packageChange).isNull() + + monitor.onPackageUpdateFinished(TEST_PACKAGE, 123) + + assertThat(packageChange) + .isEqualTo( + PackageChangeModel.UpdateFinished( + packageName = TEST_PACKAGE, + packageUid = 123 + ) + ) + } + } + + @Test + fun handlesBackflow() = + with(kosmos) { + testScope.runTest { + val latch = MutableSharedFlow<Unit>() + val packageChanges by collectValues(monitor.packageChanged.onEach { latch.first() }) + assertThat(packageChanges).isEmpty() + + monitor.onPackageAdded(TEST_PACKAGE, 123) + monitor.onPackageUpdateStarted(TEST_PACKAGE, 123) + monitor.onPackageUpdateFinished(TEST_PACKAGE, 123) + + assertThat(packageChanges).isEmpty() + latch.emit(Unit) + assertThat(packageChanges).hasSize(1) + latch.emit(Unit) + assertThat(packageChanges).hasSize(2) + latch.emit(Unit) + assertThat(packageChanges) + .containsExactly( + PackageChangeModel.Installed(TEST_PACKAGE, 123), + PackageChangeModel.UpdateStarted(TEST_PACKAGE, 123), + PackageChangeModel.UpdateFinished(TEST_PACKAGE, 123), + ) + .inOrder() + } + } + + companion object { + private val USER_100 = UserHandle.of(100) + private const val TEST_PACKAGE = "pkg.test" + } +} 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 ddaa4889d6f3..449ee6f414dd 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 @@ -42,6 +42,7 @@ import com.android.systemui.util.mockito.kotlinArgumentCaptor import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flowOf @@ -63,6 +64,8 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) class CommunalWidgetRepositoryImplTest : SysuiTestCase() { + @Mock private lateinit var appWidgetManagerOptional: Optional<AppWidgetManager> + @Mock private lateinit var appWidgetManager: AppWidgetManager @Mock private lateinit var appWidgetHost: AppWidgetHost @@ -113,6 +116,8 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { whenever(stopwatchProviderInfo.loadLabel(any())).thenReturn("Stopwatch") whenever(userTracker.userHandle).thenReturn(userHandle) whenever(communalWidgetDao.getWidgets()).thenReturn(flowOf(emptyMap())) + whenever(appWidgetManagerOptional.isPresent).thenReturn(true) + whenever(appWidgetManagerOptional.get()).thenReturn(appWidgetManager) } @Test @@ -296,7 +301,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { private fun initCommunalWidgetRepository(): CommunalWidgetRepositoryImpl { return CommunalWidgetRepositoryImpl( - appWidgetManager, + appWidgetManagerOptional, appWidgetHost, testScope.backgroundScope, testDispatcher, 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 314dfdfd6f2a..f2f97054ea12 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 @@ -33,14 +33,15 @@ import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.shade.ShadeViewController import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import javax.inject.Provider -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -56,7 +57,8 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Mock private lateinit var shadeViewController: ShadeViewController @Mock private lateinit var powerManager: PowerManager - private lateinit var testScope: TestScope + private val kosmos = testKosmos() + private val testScope = kosmos.testScope private lateinit var keyguardRepository: FakeKeyguardRepository private lateinit var communalRepository: FakeCommunalRepository @@ -71,8 +73,6 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - testScope = TestScope() - val withDeps = CommunalInteractorFactory.create() keyguardRepository = withDeps.keyguardRepository communalRepository = withDeps.communalRepository @@ -130,4 +130,17 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { assertThat(communalContent?.get(1)) .isInstanceOf(CommunalContentModel.Widget::class.java) } + + @Test + fun interactionHandlerIgnoresClicks() { + val interactionHandler = underTest.getInteractionHandler() + assertThat( + interactionHandler.onInteraction( + /* view = */ mock(), + /* pendingIntent = */ mock(), + /* response = */ mock() + ) + ) + .isEqualTo(false) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index 8a71168324aa..182cc5d750bb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -31,6 +31,7 @@ import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.ui.viewmodel.CommunalViewModel +import com.android.systemui.communal.widgets.WidgetInteractionHandler import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.media.controls.ui.MediaHost @@ -84,6 +85,7 @@ class CommunalViewModelTest : SysuiTestCase() { underTest = CommunalViewModel( withDeps.communalInteractor, + WidgetInteractionHandler(mock()), withDeps.tutorialInteractor, Provider { shadeViewController }, powerManager, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt index 941d67f0a34e..6a14220e6a42 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.keyguard.data.repository +package com.android.systemui.deviceentry.data.repository import android.app.StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP import android.app.StatusBarManager.SESSION_KEYGUARD @@ -37,10 +37,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId.fakeInstanceId import com.android.internal.logging.UiEventLogger -import com.android.keyguard.FaceAuthUiEvent -import com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN -import com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_NOTIFICATION_PANEL_CLICKED -import com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.data.repository.FakeDisplayStateRepository @@ -53,21 +49,32 @@ import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.coroutines.FlowValue import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues +import com.android.systemui.deviceentry.shared.FaceAuthUiEvent +import com.android.systemui.deviceentry.shared.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN +import com.android.systemui.deviceentry.shared.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_NOTIFICATION_PANEL_CLICKED +import com.android.systemui.deviceentry.shared.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER +import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.FaceDetectionStatus +import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus import com.android.systemui.display.data.repository.FakeDisplayRepository import com.android.systemui.display.data.repository.display import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags.KEYGUARD_WM_STATE_REFACTOR +import com.android.systemui.keyguard.data.repository.BiometricType +import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository +import com.android.systemui.keyguard.data.repository.FakeCommandQueue +import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.FakeTrustRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory -import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus -import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus -import com.android.systemui.keyguard.shared.model.FaceDetectionStatus -import com.android.systemui.keyguard.shared.model.HelpFaceAuthenticationStatus import com.android.systemui.keyguard.shared.model.KeyguardState -import com.android.systemui.keyguard.shared.model.SuccessFaceAuthenticationStatus import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.log.FaceAuthenticationLogger diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt index 477f4555ea65..032979447861 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,20 +12,19 @@ * 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.keyguard.data.quickaffordance import android.app.Activity import androidx.test.filters.SmallTest -import com.android.systemui.res.R import com.android.systemui.SysuiTestCase import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.ui.ControlsUiController +import com.android.systemui.res.R import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -37,17 +36,17 @@ import kotlinx.coroutines.test.runBlockingTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.junit.runners.Parameterized -import org.junit.runners.Parameterized.Parameter -import org.junit.runners.Parameterized.Parameters import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import platform.test.runner.parameterized.Parameter +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest -@RunWith(Parameterized::class) +@RunWith(ParameterizedAndroidJunit4::class) class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTestCase() { companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt index 9daf1860ebb8..e7037a682cca 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt @@ -94,7 +94,7 @@ class AlternateBouncerToAodTransitionViewModelTest : SysuiTestCase() { testScope, ) - assertThat(values.size).isEqualTo(6) + assertThat(values.size).isEqualTo(5) values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt index 53bca483f73f..e141c2b3107f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt @@ -55,6 +55,28 @@ class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() { private val underTest = kosmos.dreamingToLockscreenTransitionViewModel @Test + fun shortcutsAlpha_bothShortcutsReceiveLastValue() = + testScope.runTest { + val valuesLeft by collectValues(underTest.shortcutsAlpha) + val valuesRight by collectValues(underTest.shortcutsAlpha) + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + step(0.3f), + step(0.5f), + step(0.6f), + step(0.8f), + step(1f), + ), + testScope, + ) + + assertThat(valuesLeft.last()).isEqualTo(1f) + assertThat(valuesRight.last()).isEqualTo(1f) + } + + @Test fun dreamOverlayTranslationY() = testScope.runTest { val pixels = 100 @@ -73,7 +95,7 @@ class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() { testScope, ) - assertThat(values.size).isEqualTo(7) + assertThat(values.size).isEqualTo(6) values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) } } @@ -95,7 +117,7 @@ class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() { testScope, ) - assertThat(values.size).isEqualTo(4) + assertThat(values.size).isEqualTo(3) values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } } @@ -210,7 +232,7 @@ class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() { testScope, ) - assertThat(values.size).isEqualTo(5) + assertThat(values.size).isEqualTo(4) values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt index 3c07034f0e12..897ce6d305b6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt @@ -61,7 +61,7 @@ class GoneToDreamingTransitionViewModelTest : SysuiTestCase() { // Only three values should be present, since the dream overlay runs for a small // fraction of the overall animation time - assertThat(values.size).isEqualTo(5) + assertThat(values.size).isEqualTo(4) values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } } @@ -84,7 +84,7 @@ class GoneToDreamingTransitionViewModelTest : SysuiTestCase() { testScope, ) - assertThat(values.size).isEqualTo(5) + assertThat(values.size).isEqualTo(4) values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt index a346e8b45795..4843f8ba4249 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt @@ -75,7 +75,7 @@ class LockscreenToDreamingTransitionViewModelTest : SysuiTestCase() { // Only three values should be present, since the dream overlay runs for a small // fraction of the overall animation time - assertThat(values.size).isEqualTo(5) + assertThat(values.size).isEqualTo(4) values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } } @@ -98,10 +98,10 @@ class LockscreenToDreamingTransitionViewModelTest : SysuiTestCase() { testScope = testScope, ) - assertThat(values.size).isEqualTo(6) + assertThat(values.size).isEqualTo(5) values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) } // Validate finished value - assertThat(values[5]).isEqualTo(0f) + assertThat(values[4]).isEqualTo(0f) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt index 274bde1ccfdf..a1b8aab402a7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt @@ -76,7 +76,7 @@ class LockscreenToOccludedTransitionViewModelTest : SysuiTestCase() { ) // Only 3 values should be present, since the dream overlay runs for a small fraction // of the overall animation time - assertThat(values.size).isEqualTo(5) + assertThat(values.size).isEqualTo(4) values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } } @@ -99,7 +99,7 @@ class LockscreenToOccludedTransitionViewModelTest : SysuiTestCase() { ), testScope = testScope, ) - assertThat(values.size).isEqualTo(5) + assertThat(values.size).isEqualTo(4) values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) } } @@ -121,11 +121,11 @@ class LockscreenToOccludedTransitionViewModelTest : SysuiTestCase() { ), testScope = testScope, ) - assertThat(values.size).isEqualTo(4) + assertThat(values.size).isEqualTo(3) values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) } // Cancel will reset the translation - assertThat(values[3]).isEqualTo(0) + assertThat(values[2]).isEqualTo(0) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt index d419d4a2534c..2111ad5d975e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt @@ -95,7 +95,7 @@ class OccludedToLockscreenTransitionViewModelTest : SysuiTestCase() { testScope, ) - assertThat(values.size).isEqualTo(5) + assertThat(values.size).isEqualTo(4) values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt index f027bc849e51..90b83620084c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt @@ -95,7 +95,7 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() { testScope, ) - assertThat(values.size).isEqualTo(3) + assertThat(values.size).isEqualTo(1) values.forEach { assertThat(it).isEqualTo(0f) } } @@ -107,7 +107,7 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() { keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED)) keyguardTransitionRepository.sendTransitionStep(step(1f)) - assertThat(values.size).isEqualTo(2) + assertThat(values.size).isEqualTo(1) values.forEach { assertThat(it).isEqualTo(0f) } } @@ -121,7 +121,7 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() { keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED)) keyguardTransitionRepository.sendTransitionStep(step(1f)) - assertThat(values.size).isEqualTo(2) + assertThat(values.size).isEqualTo(1) values.forEach { assertThat(it).isEqualTo(1f) } } @@ -135,7 +135,7 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() { keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED)) keyguardTransitionRepository.sendTransitionStep(step(1f)) - assertThat(values.size).isEqualTo(2) + assertThat(values.size).isEqualTo(1) values.forEach { assertThat(it).isEqualTo(1f) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt index 90779cb1c0b3..20653ca18efc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt @@ -55,6 +55,7 @@ class CustomTileInteractorTest : SysuiTestCase() { private val underTest: CustomTileInteractor = with(kosmos) { CustomTileInteractor( + tileSpec, customTileDefaultsRepository, customTileRepository, testScope.backgroundScope, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index efd4f9bdf449..530d127d17a9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -265,7 +265,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { powerInteractor = powerInteractor, bouncerInteractor = bouncerInteractor, simBouncerInteractor = dagger.Lazy { utils.simBouncerInteractor }, - authenticationInteractor = dagger.Lazy { utils.authenticationInteractor() } + authenticationInteractor = dagger.Lazy { utils.authenticationInteractor() }, + windowController = mock(), ) startable.start() 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 2e4986dd1a10..dd22976ee9e1 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 @@ -37,6 +37,7 @@ import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -46,18 +47,24 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.test.TestScope 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.anyBoolean +import org.mockito.Mock import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) @EnableFlags(AconfigFlags.FLAG_SCENE_CONTAINER) class SceneContainerStartableTest : SysuiTestCase() { + @Mock private lateinit var windowController: NotificationShadeWindowController + private val utils = SceneTestUtils(this) private val testScope = utils.testScope private val sceneInteractor = utils.sceneInteractor() @@ -77,22 +84,30 @@ class SceneContainerStartableTest : SysuiTestCase() { private val falsingCollector: FalsingCollector = mock() private val powerInteractor = PowerInteractorFactory.create().powerInteractor - private val underTest = - SceneContainerStartable( - applicationScope = testScope.backgroundScope, - sceneInteractor = sceneInteractor, - deviceEntryInteractor = deviceEntryInteractor, - keyguardInteractor = keyguardInteractor, - flags = sceneContainerFlags, - sysUiState = sysUiState, - displayId = Display.DEFAULT_DISPLAY, - sceneLogger = mock(), - falsingCollector = falsingCollector, - powerInteractor = powerInteractor, - bouncerInteractor = bouncerInteractor, - simBouncerInteractor = dagger.Lazy { utils.simBouncerInteractor }, - authenticationInteractor = dagger.Lazy { authenticationInteractor }, - ) + private lateinit var underTest: SceneContainerStartable + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + underTest = + SceneContainerStartable( + applicationScope = testScope.backgroundScope, + sceneInteractor = sceneInteractor, + deviceEntryInteractor = deviceEntryInteractor, + keyguardInteractor = keyguardInteractor, + flags = sceneContainerFlags, + sysUiState = sysUiState, + displayId = Display.DEFAULT_DISPLAY, + sceneLogger = mock(), + falsingCollector = falsingCollector, + powerInteractor = powerInteractor, + bouncerInteractor = bouncerInteractor, + simBouncerInteractor = dagger.Lazy { utils.simBouncerInteractor }, + authenticationInteractor = dagger.Lazy { authenticationInteractor }, + windowController = windowController, + ) + } @Test fun hydrateVisibility() = @@ -655,6 +670,58 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(currentSceneKey).isEqualTo(SceneKey.Gone) } + @Test + fun hydrateWindowFocus() = + testScope.runTest { + val currentDesiredSceneKey by + collectLastValue(sceneInteractor.desiredScene.map { it.key }) + val transitionStateFlow = + prepareState( + isDeviceUnlocked = true, + initialSceneKey = SceneKey.Gone, + ) + assertThat(currentDesiredSceneKey).isEqualTo(SceneKey.Gone) + verify(windowController, never()).setNotificationShadeFocusable(anyBoolean()) + + underTest.start() + runCurrent() + verify(windowController, times(1)).setNotificationShadeFocusable(false) + + sceneInteractor.changeScene(SceneModel(SceneKey.Shade), "reason") + transitionStateFlow.value = + ObservableTransitionState.Transition( + fromScene = SceneKey.Gone, + toScene = SceneKey.Shade, + progress = flowOf(0.5f), + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + runCurrent() + verify(windowController, times(1)).setNotificationShadeFocusable(false) + + sceneInteractor.onSceneChanged(SceneModel(SceneKey.Shade), "reason") + transitionStateFlow.value = ObservableTransitionState.Idle(SceneKey.Shade) + runCurrent() + verify(windowController, times(1)).setNotificationShadeFocusable(true) + + sceneInteractor.changeScene(SceneModel(SceneKey.Gone), "reason") + transitionStateFlow.value = + ObservableTransitionState.Transition( + fromScene = SceneKey.Shade, + toScene = SceneKey.Gone, + progress = flowOf(0.5f), + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + runCurrent() + verify(windowController, times(1)).setNotificationShadeFocusable(true) + + sceneInteractor.onSceneChanged(SceneModel(SceneKey.Gone), "reason") + transitionStateFlow.value = ObservableTransitionState.Idle(SceneKey.Gone) + runCurrent() + verify(windowController, times(2)).setNotificationShadeFocusable(false) + } + private fun TestScope.prepareState( isDeviceUnlocked: Boolean = false, isBypassEnabled: Boolean = false, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java index 53cb8a7eb81b..7a78b366dd7f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java @@ -25,15 +25,14 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import android.graphics.Point; import android.os.PowerManager; -import android.testing.AndroidTestingRunner; import android.testing.TestableLooper.RunWithLooper; import android.view.View; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.keyguard.KeyguardUpdateMonitor; @@ -68,7 +67,7 @@ import java.util.Collections; import java.util.HashSet; @SmallTest -@RunWith(AndroidTestingRunner.class) +@RunWith(AndroidJUnit4.class) @RunWithLooper(setAsMainLooper = true) public class DozeServiceHostTest extends SysuiTestCase { @@ -181,6 +180,7 @@ public class DozeServiceHostTest extends SysuiTestCase { DozeLog.PULSE_REASON_DOCKING, DozeLog.REASON_SENSOR_WAKE_UP_PRESENCE, DozeLog.REASON_SENSOR_QUICK_PICKUP, + DozeLog.PULSE_REASON_FINGERPRINT_ACTIVATED, DozeLog.REASON_SENSOR_TAP)); HashSet<Integer> reasonsThatDontPulse = new HashSet<>( Arrays.asList(DozeLog.REASON_SENSOR_PICKUP, @@ -232,7 +232,7 @@ public class DozeServiceHostTest extends SysuiTestCase { public void onSlpiTap_doesnt_pass_negative_values() { mDozeServiceHost.onSlpiTap(-1, 200); mDozeServiceHost.onSlpiTap(100, -2); - verifyZeroInteractions(mDozeInteractor); + verify(mDozeInteractor, never()).setLastTapToWakePosition(any()); } @Test public void dozeTimeTickSentToDozeInteractor() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorTest.kt new file mode 100644 index 000000000000..dbff63f355c8 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorTest.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 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.volume.panel.domain.interactor + +class ComponentsInteractorTest { + + // TODO(b/318080198) Write tests +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/DefaultComponentsLayoutManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/DefaultComponentsLayoutManagerTest.kt new file mode 100644 index 000000000000..e5fb9426e8e2 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/DefaultComponentsLayoutManagerTest.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 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.volume.panel.ui.viewmodel + +class DefaultComponentsLayoutManagerTest { + + // TODO(b/318080198) Write tests +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt new file mode 100644 index 000000000000..9795237a3e7e --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 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.volume.panel.ui.viewmodel + +class VolumePanelViewModelTest { + + // TODO(b/318080198) Write tests +} diff --git a/packages/SystemUI/res/values-am/strings.xml b/packages/SystemUI/res/values-am/strings.xml index 74e92ba8b14f..40fddc8b97f7 100644 --- a/packages/SystemUI/res/values-am/strings.xml +++ b/packages/SystemUI/res/values-am/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"በቅንብሮች ውስጥ ነባሪ የማስታወሻዎች መተግበሪያን ያቀናብሩ"</string> <string name="install_app" msgid="5066668100199613936">"መተግበሪያን ጫን"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"ወደ ውጫዊ ማሳያ ይንጸባረቅ?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"የውስጥ ማሳያዎ ይንጸባረቃል። የፊት ማሳያዎ ይጠፋል።"</string> <string name="mirror_display" msgid="2515262008898122928">"ማሳያን አንጸባርቅ"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"አሰናብት"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"ማሳያ ተገናኝቷል"</string> diff --git a/packages/SystemUI/res/values-ar/strings.xml b/packages/SystemUI/res/values-ar/strings.xml index 8aeeaf73bbb2..d19c77b72eff 100644 --- a/packages/SystemUI/res/values-ar/strings.xml +++ b/packages/SystemUI/res/values-ar/strings.xml @@ -330,12 +330,9 @@ <string name="quick_settings_screen_record_label" msgid="8650355346742003694">"تسجيل الشاشة"</string> <string name="quick_settings_screen_record_start" msgid="1574725369331638985">"بدء"</string> <string name="quick_settings_screen_record_stop" msgid="8087348522976412119">"إيقاف"</string> - <!-- no translation found for qs_record_issue_label (8166290137285529059) --> - <skip /> - <!-- no translation found for qs_record_issue_start (2979831312582567056) --> - <skip /> - <!-- no translation found for qs_record_issue_stop (3531747965741982657) --> - <skip /> + <string name="qs_record_issue_label" msgid="8166290137285529059">"تسجيل المشكلة"</string> + <string name="qs_record_issue_start" msgid="2979831312582567056">"بدء"</string> + <string name="qs_record_issue_stop" msgid="3531747965741982657">"إيقاف"</string> <string name="qs_record_issue_dropdown_header" msgid="5995983175678658329">"ما هو الجانب الذي تأثّر في تجربة استخدام الجهاز؟"</string> <string name="qs_record_issue_dropdown_prompt" msgid="2526949919167046219">"اختيار نوع المشكلة"</string> <string name="qs_record_issue_dropdown_screenrecord" msgid="6396141928484257626">"تسجيل الشاشة"</string> @@ -416,12 +413,9 @@ <string name="keyguard_indication_charging_time_dock" msgid="3149328898931741271">"<xliff:g id="PERCENTAGE">%2$s</xliff:g> • جارٍ الشحن • ستمتلئ البطارية خلال <xliff:g id="CHARGING_TIME_LEFT">%1$s</xliff:g>"</string> <string name="communal_tutorial_indicator_text" msgid="4503010353591430123">"مرِّر سريعًا لليمين لبدء الدليل التوجيهي العام."</string> <string name="button_to_open_widget_editor" msgid="5599945944349057600">"فتح محرِّر التطبيقات المصغّرة"</string> - <!-- no translation found for button_to_remove_widget (3948204829181214098) --> - <skip /> - <!-- no translation found for hub_mode_add_widget_button_text (4831464661209971729) --> - <skip /> - <!-- no translation found for hub_mode_editing_exit_button_text (3704686734192264771) --> - <skip /> + <string name="button_to_remove_widget" msgid="3948204829181214098">"إزالة"</string> + <string name="hub_mode_add_widget_button_text" msgid="4831464661209971729">"إضافة تطبيق مصغّر"</string> + <string name="hub_mode_editing_exit_button_text" msgid="3704686734192264771">"تم"</string> <string name="accessibility_multi_user_switch_switcher" msgid="5330448341251092660">"تبديل المستخدم"</string> <string name="accessibility_multi_user_list_switcher" msgid="8574105376229857407">"القائمة المنسدلة"</string> <string name="guest_exit_guest_dialog_message" msgid="8183450985628495709">"سيتم حذف كل التطبيقات والبيانات في هذه الجلسة."</string> @@ -1214,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"يمكنك ضبط تطبيق تدوين الملاحظات التلقائي في \"الإعدادات\"."</string> <string name="install_app" msgid="5066668100199613936">"تثبيت التطبيق"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"هل تريد بث محتوى جهازك على الشاشة الخارجية؟"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"سيتم النسخ المطابق لمحتوى الشاشة الداخلية، وإيقاف الشاشة الأمامية."</string> <string name="mirror_display" msgid="2515262008898122928">"بث المحتوى على الشاشة"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"إغلاق"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"تم توصيل الشاشة"</string> diff --git a/packages/SystemUI/res/values-as/strings.xml b/packages/SystemUI/res/values-as/strings.xml index a1b538934236..4e78f55663df 100644 --- a/packages/SystemUI/res/values-as/strings.xml +++ b/packages/SystemUI/res/values-as/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"ছেটিঙত টোকাৰ ডিফ’ল্ট এপ্ ছেট কৰক"</string> <string name="install_app" msgid="5066668100199613936">"এপ্টো ইনষ্টল কৰক"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"বাহ্যিক ডিছপ্লে’লৈ মিৰ’ৰ কৰিবনে?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"আপোনাৰ ইনাৰ ডিছপ্লে’ প্ৰতিবিম্বিত কৰা হ’ব। আপোনাৰ ফ্ৰণ্ট ডিছপ্লে’ অফ কৰা হ’ব।"</string> <string name="mirror_display" msgid="2515262008898122928">"ডিছপ্লে’ মিৰ’ৰ কৰক"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"অগ্ৰাহ্য কৰক"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"ডিছপ্লে’ সংযোগ কৰা হৈছে"</string> diff --git a/packages/SystemUI/res/values-az/strings.xml b/packages/SystemUI/res/values-az/strings.xml index 2a9a9cfe2624..bf32c5e6eb0a 100644 --- a/packages/SystemUI/res/values-az/strings.xml +++ b/packages/SystemUI/res/values-az/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Ayarlarda defolt qeydlər tətbiqi ayarlayın"</string> <string name="install_app" msgid="5066668100199613936">"Tətbiqi quraşdırın"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Xarici displeyə əks etdirilsin?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"İç displey əks etdiriləcək. Ön ekran deaktiv ediləcək."</string> <string name="mirror_display" msgid="2515262008898122928">"Displeyi əks etdirin"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"İmtina edin"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Displey qoşulub"</string> diff --git a/packages/SystemUI/res/values-b+sr+Latn/strings.xml b/packages/SystemUI/res/values-b+sr+Latn/strings.xml index 47468ffb033d..56c3e45b4726 100644 --- a/packages/SystemUI/res/values-b+sr+Latn/strings.xml +++ b/packages/SystemUI/res/values-b+sr+Latn/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Podesite podrazumevanu aplikaciju za beleške u Podešavanjima"</string> <string name="install_app" msgid="5066668100199613936">"Instaliraj aplikaciju"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Želite li da preslikate na spoljnji ekran?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Unutrašnji ekran će se preslikati. Prednji ekran će se isključiti."</string> <string name="mirror_display" msgid="2515262008898122928">"Preslikaj ekran"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Odbaci"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Ekran je povezan"</string> diff --git a/packages/SystemUI/res/values-bg/strings.xml b/packages/SystemUI/res/values-bg/strings.xml index 1fe96a674e54..22d167ee50db 100644 --- a/packages/SystemUI/res/values-bg/strings.xml +++ b/packages/SystemUI/res/values-bg/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Задайте стандартно приложение за бележки от настройките"</string> <string name="install_app" msgid="5066668100199613936">"Инсталиране на приложението"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Да се дублира ли на външния екран?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Съдържанието на вътрешния ви дисплей ще бъде дублирано. Предният ви дисплей ще бъде изключен."</string> <string name="mirror_display" msgid="2515262008898122928">"Дублиране на дисплея"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Отхвърляне"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Свързан е екран"</string> diff --git a/packages/SystemUI/res/values-bn/strings.xml b/packages/SystemUI/res/values-bn/strings.xml index fa301313dc81..045af935be6c 100644 --- a/packages/SystemUI/res/values-bn/strings.xml +++ b/packages/SystemUI/res/values-bn/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"\'সেটিংস\' থেকে ডিফল্ট নোট নেওয়ার অ্যাপ সেট করুন"</string> <string name="install_app" msgid="5066668100199613936">"অ্যাপ ইনস্টল করুন"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"এক্সটার্নাল ডিসপ্লেতে মিরর করবেন?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"আপনার ইনার ডিসপ্লে মিরর করা হবে। আপনার ফ্রন্ট ডিসপ্লে বন্ধ করা হবে।"</string> <string name="mirror_display" msgid="2515262008898122928">"ডিসপ্লে দেখান"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"বাতিল করুন"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"ডিসপ্লে কানেক্ট করা আছে"</string> diff --git a/packages/SystemUI/res/values-bs/strings.xml b/packages/SystemUI/res/values-bs/strings.xml index db5987fb869a..ae962ce7ead5 100644 --- a/packages/SystemUI/res/values-bs/strings.xml +++ b/packages/SystemUI/res/values-bs/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Postavite zadanu aplikaciju za bilješke u Postavkama"</string> <string name="install_app" msgid="5066668100199613936">"Instaliraj aplikaciju"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Preslikati na vanjski ekran?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Unutrašnji ekran će se preslikavati. Prednji ekran će se isključiti."</string> <string name="mirror_display" msgid="2515262008898122928">"Preslikaj ekran"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Odbaci"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Ekran je povezan"</string> diff --git a/packages/SystemUI/res/values-ca/strings.xml b/packages/SystemUI/res/values-ca/strings.xml index af4492bbb6b3..8cf88282d646 100644 --- a/packages/SystemUI/res/values-ca/strings.xml +++ b/packages/SystemUI/res/values-ca/strings.xml @@ -1207,10 +1207,9 @@ <string name="assistant_attention_content_description" msgid="4166330881435263596">"S\'ha detectat la presència d\'usuaris"</string> <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Defineix l\'aplicació de notes predeterminada a Configuració"</string> <string name="install_app" msgid="5066668100199613936">"Instal·la l\'aplicació"</string> - <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Replicar a la pantalla externa?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> - <string name="mirror_display" msgid="2515262008898122928">"Replica la pantalla"</string> + <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Duplicar a la pantalla externa?"</string> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"La pantalla interior es duplicarà. La pantalla frontal es desactivarà."</string> + <string name="mirror_display" msgid="2515262008898122928">"Duplica la pantalla"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Ignora"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Pantalla connectada"</string> <string name="privacy_dialog_title" msgid="7839968133469098311">"Micròfon i càmera"</string> diff --git a/packages/SystemUI/res/values-cs/strings.xml b/packages/SystemUI/res/values-cs/strings.xml index 25fcf99ffd21..17084dc4c543 100644 --- a/packages/SystemUI/res/values-cs/strings.xml +++ b/packages/SystemUI/res/values-cs/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Výchozí aplikaci pro poznámky nastavíte v Nastavení"</string> <string name="install_app" msgid="5066668100199613936">"Nainstalovat aplikaci"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Zrcadlit na externí displej?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Vnitřní displej bude zrcadlen. Přední displej bude vypnutý."</string> <string name="mirror_display" msgid="2515262008898122928">"Zrcadlit displej"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Zavřít"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Displej připojen"</string> diff --git a/packages/SystemUI/res/values-da/strings.xml b/packages/SystemUI/res/values-da/strings.xml index 91c223a7ed50..41abea3a51c8 100644 --- a/packages/SystemUI/res/values-da/strings.xml +++ b/packages/SystemUI/res/values-da/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Angiv standardapp til noter i Indstillinger"</string> <string name="install_app" msgid="5066668100199613936">"Installer app"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Vil du spejle til ekstern skærm?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Din indre skærm spejles. Din skærm på forsiden slukkes."</string> <string name="mirror_display" msgid="2515262008898122928">"Spejl skærm"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Luk"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Skærmen er tilsluttet"</string> diff --git a/packages/SystemUI/res/values-de/strings.xml b/packages/SystemUI/res/values-de/strings.xml index 29b70184b82b..d95e229c2e9b 100644 --- a/packages/SystemUI/res/values-de/strings.xml +++ b/packages/SystemUI/res/values-de/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Standard-Notizen-App in den Einstellungen einrichten"</string> <string name="install_app" msgid="5066668100199613936">"App installieren"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Auf externen Bildschirm spiegeln?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Dein inneres Display wird gespiegelt. Das Frontdisplay wird ausgeschaltet."</string> <string name="mirror_display" msgid="2515262008898122928">"Bildschirm spiegeln"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Schließen"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Bildschirm verbunden"</string> diff --git a/packages/SystemUI/res/values-el/strings.xml b/packages/SystemUI/res/values-el/strings.xml index dd24ca678f1a..5848e4ff460a 100644 --- a/packages/SystemUI/res/values-el/strings.xml +++ b/packages/SystemUI/res/values-el/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Ορίστε την προεπιλεγμένη εφαρμογή σημειώσεων στις Ρυθμίσεις"</string> <string name="install_app" msgid="5066668100199613936">"Εγκατάσταση εφαρμογής"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Κατοπτρισμός σε εξωτερική οθόνη;"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Θα γίνει κατοπτρισμός της εσωτερικής προβολής. Η μπροστινή οθόνη θα απενεργοποιηθεί."</string> <string name="mirror_display" msgid="2515262008898122928">"Κατοπτρισμός οθόνης"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Παράβλεψη"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Η οθόνη είναι συνδεδεμένη"</string> diff --git a/packages/SystemUI/res/values-en-rAU/strings.xml b/packages/SystemUI/res/values-en-rAU/strings.xml index 2d052919c5b1..870e4dd499b3 100644 --- a/packages/SystemUI/res/values-en-rAU/strings.xml +++ b/packages/SystemUI/res/values-en-rAU/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Set default notes app in Settings"</string> <string name="install_app" msgid="5066668100199613936">"Install app"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Mirror to external display?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Your inner display will be mirrored. Your front display will be turned off."</string> <string name="mirror_display" msgid="2515262008898122928">"Mirror display"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Dismiss"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Display connected"</string> diff --git a/packages/SystemUI/res/values-en-rCA/strings.xml b/packages/SystemUI/res/values-en-rCA/strings.xml index c043b2c50972..f25baf286ba0 100644 --- a/packages/SystemUI/res/values-en-rCA/strings.xml +++ b/packages/SystemUI/res/values-en-rCA/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Set default notes app in Settings"</string> <string name="install_app" msgid="5066668100199613936">"Install app"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Mirror to external display?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Your inner display will be mirrored. Your front display will be turned off."</string> <string name="mirror_display" msgid="2515262008898122928">"Mirror display"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Dismiss"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Display connected"</string> diff --git a/packages/SystemUI/res/values-en-rGB/strings.xml b/packages/SystemUI/res/values-en-rGB/strings.xml index 2d052919c5b1..870e4dd499b3 100644 --- a/packages/SystemUI/res/values-en-rGB/strings.xml +++ b/packages/SystemUI/res/values-en-rGB/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Set default notes app in Settings"</string> <string name="install_app" msgid="5066668100199613936">"Install app"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Mirror to external display?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Your inner display will be mirrored. Your front display will be turned off."</string> <string name="mirror_display" msgid="2515262008898122928">"Mirror display"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Dismiss"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Display connected"</string> diff --git a/packages/SystemUI/res/values-en-rIN/strings.xml b/packages/SystemUI/res/values-en-rIN/strings.xml index 2d052919c5b1..870e4dd499b3 100644 --- a/packages/SystemUI/res/values-en-rIN/strings.xml +++ b/packages/SystemUI/res/values-en-rIN/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Set default notes app in Settings"</string> <string name="install_app" msgid="5066668100199613936">"Install app"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Mirror to external display?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Your inner display will be mirrored. Your front display will be turned off."</string> <string name="mirror_display" msgid="2515262008898122928">"Mirror display"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Dismiss"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Display connected"</string> diff --git a/packages/SystemUI/res/values-en-rXC/strings.xml b/packages/SystemUI/res/values-en-rXC/strings.xml index 3691b2c3aaf0..b3ed7145c9b8 100644 --- a/packages/SystemUI/res/values-en-rXC/strings.xml +++ b/packages/SystemUI/res/values-en-rXC/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Set default notes app in Settings"</string> <string name="install_app" msgid="5066668100199613936">"Install app"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Mirror to external display?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Your inner display will be mirrored. Your front display will be turned off."</string> <string name="mirror_display" msgid="2515262008898122928">"Mirror display"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Dismiss"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Display connected"</string> diff --git a/packages/SystemUI/res/values-es-rUS/strings.xml b/packages/SystemUI/res/values-es-rUS/strings.xml index 6a4818e1f3e1..c5355605fe17 100644 --- a/packages/SystemUI/res/values-es-rUS/strings.xml +++ b/packages/SystemUI/res/values-es-rUS/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Configura la app de notas predeterminada en Configuración"</string> <string name="install_app" msgid="5066668100199613936">"Instalar app"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"¿Quieres duplicar en la pantalla externa?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Se duplicará la pantalla interior. Se apagará la pantalla frontal."</string> <string name="mirror_display" msgid="2515262008898122928">"Duplicar pantalla"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Descartar"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Pantalla conectada"</string> diff --git a/packages/SystemUI/res/values-es/strings.xml b/packages/SystemUI/res/values-es/strings.xml index 4e41db3ef4b1..1157ff1933ba 100644 --- a/packages/SystemUI/res/values-es/strings.xml +++ b/packages/SystemUI/res/values-es/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Configura la aplicación de notas predeterminada en Ajustes"</string> <string name="install_app" msgid="5066668100199613936">"Instalar aplicación"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"¿Proyectar a pantalla externa?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Se proyectará tu pantalla interior. Se apagará tu pantalla frontal."</string> <string name="mirror_display" msgid="2515262008898122928">"Proyectar pantalla"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Cerrar"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Pantalla conectada"</string> diff --git a/packages/SystemUI/res/values-et/strings.xml b/packages/SystemUI/res/values-et/strings.xml index 231ddebb7b8a..36b3f1e219e2 100644 --- a/packages/SystemUI/res/values-et/strings.xml +++ b/packages/SystemUI/res/values-et/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Määrake seadetes märkmete vaikerakendus."</string> <string name="install_app" msgid="5066668100199613936">"Installi rakendus"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Kas peegeldada välisekraanile?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Teie siseekraani peegeldatakse. Teie esiekraan lülitatakse välja."</string> <string name="mirror_display" msgid="2515262008898122928">"Peegelda ekraani"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Loobu"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Kuvar on ühendatud"</string> diff --git a/packages/SystemUI/res/values-eu/strings.xml b/packages/SystemUI/res/values-eu/strings.xml index 20bf71b0a6a3..5ea02d173fcb 100644 --- a/packages/SystemUI/res/values-eu/strings.xml +++ b/packages/SystemUI/res/values-eu/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Ezarri oharren aplikazio lehenetsia ezarpenetan"</string> <string name="install_app" msgid="5066668100199613936">"Instalatu aplikazioa"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Kanpoko pantailan islatu nahi duzu?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Barneko pantaila islatuko da. Aurreko pantaila desaktibatu egingo da."</string> <string name="mirror_display" msgid="2515262008898122928">"Islatu pantaila"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Baztertu"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Konektatutako pantaila"</string> diff --git a/packages/SystemUI/res/values-fa/strings.xml b/packages/SystemUI/res/values-fa/strings.xml index e161d7adb35f..aa77b4b4ddb7 100644 --- a/packages/SystemUI/res/values-fa/strings.xml +++ b/packages/SystemUI/res/values-fa/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"برنامه پیشفرض یادداشت را در «تنظیمات» تنظیم کنید"</string> <string name="install_app" msgid="5066668100199613936">"نصب برنامه"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"روی نمایشگر خارجی قرینهسازی شود؟"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"نمایشگر داخلی شما قرینهسازی میشود. نمایشگر جلو خاموش میشود."</string> <string name="mirror_display" msgid="2515262008898122928">"قرینهسازی نمایشگر"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"بستن"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"نمایشگر متصل شد"</string> diff --git a/packages/SystemUI/res/values-fi/strings.xml b/packages/SystemUI/res/values-fi/strings.xml index 240607b5ebba..882c42c3f848 100644 --- a/packages/SystemUI/res/values-fi/strings.xml +++ b/packages/SystemUI/res/values-fi/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Aseta oletusmuistiinpanosovellus Asetuksista"</string> <string name="install_app" msgid="5066668100199613936">"Asenna sovellus"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Peilataanko ulkoiselle näytölle?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Sisänäyttö peilataan. Etunäyttö laitetaan pois päältä."</string> <string name="mirror_display" msgid="2515262008898122928">"Peilaa näyttö"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Ohita"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Näyttö yhdistetty"</string> diff --git a/packages/SystemUI/res/values-fr-rCA/strings.xml b/packages/SystemUI/res/values-fr-rCA/strings.xml index 82a56f5511ca..8370b01ccfd5 100644 --- a/packages/SystemUI/res/values-fr-rCA/strings.xml +++ b/packages/SystemUI/res/values-fr-rCA/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Définir l\'application de prise de notes par défaut dans les Paramètres"</string> <string name="install_app" msgid="5066668100199613936">"Installer l\'application"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Dupliquer l\'écran sur un moniteur externe?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Votre écran intérieur sera dupliqué. Votre écran frontal sera désactivé."</string> <string name="mirror_display" msgid="2515262008898122928">"Dupliquer l\'écran"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Fermer"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Écran connecté"</string> diff --git a/packages/SystemUI/res/values-fr/strings.xml b/packages/SystemUI/res/values-fr/strings.xml index c35f9eeaadff..af81a31002a4 100644 --- a/packages/SystemUI/res/values-fr/strings.xml +++ b/packages/SystemUI/res/values-fr/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Définir une appli de notes par défaut dans les paramètres"</string> <string name="install_app" msgid="5066668100199613936">"Installer l\'appli"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Dupliquer sur l\'écran externe ?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Votre écran intérieur sera dupliqué. Votre écran frontal sera éteint."</string> <string name="mirror_display" msgid="2515262008898122928">"Dupliquer l\'écran"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Fermer"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Écran connecté"</string> diff --git a/packages/SystemUI/res/values-gl/strings.xml b/packages/SystemUI/res/values-gl/strings.xml index 9cf968e883c3..ef0751b8f0c4 100644 --- a/packages/SystemUI/res/values-gl/strings.xml +++ b/packages/SystemUI/res/values-gl/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Establece a aplicación de notas predeterminada en Configuración"</string> <string name="install_app" msgid="5066668100199613936">"Instalar aplicación"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Queres proxectar contido nunha pantalla externa?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Proxectarase a pantalla interior. Desactivarase a pantalla frontal."</string> <string name="mirror_display" msgid="2515262008898122928">"Proxectar pantalla"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Pechar"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Pantalla conectada"</string> diff --git a/packages/SystemUI/res/values-gu/strings.xml b/packages/SystemUI/res/values-gu/strings.xml index df7b203a0deb..d92231c9bf93 100644 --- a/packages/SystemUI/res/values-gu/strings.xml +++ b/packages/SystemUI/res/values-gu/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"સેટિંગમાં નોંધની ડિફૉલ્ટ ઍપ સેટ કરો"</string> <string name="install_app" msgid="5066668100199613936">"ઍપ ઇન્સ્ટૉલ કરો"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"શું બાહ્ય ડિસ્પ્લે પર મિરર કરીએ?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"તમારું ઇનર ડિસ્પ્લે મિરર કરવામાં આવશે. તમારું ફ્રન્ટ ડિસ્પ્લે બંધ કરવામાં આવશે."</string> <string name="mirror_display" msgid="2515262008898122928">"મિરર ડિસ્પ્લે"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"છોડી દો"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Display કનેક્ટેડ છે"</string> diff --git a/packages/SystemUI/res/values-hi/strings.xml b/packages/SystemUI/res/values-hi/strings.xml index eec3078af9fb..f8c78a908433 100644 --- a/packages/SystemUI/res/values-hi/strings.xml +++ b/packages/SystemUI/res/values-hi/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"सेटिंग में जाकर, नोट लेने की सुविधा देने वाले ऐप्लिकेशन को डिफ़ॉल्ट के तौर पर सेट करें"</string> <string name="install_app" msgid="5066668100199613936">"ऐप्लिकेशन इंस्टॉल करें"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"क्या आपको किसी बाहरी डिवाइस पर डिसप्ले करना है?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"आपके फ़ोन के इनर डिसप्ले की स्क्रीन शेयर की जाएगी. फ़्रंट डिसप्ले को बंद कर दिया जाएगा."</string> <string name="mirror_display" msgid="2515262008898122928">"डिसप्ले करें"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"खारिज करें"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"डिसप्ले कनेक्ट किया गया"</string> diff --git a/packages/SystemUI/res/values-hr/strings.xml b/packages/SystemUI/res/values-hr/strings.xml index f0ed40036156..0803aeb7d1a3 100644 --- a/packages/SystemUI/res/values-hr/strings.xml +++ b/packages/SystemUI/res/values-hr/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Postavite zadanu aplikaciju za bilješke u postavkama"</string> <string name="install_app" msgid="5066668100199613936">"Instalacija"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Želite li zrcaliti na vanjski zaslon?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Unutarnji zaslon bit će zrcaljen. Prednji zaslon bit će isključen."</string> <string name="mirror_display" msgid="2515262008898122928">"Zrcaljenje zaslona"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Odbaci"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Zaslon je povezan"</string> diff --git a/packages/SystemUI/res/values-hu/strings.xml b/packages/SystemUI/res/values-hu/strings.xml index 30d296190f6a..fd9d832ceae0 100644 --- a/packages/SystemUI/res/values-hu/strings.xml +++ b/packages/SystemUI/res/values-hu/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Állítson be alapértelmezett jegyzetkészítő alkalmazást a Beállításokban"</string> <string name="install_app" msgid="5066668100199613936">"Alkalmazás telepítése"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Tükrözi a kijelzőt a külső képernyőre?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"A belső kijelző tükrözve lesz. Az elülső kijelző ki lesz kapcsolva."</string> <string name="mirror_display" msgid="2515262008898122928">"Kijelző tükrözése"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Elvetés"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Kijelző csatlakoztatva"</string> diff --git a/packages/SystemUI/res/values-hy/strings.xml b/packages/SystemUI/res/values-hy/strings.xml index bbdeb1683c64..a9252e2e1eda 100644 --- a/packages/SystemUI/res/values-hy/strings.xml +++ b/packages/SystemUI/res/values-hy/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Կարգավորեք նշումների կանխադրված հավելված Կարգավորումներում"</string> <string name="install_app" msgid="5066668100199613936">"Տեղադրել հավելվածը"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Հայելապատճենե՞լ արտաքին էկրանին"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Ներքին էկրանը կհայելապատճենվի։ Առջևի էկրանը կանջատվի։"</string> <string name="mirror_display" msgid="2515262008898122928">"Հայելապատճենել էկրանը"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Փակել"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Էկրանը միացած է"</string> diff --git a/packages/SystemUI/res/values-in/strings.xml b/packages/SystemUI/res/values-in/strings.xml index f2ed0e7f9159..8ef809a04b6e 100644 --- a/packages/SystemUI/res/values-in/strings.xml +++ b/packages/SystemUI/res/values-in/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Setel aplikasi catatan default di Setelan"</string> <string name="install_app" msgid="5066668100199613936">"Instal aplikasi"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Cerminkan ke layar eksternal?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Layar dalam akan dicerminkan. Layar depan akan dinonaktifkan."</string> <string name="mirror_display" msgid="2515262008898122928">"Cerminkan layar"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Tutup"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Layar terhubung"</string> diff --git a/packages/SystemUI/res/values-is/strings.xml b/packages/SystemUI/res/values-is/strings.xml index f1f1b0f5cbfc..cef3285e3558 100644 --- a/packages/SystemUI/res/values-is/strings.xml +++ b/packages/SystemUI/res/values-is/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Stilltu sjálfgefið glósuforrit í stillingunum"</string> <string name="install_app" msgid="5066668100199613936">"Setja upp forrit"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Spegla yfir á ytri skjá?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Innri skjárinn þinn verður speglaður. Slökkt verður á framskjánum þínum."</string> <string name="mirror_display" msgid="2515262008898122928">"Spegla skjá"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Hunsa"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Skjár tengdur"</string> diff --git a/packages/SystemUI/res/values-it/strings.xml b/packages/SystemUI/res/values-it/strings.xml index 223e39faef37..c5079fed39b6 100644 --- a/packages/SystemUI/res/values-it/strings.xml +++ b/packages/SystemUI/res/values-it/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Imposta l\'app per le note predefinita nelle Impostazioni"</string> <string name="install_app" msgid="5066668100199613936">"Installa app"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Vuoi eseguire il mirroring al display esterno?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Verrà eseguito il mirroring del tuo display interno. Il tuo display frontale verrà spento."</string> <string name="mirror_display" msgid="2515262008898122928">"Esegui il mirroring del display"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Chiudi"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Display collegato"</string> diff --git a/packages/SystemUI/res/values-iw/strings.xml b/packages/SystemUI/res/values-iw/strings.xml index 44fd608df981..486b22f9d222 100644 --- a/packages/SystemUI/res/values-iw/strings.xml +++ b/packages/SystemUI/res/values-iw/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"צריך להגדיר את אפליקציית ברירת המחדל לפתקים ב\'הגדרות\'"</string> <string name="install_app" msgid="5066668100199613936">"התקנת האפליקציה"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"לשקף למסך חיצוני?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"המסך הפנימי שלך ישוכפל. המסך החיצוני שלך יכובה."</string> <string name="mirror_display" msgid="2515262008898122928">"תצוגת מראה"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"סגירה"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"המסך מחובר"</string> diff --git a/packages/SystemUI/res/values-ja/strings.xml b/packages/SystemUI/res/values-ja/strings.xml index 38f2dc86e94c..c742f939466e 100644 --- a/packages/SystemUI/res/values-ja/strings.xml +++ b/packages/SystemUI/res/values-ja/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"[設定] でデフォルトのメモアプリを設定してください"</string> <string name="install_app" msgid="5066668100199613936">"アプリをインストール"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"外部ディスプレイにミラーリングしますか?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"インナー ディスプレイがミラーリングされます。フロント ディスプレイは OFF になります。"</string> <string name="mirror_display" msgid="2515262008898122928">"ディスプレイをミラーリングする"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"閉じる"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"ディスプレイに接続しました"</string> diff --git a/packages/SystemUI/res/values-ka/strings.xml b/packages/SystemUI/res/values-ka/strings.xml index bb21f3f9f75f..9d386efadd02 100644 --- a/packages/SystemUI/res/values-ka/strings.xml +++ b/packages/SystemUI/res/values-ka/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"დააყენეთ ნაგულისხმევი შენიშვნების აპი პარამეტრებში"</string> <string name="install_app" msgid="5066668100199613936">"აპის ინსტალაცია"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"აირეკლოს გარე ეკრანზე?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"თქვენი შიდა ეკრანი აირეკლება. თქვენი წინა ეკრანი გამოირთვება."</string> <string name="mirror_display" msgid="2515262008898122928">"ეკრანის არეკვლა"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"დახურვა"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"ეკრანი დაკავშირებულია"</string> diff --git a/packages/SystemUI/res/values-kk/strings.xml b/packages/SystemUI/res/values-kk/strings.xml index b39d5a553b86..d2c60f157f98 100644 --- a/packages/SystemUI/res/values-kk/strings.xml +++ b/packages/SystemUI/res/values-kk/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Параметрлерден әдепкі жазба қолданбасын орнатыңыз."</string> <string name="install_app" msgid="5066668100199613936">"Қолданбаны орнату"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Сыртқы экран арқылы да көрсету керек пе?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Ішкі экран көшірмесі көрсетіледі. Алдыңғы экран өшіріледі."</string> <string name="mirror_display" msgid="2515262008898122928">"Көрсету"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Жабу"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Дисплей қосылды"</string> diff --git a/packages/SystemUI/res/values-km/strings.xml b/packages/SystemUI/res/values-km/strings.xml index e989a9d8b242..ec81523389b5 100644 --- a/packages/SystemUI/res/values-km/strings.xml +++ b/packages/SystemUI/res/values-km/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"កំណត់កម្មវិធីកំណត់ចំណាំលំនាំដើមនៅក្នុងការកំណត់"</string> <string name="install_app" msgid="5066668100199613936">"ដំឡើងកម្មវិធី"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"បញ្ចាំងទៅផ្ទាំងអេក្រង់ខាងក្រៅឬ?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"អេក្រង់ខាងក្នុងរបស់អ្នកនឹងត្រូវបានធ្វើសមកាលកម្មទៅវិញទៅមក។ អេក្រង់មុខរបស់អ្នកនឹងត្រូវបានបិទ។"</string> <string name="mirror_display" msgid="2515262008898122928">"បញ្ចាំងទៅផ្ទាំងអេក្រង់"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"ច្រានចោល"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"ផ្ទាំងអេក្រង់ត្រូវបានភ្ជាប់"</string> diff --git a/packages/SystemUI/res/values-kn/strings.xml b/packages/SystemUI/res/values-kn/strings.xml index 39b14685cbca..33f45289ee22 100644 --- a/packages/SystemUI/res/values-kn/strings.xml +++ b/packages/SystemUI/res/values-kn/strings.xml @@ -93,7 +93,7 @@ <string name="screenshot_bottom_boundary_pct" msgid="3880821519814946478">"ಕೆಳಗಿನ ಬೌಂಡರಿ ಶೇಕಡಾ <xliff:g id="PERCENT">%1$d</xliff:g>"</string> <string name="screenshot_left_boundary_pct" msgid="8502323556112287469">"ಎಡಭಾಗದ ಬೌಂಡರಿ ಶೇಕಡಾ <xliff:g id="PERCENT">%1$d</xliff:g>"</string> <string name="screenshot_right_boundary_pct" msgid="1201150713021779321">"ಬಲಭಾಗದ ಬೌಂಡರಿ ಶೇಕಡಾ <xliff:g id="PERCENT">%1$d</xliff:g>"</string> - <string name="screenshot_work_profile_notification" msgid="203041724052970693">"ಕೆಲಸದ ಪ್ರೊಫೈಲ್ನಲ್ಲಿನ <xliff:g id="APP">%1$s</xliff:g> ನಲ್ಲಿ ಉಳಿಸಲಾಗಿದೆ"</string> + <string name="screenshot_work_profile_notification" msgid="203041724052970693">"ಕೆಲಸದ ಪ್ರೊಫೈಲ್ನಲ್ಲಿನ <xliff:g id="APP">%1$s</xliff:g> ನಲ್ಲಿ ಸೇವ್ ಮಾಡಲಾಗಿದೆ"</string> <string name="screenshot_default_files_app_name" msgid="8721579578575161912">"ಫೈಲ್ಗಳು"</string> <string name="screenshot_detected_template" msgid="7940376642921719915">"ಈ ಸ್ಕ್ರೀನ್ಶಾಟ್ ಅನ್ನು <xliff:g id="APPNAME">%1$s</xliff:g> ಪತ್ತೆಹಚ್ಚಿದೆ."</string> <string name="screenshot_detected_multiple_template" msgid="7644827792093819241">"<xliff:g id="APPNAME">%1$s</xliff:g> ಹಾಗೂ ತೆರೆದಿರುವ ಇತರ ಆ್ಯಪ್ಗಳು ಈ ಸ್ಕ್ರೀನ್ಶಾಟ್ ಅನ್ನು ಪತ್ತೆಹಚ್ಚಿವೆ."</string> @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"ಸೆಟ್ಟಿಂಗ್ಗಳಲ್ಲಿ ಡೀಫಾಲ್ಟ್ ಟಿಪ್ಪಣಿಗಳ ಆ್ಯಪ್ ಅನ್ನು ಸೆಟ್ ಮಾಡಿ"</string> <string name="install_app" msgid="5066668100199613936">"ಆ್ಯಪ್ ಇನ್ಸ್ಟಾಲ್ ಮಾಡಿ"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"ಬಾಹ್ಯ ಡಿಸ್ಪ್ಲೇಗೆ ಪ್ರತಿಬಿಂಬಿಸಬೇಕೆ?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"ನಿಮ್ಮ ಒಳಗಿನ ಡಿಸ್ಪ್ಲೇ ಅನ್ನು ಪ್ರತಿಬಿಂಬಿಸಲಾಗುತ್ತದೆ. ನಿಮ್ಮ ಫ್ರಂಟ್ ಡಿಸ್ಪ್ಲೇ ಅನ್ನು ಆಫ್ ಮಾಡಲಾಗುತ್ತದೆ."</string> <string name="mirror_display" msgid="2515262008898122928">"ಮಿರರ್ ಡಿಸ್ಪ್ಲೇ"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"ವಜಾಗೊಳಿಸಿ"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"ಡಿಸ್ಪ್ಲೇ ಕನೆಕ್ಟ್ ಆಗಿದೆ"</string> diff --git a/packages/SystemUI/res/values-kn/tiles_states_strings.xml b/packages/SystemUI/res/values-kn/tiles_states_strings.xml index 16e82eacdd63..876562dd8584 100644 --- a/packages/SystemUI/res/values-kn/tiles_states_strings.xml +++ b/packages/SystemUI/res/values-kn/tiles_states_strings.xml @@ -33,147 +33,147 @@ <!-- no translation found for tile_states_default:2 (9192445505551219506) --> <string-array name="tile_states_internet"> <item msgid="5499482407653291407">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="3048856902433862868">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="3048856902433862868">"ಆಫ್"</item> <item msgid="6877982264300789870">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_wifi"> <item msgid="8054147400538405410">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="4293012229142257455">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="4293012229142257455">"ಆಫ್"</item> <item msgid="6221288736127914861">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_cell"> <item msgid="1235899788959500719">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="2074416252859094119">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="2074416252859094119">"ಆಫ್"</item> <item msgid="287997784730044767">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_battery"> <item msgid="6311253873330062961">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="7838121007534579872">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="7838121007534579872">"ಆಫ್"</item> <item msgid="1578872232501319194">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_dnd"> <item msgid="467587075903158357">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="5376619709702103243">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="5376619709702103243">"ಆಫ್"</item> <item msgid="4875147066469902392">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_flashlight"> <item msgid="3465257127433353857">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="5044688398303285224">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="5044688398303285224">"ಆಫ್"</item> <item msgid="8527389108867454098">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_rotation"> <item msgid="4578491772376121579">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="5776427577477729185">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="5776427577477729185">"ಆಫ್"</item> <item msgid="7105052717007227415">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_bt"> <item msgid="5330252067413512277">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="5315121904534729843">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="5315121904534729843">"ಆಫ್"</item> <item msgid="503679232285959074">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_airplane"> <item msgid="1985366811411407764">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="4801037224991420996">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="4801037224991420996">"ಆಫ್"</item> <item msgid="1982293347302546665">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_location"> <item msgid="3316542218706374405">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="4813655083852587017">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="4813655083852587017">"ಆಫ್"</item> <item msgid="6744077414775180687">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_hotspot"> <item msgid="3145597331197351214">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="5715725170633593906">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="5715725170633593906">"ಆಫ್"</item> <item msgid="2075645297847971154">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_color_correction"> <item msgid="2840507878437297682">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="1909756493418256167">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="1909756493418256167">"ಆಫ್"</item> <item msgid="4531508423703413340">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_inversion"> <item msgid="3638187931191394628">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="9103697205127645916">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="9103697205127645916">"ಆಫ್"</item> <item msgid="8067744885820618230">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_saver"> <item msgid="39714521631367660">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="6983679487661600728">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="6983679487661600728">"ಆಫ್"</item> <item msgid="7520663805910678476">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_dark"> <item msgid="2762596907080603047">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="400477985171353">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="400477985171353">"ಆಫ್"</item> <item msgid="630890598801118771">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_work"> <item msgid="389523503690414094">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="8045580926543311193">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="8045580926543311193">"ಆಫ್"</item> <item msgid="4913460972266982499">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_cast"> <item msgid="6032026038702435350">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="1488620600954313499">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="1488620600954313499">"ಆಫ್"</item> <item msgid="588467578853244035">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_night"> <item msgid="7857498964264855466">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="2744885441164350155">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="2744885441164350155">"ಆಫ್"</item> <item msgid="151121227514952197">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_screenrecord"> <item msgid="1085836626613341403">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="8259411607272330225">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="8259411607272330225">"ಆಫ್"</item> <item msgid="578444932039713369">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_reverse"> <item msgid="3574611556622963971">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="8707481475312432575">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="8707481475312432575">"ಆಫ್"</item> <item msgid="8031106212477483874">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_reduce_brightness"> <item msgid="1839836132729571766">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="4572245614982283078">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="4572245614982283078">"ಆಫ್"</item> <item msgid="6536448410252185664">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_cameratoggle"> <item msgid="6680671247180519913">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="4765607635752003190">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="4765607635752003190">"ಆಫ್"</item> <item msgid="1697460731949649844">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_mictoggle"> <item msgid="6895831614067195493">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="3296179158646568218">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="3296179158646568218">"ಆಫ್"</item> <item msgid="8998632451221157987">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_controls"> <item msgid="8199009425335668294">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="4544919905196727508">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="4544919905196727508">"ಆಫ್"</item> <item msgid="3422023746567004609">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_wallet"> <item msgid="4177615438710836341">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="7571394439974244289">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="7571394439974244289">"ಆಫ್"</item> <item msgid="6866424167599381915">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_qr_code_scanner"> <item msgid="7435143266149257618">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="3301403109049256043">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="3301403109049256043">"ಆಫ್"</item> <item msgid="8878684975184010135">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_alarm"> <item msgid="4936533380177298776">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="2710157085538036590">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="2710157085538036590">"ಆಫ್"</item> <item msgid="7809470840976856149">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_onehanded"> <item msgid="8189342855739930015">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="146088982397753810">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="146088982397753810">"ಆಫ್"</item> <item msgid="460891964396502657">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_dream"> <item msgid="6184819793571079513">"ಲಭ್ಯವಿಲ್ಲ"</item> - <item msgid="8014986104355098744">"ಆಫ್ ಮಾಡಿ"</item> + <item msgid="8014986104355098744">"ಆಫ್"</item> <item msgid="5966994759929723339">"ಆನ್ ಮಾಡಿ"</item> </string-array> <string-array name="tile_states_font_scaling"> diff --git a/packages/SystemUI/res/values-ko/strings.xml b/packages/SystemUI/res/values-ko/strings.xml index 7bd6e6f95e0b..897b266fd65f 100644 --- a/packages/SystemUI/res/values-ko/strings.xml +++ b/packages/SystemUI/res/values-ko/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"설정에서 기본 메모 앱 설정"</string> <string name="install_app" msgid="5066668100199613936">"앱 설치"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"외부 디스플레이로 미러링하시겠습니까?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"내부 디스플레이가 미러링됩니다. 전면 디스플레이는 꺼집니다."</string> <string name="mirror_display" msgid="2515262008898122928">"디스플레이 미러링"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"닫기"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"디스플레이 연결됨"</string> diff --git a/packages/SystemUI/res/values-ky/strings.xml b/packages/SystemUI/res/values-ky/strings.xml index d4974e800a4b..2f091ec484e0 100644 --- a/packages/SystemUI/res/values-ky/strings.xml +++ b/packages/SystemUI/res/values-ky/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Параметрлерден демейки кыска жазуулар колдонмосун тууралаңыз"</string> <string name="install_app" msgid="5066668100199613936">"Колдонмону орнотуу"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Тышкы экранга чыгарасызбы?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Ички экраныңыз башка экранга чыгарылат. Алдыңкы экраныңыз өчүрүлөт."</string> <string name="mirror_display" msgid="2515262008898122928">"Тышкы экран"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Жабуу"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Экран туташтырылды"</string> diff --git a/packages/SystemUI/res/values-lo/strings.xml b/packages/SystemUI/res/values-lo/strings.xml index 431b77b2ebd9..c0c5d44319b7 100644 --- a/packages/SystemUI/res/values-lo/strings.xml +++ b/packages/SystemUI/res/values-lo/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"ຕັ້ງຄ່າແອັບຈົດບັນທຶກເລີ່ມຕົ້ນໃນການຕັ້ງຄ່າ"</string> <string name="install_app" msgid="5066668100199613936">"ຕິດຕັ້ງແອັບ"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"ສາຍໃສ່ຈໍສະແດງຜົນພາຍນອກບໍ?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"ການສະແດງຜົນທາງໃນຂອງທ່ານຈະຖືກສະທ້ອນ. ການສະແດງຜົນທາງໜ້າຂອງທ່ານຈະຖືກປິດໄວ້."</string> <string name="mirror_display" msgid="2515262008898122928">"ຈໍສະແດງຜົນແບບສະທ້ອນ"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"ປິດໄວ້"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"ເຊື່ອມຕໍ່ຈໍແລ້ວ"</string> diff --git a/packages/SystemUI/res/values-lt/strings.xml b/packages/SystemUI/res/values-lt/strings.xml index a0eaea2e9aed..83d2724baff6 100644 --- a/packages/SystemUI/res/values-lt/strings.xml +++ b/packages/SystemUI/res/values-lt/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Nustatykite numatytąją užrašų programą Nustatymuose"</string> <string name="install_app" msgid="5066668100199613936">"Įdiegti programą"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Bendrinti ekrano vaizdą išoriniame ekrane?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Bus bendrinamas vidinio rodinio ekrano vaizdas. Priekinis rodinys bus išjungtas."</string> <string name="mirror_display" msgid="2515262008898122928">"Bendrinti ekrano vaizdą"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Atsisakyti"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Ekranas prijungtas"</string> diff --git a/packages/SystemUI/res/values-lv/strings.xml b/packages/SystemUI/res/values-lv/strings.xml index a419f877f075..4bc71c375b74 100644 --- a/packages/SystemUI/res/values-lv/strings.xml +++ b/packages/SystemUI/res/values-lv/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Iestatījumos iestatiet noklusējuma piezīmju lietotni."</string> <string name="install_app" msgid="5066668100199613936">"Instalēt lietotni"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Vai spoguļot ārējā displejā?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Jūsu iekšējais displejs tiks spoguļots. Jūsu priekšējais displejs tiks izslēgts."</string> <string name="mirror_display" msgid="2515262008898122928">"Spoguļot displeju"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Nerādīt"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Pievienots displejs"</string> diff --git a/packages/SystemUI/res/values-mk/strings.xml b/packages/SystemUI/res/values-mk/strings.xml index ca9fe9e91c9b..28a2a024f2e6 100644 --- a/packages/SystemUI/res/values-mk/strings.xml +++ b/packages/SystemUI/res/values-mk/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Поставете стандардна апликација за белешки во „Поставки“"</string> <string name="install_app" msgid="5066668100199613936">"Инсталирајте ја апликацијата"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Да се преслика на надворешниот екран?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Вашиот внатрешен екран ќе се отслика. Вашиот преден екран ќе се исклучи."</string> <string name="mirror_display" msgid="2515262008898122928">"Пресликај екран"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Отфрли"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Екранот е поврзан"</string> diff --git a/packages/SystemUI/res/values-ml/strings.xml b/packages/SystemUI/res/values-ml/strings.xml index 5ff465672a0b..057f0b0c09e2 100644 --- a/packages/SystemUI/res/values-ml/strings.xml +++ b/packages/SystemUI/res/values-ml/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"ക്രമീകരണത്തിൽ കുറിപ്പുകൾക്കുള്ള ഡിഫോൾട്ട് ആപ്പ് സജ്ജീകരിക്കുക"</string> <string name="install_app" msgid="5066668100199613936">"ആപ്പ് ഇൻസ്റ്റാൾ ചെയ്യൂ"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"ബാഹ്യ ഡിസ്പ്ലേയിലേക്ക് മിറർ ചെയ്യണോ?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"നിങ്ങളുടെ ഇന്നർ ഡിസ്പ്ലേ മിറർ ചെയ്യും. നിങ്ങളുടെ ഫ്രണ്ട് ഡിസ്പ്ലേ ഓഫാകും."</string> <string name="mirror_display" msgid="2515262008898122928">"മിറർ ഡിസ്പ്ലേ"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"ഡിസ്മിസ് ചെയ്യുക"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"ഡിസ്പ്ലേ കണക്റ്റ് ചെയ്തിരിക്കുന്നു"</string> diff --git a/packages/SystemUI/res/values-mn/strings.xml b/packages/SystemUI/res/values-mn/strings.xml index a71aca289ab5..933652b17880 100644 --- a/packages/SystemUI/res/values-mn/strings.xml +++ b/packages/SystemUI/res/values-mn/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Тохиргоонд тэмдэглэлийн өгөгдмөл апп тохируулна уу"</string> <string name="install_app" msgid="5066668100199613936">"Аппыг суулгах"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Гадны дэлгэцэд тусгал үүсгэх үү?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Таны дотоод дэлгэцийн тусгалыг үүсгэнэ. Таны урд талын дэлгэцийг унтраана."</string> <string name="mirror_display" msgid="2515262008898122928">"Дэлгэцийн тусгал үүсгэх"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Хаах"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Дэлгэц холбогдсон"</string> diff --git a/packages/SystemUI/res/values-mr/strings.xml b/packages/SystemUI/res/values-mr/strings.xml index 9dfaa00d0e55..aad269b2460c 100644 --- a/packages/SystemUI/res/values-mr/strings.xml +++ b/packages/SystemUI/res/values-mr/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"सेटिंग्ज मध्ये डीफॉल्ट टिपा अॅप सेट करा"</string> <string name="install_app" msgid="5066668100199613936">"अॅप इंस्टॉल करा"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"बाह्य डिस्प्लेवर मिरर करायचे आहे का?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"तुमचा अंतर्गत डिस्प्ले मिरर केला जाईल. तुमचा पुढील डिस्प्ले बंद केला जाईल."</string> <string name="mirror_display" msgid="2515262008898122928">"डिस्प्ले मिरर करा"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"डिसमिस करा"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"डिस्प्ले कनेक्ट केला आहे"</string> diff --git a/packages/SystemUI/res/values-ms/strings.xml b/packages/SystemUI/res/values-ms/strings.xml index 553d221463b9..8287d2bef2ef 100644 --- a/packages/SystemUI/res/values-ms/strings.xml +++ b/packages/SystemUI/res/values-ms/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Tetapkan apl nota lalai dalam Tetapan"</string> <string name="install_app" msgid="5066668100199613936">"Pasang apl"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Paparkan pada paparan luaran?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Paparan dalaman anda akan dicerminkan. Paparan depan anda akan dimatikan."</string> <string name="mirror_display" msgid="2515262008898122928">"Segerakkan paparan"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Ketepikan"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Paparan disambungkan"</string> diff --git a/packages/SystemUI/res/values-my/strings.xml b/packages/SystemUI/res/values-my/strings.xml index d3f1badbb64b..25afad4991b6 100644 --- a/packages/SystemUI/res/values-my/strings.xml +++ b/packages/SystemUI/res/values-my/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"ဆက်တင်များတွင် မူရင်းမှတ်စုများအက်ပ် သတ်မှတ်ပါ"</string> <string name="install_app" msgid="5066668100199613936">"အက်ပ် ထည့်သွင်းရန်"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"ပြင်ပဖန်သားပြင်သို့ စကရင်ပွားမလား။"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"အတွင်းပြကွက်ကို စကရင်ပွားပါမည်။ ရှေ့မျက်နှာပြင်ပြကွက်ကို ပိတ်မည်။"</string> <string name="mirror_display" msgid="2515262008898122928">"ဖန်သားပြင်ကို စကရင်ပွားရန်"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"ပယ်ရန်"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"ဖန်သားပြင်ကို ချိတ်ဆက်လိုက်ပါပြီ"</string> diff --git a/packages/SystemUI/res/values-nb/strings.xml b/packages/SystemUI/res/values-nb/strings.xml index 234e725e5164..d91574353c83 100644 --- a/packages/SystemUI/res/values-nb/strings.xml +++ b/packages/SystemUI/res/values-nb/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Du kan velge en standardapp for notater i Innstillinger"</string> <string name="install_app" msgid="5066668100199613936">"Installer appen"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Vil du speile til en ekstern skjerm?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Den indre skjermen speiles. Den ytre skjermen slås av."</string> <string name="mirror_display" msgid="2515262008898122928">"Speil skjermen"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Lukk"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"En skjerm er koblet til"</string> diff --git a/packages/SystemUI/res/values-ne/strings.xml b/packages/SystemUI/res/values-ne/strings.xml index 51839dbe5fc7..4abb4a4be0d2 100644 --- a/packages/SystemUI/res/values-ne/strings.xml +++ b/packages/SystemUI/res/values-ne/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"सेटिङमा गई नोट बनाउने डिफल्ट एप तोक्नुहोस्"</string> <string name="install_app" msgid="5066668100199613936">"एप इन्स्टल गर्नुहोस्"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"बाह्य डिस्प्लेमा मिरर गर्ने हो?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"तपाईंको भित्री डिस्प्ले मिरर गरिने छ। तपाईंको अगाडिको डिस्प्ले अफ गरिने छ।"</string> <string name="mirror_display" msgid="2515262008898122928">"डिस्प्ले मिरर गर्नुहोस्"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"खारेज गर्नुहोस्"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"डिस्प्ले कनेक्ट गरिएको छ"</string> diff --git a/packages/SystemUI/res/values-nl/strings.xml b/packages/SystemUI/res/values-nl/strings.xml index f66b4e1972db..057673051e23 100644 --- a/packages/SystemUI/res/values-nl/strings.xml +++ b/packages/SystemUI/res/values-nl/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Standaard notitie-app instellen in Instellingen"</string> <string name="install_app" msgid="5066668100199613936">"App installeren"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Spiegelen naar extern scherm?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Het scherm aan de binnenkant wordt gemirrord. Het scherm aan de voorkant wordt uitgezet."</string> <string name="mirror_display" msgid="2515262008898122928">"Scherm spiegelen"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Sluiten"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Scherm verbonden"</string> diff --git a/packages/SystemUI/res/values-or/strings.xml b/packages/SystemUI/res/values-or/strings.xml index 0a457696c4a3..cb58e64f8eb0 100644 --- a/packages/SystemUI/res/values-or/strings.xml +++ b/packages/SystemUI/res/values-or/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"ସେଟିଂସରେ ଡିଫଲ୍ଟ ନୋଟ୍ସ ଆପ ସେଟ କରନ୍ତୁ"</string> <string name="install_app" msgid="5066668100199613936">"ଆପ ଇନଷ୍ଟଲ କରନ୍ତୁ"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"ଏକ୍ସଟର୍ନଲ ଡିସପ୍ଲେକୁ ମିରର କରିବେ?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"ଆପଣଙ୍କ ଇନର ଡିସପ୍ଲେକୁ ମିରର କରାଯିବ। ଆପଣଙ୍କ ଫ୍ରଣ୍ଟ ଡିସପ୍ଲେକୁ ବନ୍ଦ କରାଯିବ।"</string> <string name="mirror_display" msgid="2515262008898122928">"ଡିସପ୍ଲେ ମିରର କରନ୍ତୁ"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"ଖାରଜ କରନ୍ତୁ"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"ଡିସପ୍ଲେ କନେକ୍ଟ କରାଯାଇଛି"</string> diff --git a/packages/SystemUI/res/values-pa/strings.xml b/packages/SystemUI/res/values-pa/strings.xml index c83659f733d4..0340bd192d58 100644 --- a/packages/SystemUI/res/values-pa/strings.xml +++ b/packages/SystemUI/res/values-pa/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"ਸੈਟਿੰਗਾਂ ਵਿੱਚ ਜਾ ਕੇ ਪੂਰਵ-ਨਿਰਧਾਰਿਤ ਨੋਟ ਐਪ ਨੂੰ ਸੈੱਟ ਕਰੋ"</string> <string name="install_app" msgid="5066668100199613936">"ਐਪ ਸਥਾਪਤ ਕਰੋ"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"ਕੀ ਬਾਹਰੀ ਡਿਸਪਲੇ \'ਤੇ ਪ੍ਰਤਿਬਿੰਬਿਤ ਕਰਨਾ ਹੈ?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"ਤੁਹਾਡੀ ਅੰਦਰੂਨੀ ਡਿਸਪਲੇ ਪ੍ਰਤੀਬਿੰਬਤ ਕੀਤੀ ਜਾਵੇਗੀ। ਤੁਹਾਡੀ ਅਗਲੀ ਡਿਸਪਲੇ ਬੰਦ ਕਰ ਦਿੱਤੀ ਜਾਵੇਗੀ।"</string> <string name="mirror_display" msgid="2515262008898122928">"ਡਿਸਪਲੇ ਨੂੰ ਪ੍ਰਤਿਬਿੰਬਿਤ ਕਰੋ"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"ਖਾਰਜ ਕਰੋ"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"ਡਿਸਪਲੇ ਨੂੰ ਕਨੈਕਟ ਕੀਤਾ ਗਿਆ"</string> diff --git a/packages/SystemUI/res/values-pl/strings.xml b/packages/SystemUI/res/values-pl/strings.xml index 91f2c7817ed6..aa24818d6aee 100644 --- a/packages/SystemUI/res/values-pl/strings.xml +++ b/packages/SystemUI/res/values-pl/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Ustaw domyślną aplikację do obsługi notatek w Ustawieniach"</string> <string name="install_app" msgid="5066668100199613936">"Zainstaluj aplikację"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Powielić na wyświetlaczu zewnętrznym?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Powstanie odbicie lustrzane Twojego wewnętrznego ekranu. Przedni ekran zostanie wyłączony."</string> <string name="mirror_display" msgid="2515262008898122928">"Powielaj obraz"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Zamknij"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Wyświetlacz podłączony"</string> diff --git a/packages/SystemUI/res/values-pt-rBR/strings.xml b/packages/SystemUI/res/values-pt-rBR/strings.xml index 9fe372b66c6c..c65c56e8cefd 100644 --- a/packages/SystemUI/res/values-pt-rBR/strings.xml +++ b/packages/SystemUI/res/values-pt-rBR/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Defina o app de notas padrão nas Configurações"</string> <string name="install_app" msgid="5066668100199613936">"Instalar o app"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Espelhar para a tela externa?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Seu display interno será espelhado. O display frontal será desligado."</string> <string name="mirror_display" msgid="2515262008898122928">"Espelhar tela"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Dispensar"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Tela conectada"</string> diff --git a/packages/SystemUI/res/values-pt-rPT/strings.xml b/packages/SystemUI/res/values-pt-rPT/strings.xml index 3bf9c7b906ec..0b9580d2a01c 100644 --- a/packages/SystemUI/res/values-pt-rPT/strings.xml +++ b/packages/SystemUI/res/values-pt-rPT/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Predefina a app de notas nas Definições"</string> <string name="install_app" msgid="5066668100199613936">"Instalar app"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Espelhar para o ecrã externo?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"O seu ecrã interior vai ser espelhado. O seu ecrã frontal vai ser desativado."</string> <string name="mirror_display" msgid="2515262008898122928">"Espelhar ecrã"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Ignorar"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Ecrã ligado"</string> diff --git a/packages/SystemUI/res/values-pt/strings.xml b/packages/SystemUI/res/values-pt/strings.xml index 9fe372b66c6c..c65c56e8cefd 100644 --- a/packages/SystemUI/res/values-pt/strings.xml +++ b/packages/SystemUI/res/values-pt/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Defina o app de notas padrão nas Configurações"</string> <string name="install_app" msgid="5066668100199613936">"Instalar o app"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Espelhar para a tela externa?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Seu display interno será espelhado. O display frontal será desligado."</string> <string name="mirror_display" msgid="2515262008898122928">"Espelhar tela"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Dispensar"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Tela conectada"</string> diff --git a/packages/SystemUI/res/values-ro/strings.xml b/packages/SystemUI/res/values-ro/strings.xml index 18aa668d2fa5..b2fdef3bcde6 100644 --- a/packages/SystemUI/res/values-ro/strings.xml +++ b/packages/SystemUI/res/values-ro/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Setează aplicația prestabilită de note din Setări"</string> <string name="install_app" msgid="5066668100199613936">"Instalează aplicația"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Oglindești pe ecranul extern?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Ecranul interior va fi oglindit. Ecranul frontal va fi dezactivat."</string> <string name="mirror_display" msgid="2515262008898122928">"Afișare în oglindă"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Închide"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Ecran conectat"</string> diff --git a/packages/SystemUI/res/values-ru/strings.xml b/packages/SystemUI/res/values-ru/strings.xml index 51192f8e11ce..ff4bd45745c9 100644 --- a/packages/SystemUI/res/values-ru/strings.xml +++ b/packages/SystemUI/res/values-ru/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Задайте стандартное приложение для заметок в настройках."</string> <string name="install_app" msgid="5066668100199613936">"Установить приложение"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Дублировать на внешний дисплей?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Для внутреннего экрана включится дублирование. Передний экран будет отключен."</string> <string name="mirror_display" msgid="2515262008898122928">"Дублировать"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Закрыть"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Экран подключен"</string> diff --git a/packages/SystemUI/res/values-si/strings.xml b/packages/SystemUI/res/values-si/strings.xml index 7ade110cd5db..4b4d08bd8189 100644 --- a/packages/SystemUI/res/values-si/strings.xml +++ b/packages/SystemUI/res/values-si/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"සැකසීම් තුළ පෙරනිමි සටහන් යෙදුම සකසන්න"</string> <string name="install_app" msgid="5066668100199613936">"යෙදුම ස්ථාපනය කරන්න"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"බාහිර සංදර්ශකයට දර්පණය කරන්න ද?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"ඔබේ අභ්යන්තර සංදර්ශකය පිළිබිඹු වනු ඇත. ඔබේ ඉදිරිපස සංදර්ශකය ක්රියාවිරහිත වනු ඇත."</string> <string name="mirror_display" msgid="2515262008898122928">"සංදර්ශකය දර්පණය කරන්න"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"අස් කරන්න"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"සංදර්ශකය සම්බන්ධ කර ඇත"</string> diff --git a/packages/SystemUI/res/values-sk/strings.xml b/packages/SystemUI/res/values-sk/strings.xml index 8d760d51603d..9e9507e92eb9 100644 --- a/packages/SystemUI/res/values-sk/strings.xml +++ b/packages/SystemUI/res/values-sk/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Nastavte predvolenú aplikáciu na poznámky v Nastaveniach"</string> <string name="install_app" msgid="5066668100199613936">"Inštalovať aplikáciu"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Chcete zrkadliť na externú obrazovku?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Vnútorná obrazovka bude zrkadlená. Predná obrazovka bude vypnutá."</string> <string name="mirror_display" msgid="2515262008898122928">"Zrkadliť obrazovku"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Zavrieť"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Obrazovka je pripojená"</string> diff --git a/packages/SystemUI/res/values-sl/strings.xml b/packages/SystemUI/res/values-sl/strings.xml index 4ae80ef5ba4c..cba54167fe86 100644 --- a/packages/SystemUI/res/values-sl/strings.xml +++ b/packages/SystemUI/res/values-sl/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Nastavite privzeto aplikacijo za zapiske v nastavitvah."</string> <string name="install_app" msgid="5066668100199613936">"Namesti aplikacijo"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Želite zrcaliti na zunanji zaslon?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Notranji zaslon bo zrcaljen. Sprednji zaslon bo izklopljen."</string> <string name="mirror_display" msgid="2515262008898122928">"Zrcali zaslon"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Opusti"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Zaslon je povezan"</string> diff --git a/packages/SystemUI/res/values-sq/strings.xml b/packages/SystemUI/res/values-sq/strings.xml index 83727038c0af..b35668f45d37 100644 --- a/packages/SystemUI/res/values-sq/strings.xml +++ b/packages/SystemUI/res/values-sq/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Cakto aplikacionin e parazgjedhur të shënimeve te \"Cilësimet\""</string> <string name="install_app" msgid="5066668100199613936">"Instalo aplikacionin"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Të pasqyrohet në ekranin e jashtëm?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Ekrani i brendshëm do të pasqyrohet. Ekrani i parmë do të çaktivizohet."</string> <string name="mirror_display" msgid="2515262008898122928">"Pasqyro ekranin"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Hiq"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Ekrani është lidhur"</string> diff --git a/packages/SystemUI/res/values-sr/strings.xml b/packages/SystemUI/res/values-sr/strings.xml index 1bda1dbe0ae1..1d45fbd5267c 100644 --- a/packages/SystemUI/res/values-sr/strings.xml +++ b/packages/SystemUI/res/values-sr/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Подесите подразумевану апликацију за белешке у Подешавањима"</string> <string name="install_app" msgid="5066668100199613936">"Инсталирај апликацију"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Желите ли да пресликате на спољњи екран?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Унутрашњи екран ће се пресликати. Предњи екран ће се искључити."</string> <string name="mirror_display" msgid="2515262008898122928">"Пресликај екран"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Одбаци"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Екран је повезан"</string> diff --git a/packages/SystemUI/res/values-sv/strings.xml b/packages/SystemUI/res/values-sv/strings.xml index 8cfafcf2fcdd..0d6272f532b1 100644 --- a/packages/SystemUI/res/values-sv/strings.xml +++ b/packages/SystemUI/res/values-sv/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Ställ in en standardapp för anteckningar i inställningarna"</string> <string name="install_app" msgid="5066668100199613936">"Installera appen"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Vill du spegla till extern skärm?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Den inre skärmen speglas. Den främre skärmen stängs av."</string> <string name="mirror_display" msgid="2515262008898122928">"Spegla skärm"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Ignorera"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Skärm har anslutits"</string> diff --git a/packages/SystemUI/res/values-sw/strings.xml b/packages/SystemUI/res/values-sw/strings.xml index 286e65abf384..1bd2ed7aa30d 100644 --- a/packages/SystemUI/res/values-sw/strings.xml +++ b/packages/SystemUI/res/values-sw/strings.xml @@ -252,7 +252,7 @@ <string name="quick_settings_bluetooth_label" msgid="7018763367142041481">"Bluetooth"</string> <string name="quick_settings_bluetooth_detail_empty_text" msgid="5760239584390514322">"Hakuna vifaa vilivyooanishwa vinavyopatikana"</string> <string name="quick_settings_bluetooth_tile_subtitle" msgid="212752719010829550">"Gusa ili uunganishe au utenganishe kifaa"</string> - <string name="pair_new_bluetooth_devices" msgid="4601767620843349645">"Oanisha kifaa kipya"</string> + <string name="pair_new_bluetooth_devices" msgid="4601767620843349645">"Unganisha kifaa kipya"</string> <string name="see_all_bluetooth_devices" msgid="1761596816620200433">"Angalia vyote"</string> <string name="turn_on_bluetooth" msgid="5681370462180289071">"Tumia Bluetooth"</string> <string name="quick_settings_bluetooth_device_connected" msgid="7884777006729260996">"Imeunganishwa"</string> @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Teua programu chaguomsingi ya madokezo katika Mipangilio"</string> <string name="install_app" msgid="5066668100199613936">"Sakinisha programu"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Ungependa kuonyesha kwenye skrini ya nje?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Mwonekano wa ndani wa kifaa chako utaakisiwa. Mwonekano wa mbele wa kifaa chako utazimwa."</string> <string name="mirror_display" msgid="2515262008898122928">"Akisi skrini"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Ondoa"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Skrini imeunganishwa"</string> diff --git a/packages/SystemUI/res/values-sw600dp/config.xml b/packages/SystemUI/res/values-sw600dp/config.xml index 1f671ac4c875..f1017d8fe36e 100644 --- a/packages/SystemUI/res/values-sw600dp/config.xml +++ b/packages/SystemUI/res/values-sw600dp/config.xml @@ -40,6 +40,9 @@ <!-- Whether to show bottom sheets edge to edge --> <bool name="config_edgeToEdgeBottomSheetDialog">false</bool> + <!-- Flag to activate drag notification to contents feature --> + <bool name="config_notificationToContents">true</bool> + <!-- A collection of defaults for the quick affordances on the lock screen. Each item must be a string with two parts: the ID of the slot and the comma-delimited list of affordance IDs, separated by a colon ':' character. For example: <item>bottom_end:home,wallet</item>. The diff --git a/packages/SystemUI/res/values-ta/strings.xml b/packages/SystemUI/res/values-ta/strings.xml index aab713f7517a..0cd076f77d3e 100644 --- a/packages/SystemUI/res/values-ta/strings.xml +++ b/packages/SystemUI/res/values-ta/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"குறிப்பு எடுப்பதற்கான இயல்புநிலை ஆப்ஸை அமைப்புகளில் அமையுங்கள்"</string> <string name="install_app" msgid="5066668100199613936">"ஆப்ஸை நிறுவுங்கள்"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"வெளிப்புறக் காட்சிக்கு மிரர் செய்யவா?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"உங்கள் உட்புற டிஸ்பிளே பிரதிபலிக்கப்படும். உங்கள் முன்புற டிஸ்பிளே முடக்கப்படும்."</string> <string name="mirror_display" msgid="2515262008898122928">"டிஸ்பிளேயை மிரர் செய்"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"வேண்டாம்"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"டிஸ்ப்ளே இணைக்கப்பட்டது"</string> diff --git a/packages/SystemUI/res/values-te/strings.xml b/packages/SystemUI/res/values-te/strings.xml index 2536a96050fa..6a59812d39b4 100644 --- a/packages/SystemUI/res/values-te/strings.xml +++ b/packages/SystemUI/res/values-te/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"సెట్టింగ్లలో ఆటోమేటిక్గా ఉండేలా ఒక నోట్స్ యాప్ను సెట్ చేసుకోండి"</string> <string name="install_app" msgid="5066668100199613936">"యాప్ను ఇన్స్టాల్ చేయండి"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"ఎక్స్టర్నల్ డిస్ప్లేకి మిర్రర్ చేయాలా?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"మీ లోపలి డిస్ప్లే మిర్రర్ చేయబడుతుంది. మీ ముందు వైపు డిస్ప్లే ఆఫ్ చేయబడుతుంది."</string> <string name="mirror_display" msgid="2515262008898122928">"మిర్రర్ డిస్ప్లే"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"విస్మరించండి"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"డిస్ప్లే కనెక్ట్ చేయబడింది"</string> diff --git a/packages/SystemUI/res/values-th/strings.xml b/packages/SystemUI/res/values-th/strings.xml index 3c96e30f4631..dc4c6cf36b7a 100644 --- a/packages/SystemUI/res/values-th/strings.xml +++ b/packages/SystemUI/res/values-th/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"กำหนดแอปการจดบันทึกเริ่มต้นในการตั้งค่า"</string> <string name="install_app" msgid="5066668100199613936">"ติดตั้งแอป"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"มิเรอร์ไปยังจอแสดงผลภายนอกไหม"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"ระบบจะมิเรอร์หน้าจอด้านใน และจะปิดหน้าจอด้านหน้า"</string> <string name="mirror_display" msgid="2515262008898122928">"มิเรอร์จอแสดงผล"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"ปิด"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"เชื่อมต่อจอแสดงผลแล้ว"</string> diff --git a/packages/SystemUI/res/values-tl/strings.xml b/packages/SystemUI/res/values-tl/strings.xml index 8fc5b6b6668a..c09ac9748b6f 100644 --- a/packages/SystemUI/res/values-tl/strings.xml +++ b/packages/SystemUI/res/values-tl/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Magtakda ng default na app sa pagtatala sa Mga Setting"</string> <string name="install_app" msgid="5066668100199613936">"I-install ang app"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"I-mirror sa external na display?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Imi-mirror ang inner display mo. Io-off ang iyong front display."</string> <string name="mirror_display" msgid="2515262008898122928">"I-mirror ang display"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"I-dismiss"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Naikonekta ang display"</string> diff --git a/packages/SystemUI/res/values-tr/strings.xml b/packages/SystemUI/res/values-tr/strings.xml index a4083efc84f8..ee1909b28098 100644 --- a/packages/SystemUI/res/values-tr/strings.xml +++ b/packages/SystemUI/res/values-tr/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Ayarlar\'ı kullanarak varsayılan notlar ayarlayın"</string> <string name="install_app" msgid="5066668100199613936">"Uygulamayı yükle"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Harici ekrana yansıtılsın mı?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"İç ekranınız yansıtılacak. Ön ekranınız kapatılacak."</string> <string name="mirror_display" msgid="2515262008898122928">"Ekranı yansıt"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Kapat"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Ekran bağlandı"</string> diff --git a/packages/SystemUI/res/values-uk/strings.xml b/packages/SystemUI/res/values-uk/strings.xml index 5e0f2c2ed352..04a97bcecb80 100644 --- a/packages/SystemUI/res/values-uk/strings.xml +++ b/packages/SystemUI/res/values-uk/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Призначте стандартний додаток для нотаток у налаштуваннях"</string> <string name="install_app" msgid="5066668100199613936">"Установити додаток"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Дублювати на зовнішньому екрані?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Ваш внутрішній екран буде продубльовано. Передній екран буде вимкнено."</string> <string name="mirror_display" msgid="2515262008898122928">"Дублювати екран"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Закрити"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Дисплей під’єднано"</string> diff --git a/packages/SystemUI/res/values-ur/strings.xml b/packages/SystemUI/res/values-ur/strings.xml index c28e064e1f22..fd984b9c4ae7 100644 --- a/packages/SystemUI/res/values-ur/strings.xml +++ b/packages/SystemUI/res/values-ur/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"ترتیبات میں ڈیفالٹ نوٹس ایپ سیٹ کریں"</string> <string name="install_app" msgid="5066668100199613936">"ایپ انسٹال کریں"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"بیرونی ڈسپلے پر مرر کریں؟"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"آپ کے اندرونی ڈسپلے کو دو طرفہ مطابقت پذیر بنایا جائے گا۔ آپ کا فرنٹ ڈسپلے آف ہو جائے گا۔"</string> <string name="mirror_display" msgid="2515262008898122928">"ڈسپلے کو مرر کریں"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"برخاست کریں"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"ڈسپلے منسلک ہے"</string> diff --git a/packages/SystemUI/res/values-uz/strings.xml b/packages/SystemUI/res/values-uz/strings.xml index e032c76f48b5..b9a9832ea249 100644 --- a/packages/SystemUI/res/values-uz/strings.xml +++ b/packages/SystemUI/res/values-uz/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Standart qaydlar ilovasini Sozlamalar orqali tanlang"</string> <string name="install_app" msgid="5066668100199613936">"Ilovani oʻrnatish"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Tashqi displeyda aks ettirilsinmi?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Ichki ekran uchun aks ettirish yoqiladi. Old ekran oʻchiriladi."</string> <string name="mirror_display" msgid="2515262008898122928">"Displeyni aks ettirish"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Yopish"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Displey ulandi"</string> diff --git a/packages/SystemUI/res/values-vi/strings.xml b/packages/SystemUI/res/values-vi/strings.xml index b87619e497df..b1ff9a86ee6e 100644 --- a/packages/SystemUI/res/values-vi/strings.xml +++ b/packages/SystemUI/res/values-vi/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Đặt ứng dụng ghi chú mặc định trong phần Cài đặt"</string> <string name="install_app" msgid="5066668100199613936">"Cài đặt ứng dụng"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Phản chiếu sang màn hình ngoài?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Màn hình trong của bạn sẽ được phản chiếu. Màn hình ngoài của bạn sẽ tắt."</string> <string name="mirror_display" msgid="2515262008898122928">"Phản chiếu màn hình"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Đóng"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Đã kết nối màn hình"</string> diff --git a/packages/SystemUI/res/values-zh-rCN/strings.xml b/packages/SystemUI/res/values-zh-rCN/strings.xml index 43dfbe8e8ff1..237fd572530f 100644 --- a/packages/SystemUI/res/values-zh-rCN/strings.xml +++ b/packages/SystemUI/res/values-zh-rCN/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"在设置中设置默认记事应用"</string> <string name="install_app" msgid="5066668100199613936">"安装应用"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"镜像到外接显示屏?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"系统将镜像您的内屏,而关闭外屏。"</string> <string name="mirror_display" msgid="2515262008898122928">"镜像到显示屏"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"关闭"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"显示屏已连接"</string> diff --git a/packages/SystemUI/res/values-zh-rHK/strings.xml b/packages/SystemUI/res/values-zh-rHK/strings.xml index 784f667575b9..313af30bf5a6 100644 --- a/packages/SystemUI/res/values-zh-rHK/strings.xml +++ b/packages/SystemUI/res/values-zh-rHK/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"在「設定」中指定預設筆記應用程式"</string> <string name="install_app" msgid="5066668100199613936">"安裝應用程式"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"要鏡像投射至外部顯示屏嗎?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"鏡像畫面將顯示在內部螢幕,前方螢幕則會關閉。"</string> <string name="mirror_display" msgid="2515262008898122928">"鏡像顯示"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"關閉"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"已連接顯示屏"</string> diff --git a/packages/SystemUI/res/values-zh-rTW/strings.xml b/packages/SystemUI/res/values-zh-rTW/strings.xml index 15da239353d0..6a13d3dc22a5 100644 --- a/packages/SystemUI/res/values-zh-rTW/strings.xml +++ b/packages/SystemUI/res/values-zh-rTW/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"在「設定」中指定預設記事應用程式"</string> <string name="install_app" msgid="5066668100199613936">"安裝應用程式"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"要以鏡像方式投放至外部螢幕嗎?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"鏡像畫面將顯示在內螢幕,封面螢幕則會關閉。"</string> <string name="mirror_display" msgid="2515262008898122928">"鏡像顯示"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"關閉"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"螢幕已連結"</string> diff --git a/packages/SystemUI/res/values-zu/strings.xml b/packages/SystemUI/res/values-zu/strings.xml index 31cdca973cde..23862a78b9d9 100644 --- a/packages/SystemUI/res/values-zu/strings.xml +++ b/packages/SystemUI/res/values-zu/strings.xml @@ -1208,8 +1208,7 @@ <string name="set_default_notes_app_toast_content" msgid="2812374329662610753">"Setha i-app yamanothi azenzakalelayo Kumsethingi"</string> <string name="install_app" msgid="5066668100199613936">"Faka i-app"</string> <string name="connected_display_dialog_start_mirroring" msgid="6237895789920854982">"Fanisa nesibonisi sangaphandle?"</string> - <!-- no translation found for connected_display_dialog_dual_display_stop_warning (4174707498892447947) --> - <skip /> + <string name="connected_display_dialog_dual_display_stop_warning" msgid="4174707498892447947">"Isibonisi sakho sangaphakathi sizoboniswa. Isibonisi sakho sangaphambili sizovalwa."</string> <string name="mirror_display" msgid="2515262008898122928">"Isibonisi sokufanisa"</string> <string name="dismiss_dialog" msgid="2195508495854675882">"Chitha"</string> <string name="connected_display_icon_desc" msgid="6373560639989971997">"Isibonisi sixhunyiwe"</string> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index e01a2aa674b3..10f7c4d3ee5b 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -962,11 +962,20 @@ <!-- Whether to show bottom sheets edge to edge --> <bool name="config_edgeToEdgeBottomSheetDialog">true</bool> + <!-- Device specific config that controls whether rest to unlock feature is supported. --> + <bool name="config_restToUnlockSupported">false</bool> + + <!-- + Time in milliseconds the user has to touch the side FPS sensor to successfully authenticate when + the screen is turned off with AOD not enabled. + TODO(b/302332976) Get this value from the HAL if they can provide an API for it. + --> + <integer name="config_restToUnlockDurationScreenOff">500</integer> <!-- Time in milliseconds the user has to touch the side FPS sensor to successfully authenticate TODO(b/302332976) Get this value from the HAL if they can provide an API for it. --> - <integer name="config_restToUnlockDuration">300</integer> + <integer name="config_restToUnlockDurationDefault">300</integer> <!-- Width in pixels of the Side FPS sensor. diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index ee89edefcdbf..90d8cdb724e4 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -608,6 +608,11 @@ <dimen name="volume_panel_slice_vertical_padding">8dp</dimen> <dimen name="volume_panel_slice_horizontal_padding">24dp</dimen> + <dimen name="volume_panel_corner_radius">52dp</dimen> + <dimen name="volume_panel_content_padding">24dp</dimen> + <dimen name="volume_panel_bottom_bar_horizontal_padding">24dp</dimen> + <dimen name="volume_panel_bottom_bar_bottom_padding">20dp</dimen> + <!-- Size of each item in the ringer selector drawer. --> <dimen name="volume_ringer_drawer_item_size">42dp</dimen> <dimen name="volume_ringer_drawer_item_size_half">21dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index e7eb984746e8..854bb0f05c5e 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3262,6 +3262,9 @@ <!-- Label for a button that, when clicked, sends the user to the app store to install an app. [CHAR LIMIT=64]. --> <string name="install_app">Install app</string> + <!-- Instructions informing the user they can swipe up on the lockscreen to dismiss [CHAR LIMIT=48]--> + <string name="dismissible_keyguard_swipe">Swipe to continue</string> + <!--- Title of the dialog appearing when an external display is connected, asking whether to start mirroring [CHAR LIMIT=NONE]--> <string name="connected_display_dialog_start_mirroring">Mirror to external display?</string> <!--- Body of the mirroring dialog, shown when dual display is enabled. This signals that enabling mirroring will stop concurrent displays on a foldable device. [CHAR LIMIT=NONE]--> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index c48dd9d2f68b..3f026a4cec8a 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -930,6 +930,15 @@ <item name="wallpaperTextColor">@*android:color/primary_text_material_dark</item> </style> + <style name="Theme.VolumePanelActivity" parent="@style/Theme.SystemUI"> + <item name="android:windowIsTranslucent">true</item> + <item name="android:windowBackground">@android:color/transparent</item> + <item name="android:windowActionBar">false</item> + <item name="android:windowNoTitle">true</item> + <!-- Setting a placeholder will avoid using the SystemUI icon on the splash screen. --> + <item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_blank</item> + </style> + <style name="Theme.UserSwitcherFullscreenDialog" parent="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen"> <item name="android:statusBarColor">@color/user_switcher_fullscreen_bg</item> <item name="android:windowBackground">@color/user_switcher_fullscreen_bg</item> diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp index 2b4117866254..3a26ebff6c6a 100644 --- a/packages/SystemUI/shared/Android.bp +++ b/packages/SystemUI/shared/Android.bp @@ -66,6 +66,7 @@ android_library { "kotlinx_coroutines", "dagger2", "jsr330", + "com_android_systemui_shared_flags_lib", ], resource_dirs: [ "res", diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java index 131eb6b63df3..c08b0837da8e 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java @@ -20,10 +20,11 @@ import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; +import static com.android.systemui.shared.Flags.shadeAllowBackGesture; + import android.annotation.IntDef; import android.content.Context; import android.content.res.Resources; -import android.os.SystemProperties; import android.view.ViewConfiguration; import android.view.WindowManagerPolicyConstants; @@ -132,8 +133,7 @@ public class QuickStepContract { SYSUI_STATE_WAKEFULNESS_TRANSITION | SYSUI_STATE_AWAKE; // Whether the back gesture is allowed (or ignored) by the Shade - public static final boolean ALLOW_BACK_GESTURE_IN_SHADE = SystemProperties.getBoolean( - "persist.wm.debug.shade_allow_back_gesture", false); + public static final boolean ALLOW_BACK_GESTURE_IN_SHADE = shadeAllowBackGesture(); @Retention(RetentionPolicy.SOURCE) @IntDef({SYSUI_STATE_SCREEN_PINNING, diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index bcc20448297d..82410fd39dcd 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -33,6 +33,7 @@ import android.view.ViewTreeObserver.OnGlobalLayoutListener import androidx.annotation.VisibleForTesting import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.customization.R import com.android.systemui.dagger.qualifiers.Background @@ -325,6 +326,10 @@ constructor( } } + if (visible) { + refreshTime() + } + smallTimeListener?.update(shouldTimeListenerRun) largeTimeListener?.update(shouldTimeListenerRun) } @@ -346,6 +351,19 @@ constructor( weatherData = data clock?.run { events.onWeatherDataChanged(data) } } + + override fun onTimeChanged() { + refreshTime() + } + + private fun refreshTime() { + if (!migrateClocksToBlueprint()) { + return + } + + clock?.smallClock?.events?.onTimeTick() + clock?.largeClock?.events?.onTimeTick() + } } private val zenModeCallback = object : ZenModeController.Callback { @@ -558,7 +576,8 @@ constructor( isRunning = true when (clockFace.config.tickRate) { ClockTickRate.PER_MINUTE -> { - /* Handled by KeyguardClockSwitchController */ + // Handled by KeyguardClockSwitchController and + // by KeyguardUpdateMonitorCallback#onTimeChanged. } ClockTickRate.PER_SECOND -> executor.execute(secondsRunnable) ClockTickRate.PER_FRAME -> { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index a8bf229eb991..05eeac6fc199 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -133,7 +133,8 @@ public class KeyguardSecurityContainer extends ConstraintLayout { static final int BOUNCER_DISMISS_EXTENDED_ACCESS = 3; // Bouncer is dismissed due to sim card unlock code entered. static final int BOUNCER_DISMISS_SIM = 4; - + // Bouncer dismissed after being allowed to dismiss by forceDismissiblekeyguard + static final int BOUNCER_DISMISSIBLE_KEYGUARD = 5; private static final String TAG = "KeyguardSecurityView"; // Make the view move slower than the finger, as if the spring were applying force. diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 5e35e7764dd8..e457ca18d071 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -21,6 +21,7 @@ import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_BIOMETRIC; import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_EXTENDED_ACCESS; +import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISSIBLE_KEYGUARD; import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_NONE_SECURITY; import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_PASSWORD; import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_SIM; @@ -78,10 +79,10 @@ import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor; import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.classifier.FalsingA11yDelegate; import com.android.systemui.classifier.FalsingCollector; +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.ActivityStarter; @@ -135,7 +136,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard private final SessionTracker mSessionTracker; private final Optional<SideFpsController> mSideFpsController; private final FalsingA11yDelegate mFalsingA11yDelegate; - private final KeyguardFaceAuthInteractor mKeyguardFaceAuthInteractor; + private final DeviceEntryFaceAuthInteractor mDeviceEntryFaceAuthInteractor; private final BouncerMessageInteractor mBouncerMessageInteractor; private int mTranslationY; private final KeyguardTransitionInteractor mKeyguardTransitionInteractor; @@ -216,7 +217,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard @Override public void onUserInput() { mBouncerMessageInteractor.onPrimaryBouncerUserInput(); - mKeyguardFaceAuthInteractor.onPrimaryBouncerUserInput(); + mDeviceEntryFaceAuthInteractor.onPrimaryBouncerUserInput(); } @Override @@ -347,11 +348,11 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard private final SwipeListener mSwipeListener = new SwipeListener() { @Override public void onSwipeUp() { - if (mKeyguardFaceAuthInteractor.canFaceAuthRun()) { + if (mDeviceEntryFaceAuthInteractor.canFaceAuthRun()) { mKeyguardSecurityCallback.userActivity(); } - mKeyguardFaceAuthInteractor.onSwipeUpOnBouncer(); - if (mKeyguardFaceAuthInteractor.isFaceAuthEnabledAndEnrolled()) { + mDeviceEntryFaceAuthInteractor.onSwipeUpOnBouncer(); + if (mDeviceEntryFaceAuthInteractor.isFaceAuthEnabledAndEnrolled()) { mUpdateMonitor.requestActiveUnlock( ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT, "swipeUpOnBouncer"); @@ -456,7 +457,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard TelephonyManager telephonyManager, ViewMediatorCallback viewMediatorCallback, AudioManager audioManager, - KeyguardFaceAuthInteractor keyguardFaceAuthInteractor, + DeviceEntryFaceAuthInteractor deviceEntryFaceAuthInteractor, BouncerMessageInteractor bouncerMessageInteractor, Provider<JavaAdapter> javaAdapter, SelectedUserInteractor selectedUserInteractor, @@ -495,7 +496,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard mTelephonyManager = telephonyManager; mViewMediatorCallback = viewMediatorCallback; mAudioManager = audioManager; - mKeyguardFaceAuthInteractor = keyguardFaceAuthInteractor; + mDeviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor; mBouncerMessageInteractor = bouncerMessageInteractor; mSelectedUserInteractor = selectedUserInteractor; mDeviceEntryInteractor = deviceEntryInteractor; @@ -862,7 +863,11 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard boolean finish = false; int eventSubtype = -1; BouncerUiEvent uiEvent = BouncerUiEvent.UNKNOWN; - if (mUpdateMonitor.getUserHasTrust(targetUserId)) { + if (mUpdateMonitor.forceIsDismissibleIsKeepingDeviceUnlocked()) { + finish = true; + eventSubtype = BOUNCER_DISMISSIBLE_KEYGUARD; + // TODO: b/308417021 add UI event + } else if (mUpdateMonitor.getUserHasTrust(targetUserId)) { finish = true; eventSubtype = BOUNCER_DISMISS_EXTENDED_ACCESS; uiEvent = BouncerUiEvent.BOUNCER_DISMISS_EXTENDED_ACCESS; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index f3cd01f908da..fe96099b0824 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -102,6 +102,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.foldables.FoldGracePeriodProvider; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.InstanceId; import com.android.internal.logging.UiEventLogger; @@ -113,24 +114,26 @@ import com.android.settingslib.WirelessUtils; import com.android.settingslib.fuelgauge.BatteryStatus; import com.android.systemui.CoreStartable; import com.android.systemui.Dumpable; +import com.android.systemui.Flags; import com.android.systemui.biometrics.AuthController; import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.deviceentry.data.repository.FaceWakeUpTriggersConfig; +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor; +import com.android.systemui.deviceentry.domain.interactor.FaceAuthenticationListener; +import com.android.systemui.deviceentry.shared.model.AcquiredFaceAuthenticationStatus; +import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus; +import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus; +import com.android.systemui.deviceentry.shared.model.FaceDetectionStatus; +import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus; +import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus; +import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus; import com.android.systemui.dump.DumpManager; import com.android.systemui.dump.DumpsysTableLogger; -import com.android.systemui.keyguard.domain.interactor.FaceAuthenticationListener; -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor; import com.android.systemui.keyguard.shared.constants.TrustAgentUiEvent; -import com.android.systemui.keyguard.shared.model.AcquiredFaceAuthenticationStatus; -import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus; -import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus; -import com.android.systemui.keyguard.shared.model.FaceDetectionStatus; -import com.android.systemui.keyguard.shared.model.FailedFaceAuthenticationStatus; -import com.android.systemui.keyguard.shared.model.HelpFaceAuthenticationStatus; -import com.android.systemui.keyguard.shared.model.SuccessFaceAuthenticationStatus; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.clocks.WeatherData; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -217,7 +220,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private static final int MSG_BIOMETRIC_ENROLLMENT_STATE_CHANGED = 348; /** Biometric authentication state: Not listening. */ - private static final int BIOMETRIC_STATE_STOPPED = 0; + @VisibleForTesting + protected static final int BIOMETRIC_STATE_STOPPED = 0; /** Biometric authentication state: Listening. */ private static final int BIOMETRIC_STATE_RUNNING = 1; @@ -305,6 +309,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private boolean mKeyguardOccluded; private boolean mCredentialAttempted; private boolean mKeyguardGoingAway; + /** + * Whether the keyguard is forced into a dismissible state. + */ + private boolean mForceIsDismissible; private boolean mGoingToSleep; private boolean mPrimaryBouncerFullyShown; private boolean mPrimaryBouncerIsOrWillBeShowing; @@ -358,7 +366,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab @Nullable private final BiometricManager mBiometricManager; @Nullable - private KeyguardFaceAuthInteractor mFaceAuthInteractor; + private DeviceEntryFaceAuthInteractor mFaceAuthInteractor; + @VisibleForTesting + protected FoldGracePeriodProvider mFoldGracePeriodProvider = + new FoldGracePeriodProvider(); private final DevicePostureController mDevicePostureController; private final TaskStackChangeListeners mTaskStackChangeListeners; private final IActivityTaskManager mActivityTaskManager; @@ -372,6 +383,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private List<SubscriptionInfo> mSubscriptionInfo; @VisibleForTesting protected int mFingerprintRunningState = BIOMETRIC_STATE_STOPPED; + private boolean mFingerprintDetectRunning; private boolean mIsDreaming; private boolean mLogoutEnabled; private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID; @@ -724,6 +736,14 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } /** + * Updates KeyguardUpdateMonitor's internal state to know the device should remain unlocked + * until the next signal to lock. Does nothing if the keyguard is already showing. + */ + public void tryForceIsDismissibleKeyguard() { + setForceIsDismissibleKeyguard(true); + } + + /** * Updates KeyguardUpdateMonitor's internal state to know if keyguard is going away. */ public void setKeyguardGoingAway(boolean goingAway) { @@ -985,6 +1005,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab final boolean wasCancellingRestarting = mFingerprintRunningState == BIOMETRIC_STATE_CANCELLING_RESTARTING; mFingerprintRunningState = BIOMETRIC_STATE_STOPPED; + mFingerprintDetectRunning = false; if (wasCancellingRestarting) { KeyguardUpdateMonitor.this.updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); } else { @@ -1093,6 +1114,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab boolean wasRunning = mFingerprintRunningState == BIOMETRIC_STATE_RUNNING; boolean isRunning = fingerprintRunningState == BIOMETRIC_STATE_RUNNING; mFingerprintRunningState = fingerprintRunningState; + if (mFingerprintRunningState == BIOMETRIC_STATE_STOPPED) { + mFingerprintDetectRunning = false; + } mLogger.logFingerprintRunningState(mFingerprintRunningState); // Clients of KeyguardUpdateMonitor don't care about the internal state about the // asynchronousness of the cancel cycle. So only notify them if the actually running state @@ -1265,14 +1289,14 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return getFaceAuthInteractor() != null && getFaceAuthInteractor().isRunning(); } - private @Nullable KeyguardFaceAuthInteractor getFaceAuthInteractor() { + private @Nullable DeviceEntryFaceAuthInteractor getFaceAuthInteractor() { return mFaceAuthInteractor; } /** * Set the face auth interactor that should be used for initiating face authentication. */ - public void setFaceAuthInteractor(KeyguardFaceAuthInteractor faceAuthInteractor) { + public void setFaceAuthInteractor(DeviceEntryFaceAuthInteractor faceAuthInteractor) { if (mFaceAuthInteractor != null) { mFaceAuthInteractor.unregisterListener(mFaceAuthenticationListener); } @@ -1355,7 +1379,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab * @return whether the current user has been authenticated with face. This may be true * on the lockscreen if the user doesn't have bypass enabled. * - * @deprecated Use {@link KeyguardFaceAuthInteractor#isAuthenticated()} + * @deprecated Use {@link DeviceEntryFaceAuthInteractor#isAuthenticated()} */ @Deprecated public boolean getIsFaceAuthenticated() { @@ -1363,7 +1387,18 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } public boolean getUserCanSkipBouncer(int userId) { - return getUserHasTrust(userId) || getUserUnlockedWithBiometric(userId); + return getUserHasTrust(userId) || getUserUnlockedWithBiometric(userId) + || forceIsDismissibleIsKeepingDeviceUnlocked(); + } + + /** + * Whether the keyguard should be kept unlocked for the folding grace period. + */ + public boolean forceIsDismissibleIsKeepingDeviceUnlocked() { + if (mFoldGracePeriodProvider.isEnabled()) { + return mForceIsDismissible && isUnlockingWithForceKeyguardDismissibleAllowed(); + } + return false; } public boolean getUserHasTrust(int userId) { @@ -1386,7 +1421,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab /** * Returns whether the user is unlocked with face. - * @deprecated Use {@link KeyguardFaceAuthInteractor#isAuthenticated()} instead + * @deprecated Use {@link DeviceEntryFaceAuthInteractor#isAuthenticated()} instead */ @Deprecated public boolean isCurrentUserUnlockedWithFace() { @@ -1467,6 +1502,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return isUnlockingWithBiometricAllowed(true); } + private boolean isUnlockingWithForceKeyguardDismissibleAllowed() { + return isUnlockingWithBiometricAllowed(false); + } + public boolean isUnlockingWithBiometricAllowed(boolean isStrongBiometric) { // StrongAuthTracker#isUnlockingWithBiometricAllowed includes // STRONG_AUTH_REQUIRED_AFTER_LOCKOUT which is the same as mFingerprintLockedOutPermanent; @@ -1803,6 +1842,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab public void onFingerprintDetected(int sensorId, int userId, boolean isStrongBiometric) { handleBiometricDetected(userId, FINGERPRINT, isStrongBiometric); + setFingerprintRunningState(BIOMETRIC_STATE_STOPPED); } }; @@ -1977,6 +2017,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab protected void handleStartedGoingToSleep(int arg1) { Assert.isMainThread(); + setForceIsDismissibleKeyguard(false); clearFingerprintRecognized(); for (int i = 0; i < mCallbacks.size(); i++) { KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); @@ -2064,6 +2105,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab @VisibleForTesting void resetBiometricListeningState() { mFingerprintRunningState = BIOMETRIC_STATE_STOPPED; + mFingerprintDetectRunning = false; } @VisibleForTesting @@ -2455,7 +2497,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab /** * @return true if there's at least one face enrolled - * @deprecated Use {@link KeyguardFaceAuthInteractor#isFaceAuthEnabledAndEnrolled()} + * @deprecated Use {@link DeviceEntryFaceAuthInteractor#isFaceAuthEnabledAndEnrolled()} */ @Deprecated public boolean isFaceEnabledAndEnrolled() { @@ -2502,8 +2544,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return; } final boolean shouldListenForFingerprint = shouldListenForFingerprint(isUdfpsSupported()); - final boolean runningOrRestarting = mFingerprintRunningState == BIOMETRIC_STATE_RUNNING + final boolean running = mFingerprintRunningState == BIOMETRIC_STATE_RUNNING; + final boolean runningOrRestarting = running || mFingerprintRunningState == BIOMETRIC_STATE_CANCELLING_RESTARTING; + final boolean runDetect = shouldRunFingerprintDetect(); if (runningOrRestarting && !shouldListenForFingerprint) { if (action == BIOMETRIC_ACTION_START) { mLogger.v("Ignoring stopListeningForFingerprint()"); @@ -2515,10 +2559,24 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mLogger.v("Ignoring startListeningForFingerprint()"); return; } - startListeningForFingerprint(); + startListeningForFingerprint(runDetect); + } else if (running && runDetect && !mFingerprintDetectRunning) { + if (action == BIOMETRIC_ACTION_STOP) { + mLogger.v("Ignoring startListeningForFingerprint(detect)"); + return; + } + // stop running authentication and start running fingerprint detection + stopListeningForFingerprint(); + startListeningForFingerprint(true); } } + private boolean shouldRunFingerprintDetect() { + return !isUnlockingWithFingerprintAllowed() + || (Flags.runFingerprintDetectOnDismissibleKeyguard() + && getUserCanSkipBouncer(mSelectedUserInteractor.getSelectedUserId())); + } + /** * If a user is encrypted or not. * This is NOT related to the lock screen being visible or not. @@ -2774,7 +2832,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab && biometricEnabledForUser && !isUserInLockdown(user); final boolean strongerAuthRequired = !isUnlockingWithFingerprintAllowed(); - final boolean isSideFps = isSfpsSupported() && isSfpsEnrolled(); final boolean shouldListenBouncerState = !strongerAuthRequired || !mPrimaryBouncerIsOrWillBeShowing; @@ -2820,7 +2877,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab /** * If face auth is allows to scan on this exact moment. * - * @deprecated Use {@link KeyguardFaceAuthInteractor#canFaceAuthRun()} + * @deprecated Use {@link DeviceEntryFaceAuthInteractor#canFaceAuthRun()} */ @Deprecated public boolean shouldListenForFace() { @@ -2837,7 +2894,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } } - private void startListeningForFingerprint() { + private void startListeningForFingerprint(boolean runDetect) { final int userId = mSelectedUserInteractor.getSelectedUserId(); final boolean unlockPossible = isUnlockWithFingerprintPossible(userId); if (mFingerprintCancelSignal != null) { @@ -2867,18 +2924,20 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mFingerprintInteractiveToAuthProvider.getVendorExtension(userId)); } - if (!isUnlockingWithFingerprintAllowed()) { + if (runDetect) { mLogger.v("startListeningForFingerprint - detect"); mFpm.detectFingerprint( mFingerprintCancelSignal, mFingerprintDetectionCallback, fingerprintAuthenticateOptions); + mFingerprintDetectRunning = true; } else { mLogger.v("startListeningForFingerprint"); mFpm.authenticate(null /* crypto */, mFingerprintCancelSignal, mFingerprintAuthenticationCallback, null /* handler */, fingerprintAuthenticateOptions); + mFingerprintDetectRunning = false; } setFingerprintRunningState(BIOMETRIC_STATE_RUNNING); } @@ -2889,7 +2948,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } /** - * @deprecated Use {@link KeyguardFaceAuthInteractor#isLockedOut()} + * @deprecated Use {@link DeviceEntryFaceAuthInteractor#isLockedOut()} */ @Deprecated public boolean isFaceLockedOut() { @@ -2930,7 +2989,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } /** - * @deprecated Use {@link KeyguardFaceAuthInteractor#isFaceAuthEnabledAndEnrolled()} + * @deprecated Use {@link DeviceEntryFaceAuthInteractor#isFaceAuthEnabledAndEnrolled()} */ @VisibleForTesting @Deprecated @@ -3031,6 +3090,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab void handleUserSwitching(int userId, Runnable resultCallback) { mLogger.logUserSwitching(userId, "from UserTracker"); Assert.isMainThread(); + setForceIsDismissibleKeyguard(false); clearFingerprintRecognized(); boolean trustUsuallyManaged = mTrustManager.isTrustUsuallyManaged(userId); mLogger.logTrustUsuallyManagedUpdated(userId, mUserTrustIsUsuallyManaged.get(userId), @@ -3609,6 +3669,31 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } } + private void setForceIsDismissibleKeyguard(boolean forceIsDismissible) { + Assert.isMainThread(); + if (!mFoldGracePeriodProvider.isEnabled()) { + // never send updates if the feature isn't enabled + return; + } + if (mKeyguardShowing && forceIsDismissible) { + // never keep the device unlocked if the keyguard was already showing + mLogger.d("Skip setting forceIsDismissibleKeyguard to true. " + + "Keyguard already showing."); + return; + } + if (mForceIsDismissible != forceIsDismissible) { + mForceIsDismissible = forceIsDismissible; + mLogger.logForceIsDismissibleKeyguard(mForceIsDismissible); + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onForceIsDismissibleChanged(forceIsDismissibleIsKeepingDeviceUnlocked()); + } + } + } + + } + public boolean isSimPinVoiceSecure() { // TODO: only count SIMs that handle voice return isSimPinSecure(); @@ -3870,10 +3955,14 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab @Override public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { pw.println("KeyguardUpdateMonitor state:"); + pw.println(" forceIsDismissible=" + mForceIsDismissible); + pw.println(" forceIsDismissibleIsKeepingDeviceUnlocked=" + + forceIsDismissibleIsKeepingDeviceUnlocked()); pw.println(" getUserHasTrust()=" + getUserHasTrust( mSelectedUserInteractor.getSelectedUserId())); pw.println(" getUserUnlockedWithBiometric()=" + getUserUnlockedWithBiometric(mSelectedUserInteractor.getSelectedUserId())); + pw.println(" mFingerprintDetectRunning=" + mFingerprintDetectRunning); pw.println(" SIM States:"); for (SimData data : mSimDatas.values()) { pw.println(" " + data.toString()); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java index 9d216dcede2b..7ac5ac229793 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java @@ -339,4 +339,9 @@ public class KeyguardUpdateMonitorCallback { * On biometric enrollment state changed */ public void onBiometricEnrollmentStateChanged(BiometricSourceType biometricSourceType) { } + + /** + * On force is dismissible state changed. + */ + public void onForceIsDismissibleChanged(boolean forceIsDismissible) { } } diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java index 175fcdb6e11a..d5dc85cd8715 100644 --- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java @@ -26,7 +26,6 @@ import static com.android.systemui.Flags.keyguardBottomAreaRefactor; import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset; import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1; import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED; -import static com.android.systemui.Flags.newAodTransition; import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.annotation.SuppressLint; @@ -395,16 +394,6 @@ public class LockIconViewController implements Dumpable { mView.updateIcon(ICON_LOCK, true); mView.setContentDescription(mLockedLabel); mView.setVisibility(View.VISIBLE); - } else if (mIsDozing && newAodTransition()) { - mView.animate() - .alpha(0f) - .setDuration(FADE_OUT_DURATION_MS) - .withEndAction(() -> { - mView.clearIcon(); - mView.setVisibility(View.INVISIBLE); - mView.setContentDescription(null); - }) - .start(); } else { mView.clearIcon(); mView.setVisibility(View.INVISIBLE); diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt index 055ca565a933..1f4e732c21e4 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt @@ -639,4 +639,12 @@ constructor(@KeyguardUpdateMonitorLog private val logBuffer: LogBuffer) { { "fingerprint acquire message: $int1" } ) } + fun logForceIsDismissibleKeyguard(keepUnlocked: Boolean) { + logBuffer.log( + TAG, + DEBUG, + { bool1 = keepUnlocked }, + { "keepUnlockedOnFold changed to: $bool1" } + ) + } } diff --git a/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt b/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt index 603471b1de41..7a560e846318 100644 --- a/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt +++ b/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt @@ -19,6 +19,7 @@ package com.android.keyguard.mediator import android.annotation.BinderThread import android.os.Handler import android.os.Trace +import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.unfold.SysUIUnfoldComponent @@ -59,8 +60,11 @@ class ScreenOnCoordinator @Inject constructor( foldAodAnimationController?.onScreenTurningOn(pendingTasks.registerTask("fold-to-aod")) pendingTasks.onTasksComplete { - mainHandler.post { + if (Flags.enableBackgroundKeyguardOndrawnCallback()) { + // called by whatever thread completes the last task registered. onDrawn.run() + } else { + mainHandler.post { onDrawn.run() } } } Trace.endSection() diff --git a/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt index 066cba230b76..6076f32486b9 100644 --- a/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt @@ -22,10 +22,9 @@ import android.window.OnBackInvokedCallback import android.window.OnBackInvokedDispatcher import android.window.WindowOnBackInvokedDispatcher import com.android.systemui.CoreStartable +import com.android.systemui.Flags.predictiveBackAnimateShade import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor import com.android.systemui.shade.QuickSettingsController @@ -48,14 +47,13 @@ constructor( private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager, private val shadeController: ShadeController, private val notificationShadeWindowController: NotificationShadeWindowController, - private val windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor, - featureFlags: FeatureFlags, + private val windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor ) : CoreStartable { private var isCallbackRegistered = false private val callback = - if (featureFlags.isEnabled(Flags.WM_SHADE_ANIMATE_BACK_GESTURE)) { + if (predictiveBackAnimateShade()) { /** * New callback that handles back gesture invoked, cancel, progress and provides * feedback via Shade animation. diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt index 634531215002..45967c600a3c 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt @@ -32,13 +32,13 @@ import com.android.keyguard.logging.KeyguardLogger import com.android.settingslib.Utils import com.android.systemui.CoreStartable import com.android.systemui.Flags.lightRevealMigration -import com.android.systemui.res.R import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.flags.FeatureFlags +import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.log.core.LogLevel import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.res.R import com.android.systemui.statusbar.CircleReveal import com.android.systemui.statusbar.LiftReveal import com.android.systemui.statusbar.LightRevealEffect @@ -50,6 +50,7 @@ import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.ViewController +import kotlinx.coroutines.ExperimentalCoroutinesApi import java.io.PrintWriter import javax.inject.Inject import javax.inject.Provider @@ -62,6 +63,7 @@ import javax.inject.Provider * * The ripple uses the accent color of the current theme. */ +@ExperimentalCoroutinesApi @SysUISingleton class AuthRippleController @Inject constructor( private val sysuiContext: Context, @@ -75,7 +77,6 @@ class AuthRippleController @Inject constructor( private val udfpsControllerProvider: Provider<UdfpsController>, private val statusBarStateController: StatusBarStateController, private val displayMetrics: DisplayMetrics, - private val featureFlags: FeatureFlags, private val logger: KeyguardLogger, private val biometricUnlockController: BiometricUnlockController, private val lightRevealScrim: LightRevealScrim, @@ -313,6 +314,18 @@ class AuthRippleController @Inject constructor( mView.fadeDwellRipple() } } + + override fun onBiometricDetected( + userId: Int, + biometricSourceType: BiometricSourceType, + isStrongBiometric: Boolean + ) { + // TODO (b/309804148): add support detect auth ripple for deviceEntryUdfpsRefactor + if (!DeviceEntryUdfpsRefactor.isEnabled && + keyguardUpdateMonitor.getUserCanSkipBouncer(userId)) { + showUnlockRipple(biometricSourceType) + } + } } private val configurationChangedListener = diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt b/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt index cb750493ba26..3ea1632b20d7 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt @@ -22,7 +22,7 @@ import android.view.View import android.view.accessibility.AccessibilityNodeInfo import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.res.R import javax.inject.Inject @@ -35,7 +35,7 @@ class FaceAuthAccessibilityDelegate @Inject constructor( @Main private val resources: Resources, - private val faceAuthInteractor: KeyguardFaceAuthInteractor, + private val faceAuthInteractor: DeviceEntryFaceAuthInteractor, ) : View.AccessibilityDelegate() { override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) { super.onInitializeAccessibilityNodeInfo(host, info) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 2fd13b349438..d6646378681a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -80,11 +80,11 @@ import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor; import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor; import com.android.systemui.doze.DozeReceiver; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.ScreenLifecycle; -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.FalsingManager; @@ -149,7 +149,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { @NonNull private final DumpManager mDumpManager; @NonNull private final SystemUIDialogManager mDialogManager; @NonNull private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; - @NonNull private final KeyguardFaceAuthInteractor mKeyguardFaceAuthInteractor; + @NonNull private final DeviceEntryFaceAuthInteractor mDeviceEntryFaceAuthInteractor; @NonNull private final VibratorHelper mVibrator; @NonNull private final FalsingManager mFalsingManager; @NonNull private final PowerManager mPowerManager; @@ -664,7 +664,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { @NonNull SessionTracker sessionTracker, @NonNull AlternateBouncerInteractor alternateBouncerInteractor, @NonNull InputManager inputManager, - @NonNull KeyguardFaceAuthInteractor keyguardFaceAuthInteractor, + @NonNull DeviceEntryFaceAuthInteractor deviceEntryFaceAuthInteractor, @NonNull UdfpsKeyguardAccessibilityDelegate udfpsKeyguardAccessibilityDelegate, @NonNull SelectedUserInteractor selectedUserInteractor, @NonNull FpsUnlockTracker fpsUnlockTracker, @@ -736,7 +736,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { } return Unit.INSTANCE; }); - mKeyguardFaceAuthInteractor = keyguardFaceAuthInteractor; + mDeviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor; final UdfpsOverlayController mUdfpsOverlayController = new UdfpsOverlayController(); mFingerprintManager.setUdfpsOverlayController(mUdfpsOverlayController); @@ -1027,7 +1027,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { if (!mOnFingerDown) { playStartHaptic(); - mKeyguardFaceAuthInteractor.onUdfpsSensorTouched(); + mDeviceEntryFaceAuthInteractor.onUdfpsSensorTouched(); } mOnFingerDown = true; mFingerprintManager.onPointerDown(requestId, mSensorProps.sensorId, pointerId, x, y, diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt index f4231ac01fee..348b54e0c7f6 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt @@ -26,6 +26,8 @@ import com.android.systemui.biometrics.shared.model.DisplayRotation import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.isDefaultOrientation import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.log.SideFpsLogger import com.android.systemui.res.R import java.util.Optional @@ -47,9 +49,13 @@ constructor( windowManager: WindowManager, displayStateInteractor: DisplayStateInteractor, fingerprintInteractiveToAuthProvider: Optional<FingerprintInteractiveToAuthProvider>, + keyguardTransitionInteractor: KeyguardTransitionInteractor, private val logger: SideFpsLogger, ) { + private val isProlongedTouchEnabledForDevice = + context.resources.getBoolean(R.bool.config_restToUnlockSupported) + private val sensorLocationForCurrentDisplay = combine( displayStateInteractor.displayChanges, @@ -62,11 +68,24 @@ constructor( val isAvailable: Flow<Boolean> = fingerprintPropertyRepository.sensorType.map { it == FingerprintSensorType.POWER_BUTTON } - val authenticationDuration: Long = - context.resources?.getInteger(R.integer.config_restToUnlockDuration)?.toLong() ?: 0L + val authenticationDuration: Flow<Long> = + keyguardTransitionInteractor + .isFinishedInStateWhere { it == KeyguardState.OFF || it == KeyguardState.DOZING } + .map { + if (it) + context.resources + ?.getInteger(R.integer.config_restToUnlockDurationScreenOff) + ?.toLong() + else + context.resources + ?.getInteger(R.integer.config_restToUnlockDurationDefault) + ?.toLong() + } + .map { it ?: 0L } + .onEach { logger.authDurationChanged(it) } val isProlongedTouchRequiredForAuthentication: Flow<Boolean> = - if (fingerprintInteractiveToAuthProvider.isEmpty) { + if (fingerprintInteractiveToAuthProvider.isEmpty || !isProlongedTouchEnabledForDevice) { flowOf(false) } else { combine( diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt index 90e4a3821634..7b8cb829e03f 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt @@ -77,6 +77,13 @@ object BiometricViewBinder { applicationScope: CoroutineScope, vibratorHelper: VibratorHelper, ): Spaghetti { + /** + * View is only set visible in BiometricViewSizeBinder once PromptSize is determined that + * accounts for iconView size, to prevent prompt resizing being visible to the user. + * + * TODO(b/288175072): May be able to remove this once constraint layout is implemented + */ + view.visibility = View.INVISIBLE val accessibilityManager = view.context.getSystemService(AccessibilityManager::class.java)!! val textColorError = @@ -102,7 +109,7 @@ object BiometricViewBinder { iconView, iconOverlayView, view.getUpdatedFingerprintAffordanceSize(), - viewModel.iconViewModel + viewModel ) val indicatorMessageView = view.requireViewById<TextView>(R.id.indicator) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt index 7e16d1e1d668..1a7b6c974d0c 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt @@ -30,7 +30,6 @@ import androidx.core.animation.addListener import androidx.core.view.doOnLayout import androidx.core.view.isGone import androidx.lifecycle.lifecycleScope -import com.android.systemui.res.R import com.android.systemui.biometrics.AuthPanelController import com.android.systemui.biometrics.Utils import com.android.systemui.biometrics.ui.BiometricPromptLayout @@ -41,6 +40,8 @@ import com.android.systemui.biometrics.ui.viewmodel.isMedium import com.android.systemui.biometrics.ui.viewmodel.isNullOrNotSmall import com.android.systemui.biometrics.ui.viewmodel.isSmall import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.res.R +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch /** Helper for [BiometricViewBinder] to handle resize transitions. */ @@ -93,7 +94,20 @@ object BiometricViewSizeBinder { view.repeatWhenAttached { var currentSize: PromptSize? = null lifecycleScope.launch { - viewModel.size.collect { size -> + /** + * View is only set visible in BiometricViewSizeBinder once PromptSize is + * determined that accounts for iconView size, to prevent prompt resizing being + * visible to the user. + * + * TODO(b/288175072): May be able to remove isIconViewLoaded once constraint + * layout is implemented + */ + combine(viewModel.isIconViewLoaded, viewModel.size, ::Pair).collect { + (isIconViewLoaded, size) -> + if (!isIconViewLoaded) { + return@collect + } + // prepare for animated size transitions for (v in viewsToHideWhenSmall) { v.showTextOrHide(forceHide = size.isSmall) @@ -198,6 +212,8 @@ object BiometricViewSizeBinder { } currentSize = size + view.visibility = View.VISIBLE + viewModel.setIsIconViewLoaded(false) notifyAccessibilityChanged() } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt index 475ef18e5099..6e3bcf575072 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt @@ -26,6 +26,7 @@ import com.airbnb.lottie.LottieAnimationView import com.android.settingslib.widget.LottieColorUtils import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel.AuthType +import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.util.kotlin.Utils.Companion.toQuad import com.android.systemui.util.kotlin.Utils.Companion.toQuint @@ -45,8 +46,9 @@ object PromptIconViewBinder { iconView: LottieAnimationView, iconOverlayView: LottieAnimationView, iconViewLayoutParamSizeOverride: Pair<Int, Int>?, - viewModel: PromptIconViewModel + promptViewModel: PromptViewModel ) { + val viewModel = promptViewModel.iconViewModel iconView.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.onConfigurationChanged(iconView.context.resources.configuration) @@ -71,25 +73,45 @@ object PromptIconViewBinder { } launch { + var width: Int + var height: Int viewModel.activeAuthType.collect { activeAuthType -> - if (iconViewLayoutParamSizeOverride == null) { - val width: Int - val height: Int - when (activeAuthType) { - AuthType.Fingerprint, - AuthType.Coex -> { - width = viewModel.fingerprintIconWidth - height = viewModel.fingerprintIconHeight - } - AuthType.Face -> { - width = viewModel.faceIconWidth - height = viewModel.faceIconHeight + when (activeAuthType) { + AuthType.Fingerprint, + AuthType.Coex -> { + width = viewModel.fingerprintIconWidth + height = viewModel.fingerprintIconHeight + + /** + * View is only set visible in BiometricViewSizeBinder once + * PromptSize is determined that accounts for iconView size, to + * prevent prompt resizing being visible to the user. + * + * TODO(b/288175072): May be able to remove this once constraint + * layout is implemented + */ + iconView.removeAllLottieOnCompositionLoadedListener() + iconView.addLottieOnCompositionLoadedListener { + promptViewModel.setIsIconViewLoaded(true) } } + AuthType.Face -> { + width = viewModel.faceIconWidth + height = viewModel.faceIconHeight + /** + * Set to true by default since face icon is a drawable, which + * doesn't have a LottieOnCompositionLoadedListener equivalent. + * + * TODO(b/318569643): To be updated once face assets are updated + * from drawables + */ + promptViewModel.setIsIconViewLoaded(true) + } + } + if (iconViewLayoutParamSizeOverride == null) { iconView.layoutParams.width = width iconView.layoutParams.height = height - iconOverlayView.layoutParams.width = width iconOverlayView.layoutParams.height = height } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt index 6d0a58e202bd..d899827ebb2e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt @@ -192,6 +192,28 @@ constructor( val iconViewModel: PromptIconViewModel = PromptIconViewModel(this, displayStateInteractor, promptSelectorInteractor) + private val _isIconViewLoaded = MutableStateFlow(false) + + /** + * For prompts with an iconView, false until the prompt's iconView animation has been loaded in + * the view, otherwise true by default. Used for BiometricViewSizeBinder to wait for the icon + * asset to be loaded before determining the prompt size. + */ + val isIconViewLoaded: Flow<Boolean> = + combine(credentialKind, _isIconViewLoaded.asStateFlow()) { credentialKind, isIconViewLoaded + -> + if (credentialKind is PromptKind.Biometric) { + isIconViewLoaded + } else { + true + } + } + + // Sets whether the prompt's iconView animation has been loaded in the view yet. + fun setIsIconViewLoaded(iconViewLoaded: Boolean) { + _isIconViewLoaded.value = iconViewLoaded + } + /** Padding for prompt UI elements */ val promptPadding: Flow<Rect> = combine(size, displayStateInteractor.currentRotation) { size, rotation -> diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt index 81d822fa7b03..c8ce245e48bf 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt @@ -29,7 +29,7 @@ import com.android.systemui.classifier.FalsingClassifier import com.android.systemui.classifier.domain.interactor.FalsingInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.res.R import javax.inject.Inject @@ -52,7 +52,7 @@ constructor( @Application private val applicationContext: Context, private val repository: BouncerRepository, private val authenticationInteractor: AuthenticationInteractor, - private val keyguardFaceAuthInteractor: KeyguardFaceAuthInteractor, + private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor, private val falsingInteractor: FalsingInteractor, private val powerInteractor: PowerInteractor, private val simBouncerInteractor: SimBouncerInteractor, @@ -100,7 +100,7 @@ constructor( * user's pocket or by the user's face while holding their device up to their ear. */ fun onIntentionalUserInput() { - keyguardFaceAuthInteractor.onPrimaryBouncerUserInput() + deviceEntryFaceAuthInteractor.onPrimaryBouncerUserInput() powerInteractor.onUserTouch() falsingInteractor.updateFalseConfidence(FalsingClassifier.Result.passed(0.6)) } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt index b587ed846a38..ef4554cfdd7c 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt @@ -30,9 +30,9 @@ import com.android.systemui.bouncer.shared.model.BouncerMessageModel import com.android.systemui.bouncer.shared.model.Message import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository import com.android.systemui.flags.SystemPropertiesHelper import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository -import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.TrustRepository import com.android.systemui.res.R.string.bouncer_face_not_recognized diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt index 621ca5dc6f5a..654fa220d491 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt @@ -38,9 +38,9 @@ import com.android.systemui.classifier.FalsingCollector import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.repository.TrustRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.shared.system.SysUiStatsLog @@ -77,7 +77,7 @@ constructor( private val trustRepository: TrustRepository, @Application private val applicationScope: CoroutineScope, private val selectedUserInteractor: SelectedUserInteractor, - private val keyguardFaceAuthInteractor: KeyguardFaceAuthInteractor, + private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor, ) { private val passiveAuthBouncerDelay = context.resources.getInteger(R.integer.primary_bouncer_passive_auth_delay).toLong() @@ -429,7 +429,7 @@ constructor( keyguardUpdateMonitor.canTriggerActiveUnlockBasedOnDeviceState() return !needsFullscreenBouncer() && - (keyguardFaceAuthInteractor.canFaceAuthRun() || canRunActiveUnlock) + (deviceEntryFaceAuthInteractor.canFaceAuthRun() || canRunActiveUnlock) } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/common/data/CommonDataLayerModule.kt b/packages/SystemUI/src/com/android/systemui/common/data/CommonDataLayerModule.kt index 27c9b3fa7c9e..d1c728cd74fa 100644 --- a/packages/SystemUI/src/com/android/systemui/common/data/CommonDataLayerModule.kt +++ b/packages/SystemUI/src/com/android/systemui/common/data/CommonDataLayerModule.kt @@ -16,6 +16,8 @@ package com.android.systemui.common.data +import com.android.systemui.common.data.repository.PackageChangeRepository +import com.android.systemui.common.data.repository.PackageChangeRepositoryImpl import com.android.systemui.common.ui.data.repository.ConfigurationRepository import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryImpl import dagger.Binds @@ -23,5 +25,13 @@ import dagger.Module @Module abstract class CommonDataLayerModule { - @Binds abstract fun bindRepository(impl: ConfigurationRepositoryImpl): ConfigurationRepository + @Binds + abstract fun bindConfigurationRepository( + impl: ConfigurationRepositoryImpl + ): ConfigurationRepository + + @Binds + abstract fun bindPackageChangeRepository( + impl: PackageChangeRepositoryImpl + ): PackageChangeRepository } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundComponent.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepository.kt index d382d2063f06..7c7b3db53b51 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepository.kt @@ -14,23 +14,18 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.impl.custom.di.bound +package com.android.systemui.common.data.repository import android.os.UserHandle -import dagger.BindsInstance -import dagger.Subcomponent -import kotlinx.coroutines.CoroutineScope +import com.android.systemui.common.data.shared.model.PackageChangeModel +import kotlinx.coroutines.flow.Flow -/** @see CustomTileBoundScope */ -@CustomTileBoundScope -@Subcomponent(modules = [CustomTileBoundModule::class]) -interface CustomTileBoundComponent { - - @Subcomponent.Builder - interface Builder { - @BindsInstance fun user(@CustomTileUser user: UserHandle): Builder - @BindsInstance fun coroutineScope(@CustomTileBoundScope scope: CoroutineScope): Builder - - fun build(): CustomTileBoundComponent - } +interface PackageChangeRepository { + /** + * Emits values when packages for the specified user are changed. See supported modifications in + * [PackageChangeModel] + * + * [UserHandle.USER_ALL] may be used to listen to all users. + */ + fun packageChanged(user: UserHandle): Flow<PackageChangeModel> } diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt new file mode 100644 index 000000000000..b1b348c1600a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 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.common.data.repository + +import android.os.UserHandle +import com.android.systemui.common.data.shared.model.PackageChangeModel +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter + +@SysUISingleton +class PackageChangeRepositoryImpl +@Inject +constructor( + private val monitorFactory: PackageUpdateMonitor.Factory, +) : PackageChangeRepository { + /** + * A [PackageUpdateMonitor] which monitors package updates for all users. The per-user filtering + * is done by [packageChanged]. + */ + private val monitor by lazy { monitorFactory.create(UserHandle.ALL) } + + override fun packageChanged(user: UserHandle): Flow<PackageChangeModel> = + monitor.packageChanged.filter { + user == UserHandle.ALL || user == UserHandle.getUserHandleForUid(it.packageUid) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateLogger.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateLogger.kt new file mode 100644 index 000000000000..adbb37cc3a42 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateLogger.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2023 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.common.data.repository + +import android.os.UserHandle +import com.android.systemui.common.data.shared.model.PackageChangeModel +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel +import com.android.systemui.log.dagger.PackageChangeRepoLog +import javax.inject.Inject + +private fun getChangeString(model: PackageChangeModel) = + when (model) { + is PackageChangeModel.Installed -> "installed" + is PackageChangeModel.Uninstalled -> "uninstalled" + is PackageChangeModel.UpdateStarted -> "started updating" + is PackageChangeModel.UpdateFinished -> "finished updating" + is PackageChangeModel.Changed -> "changed" + } + +/** A debug logger for [PackageChangeRepository]. */ +@SysUISingleton +class PackageUpdateLogger @Inject constructor(@PackageChangeRepoLog private val buffer: LogBuffer) { + + fun logChange(model: PackageChangeModel) { + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = model.packageName + str2 = getChangeString(model) + int1 = model.packageUid + }, + { + val user = UserHandle.getUserHandleForUid(int1) + "Package $str1 ($int1) $str2 on user $user" + } + ) + } +} + +private const val TAG = "PackageChangeRepoLog" diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateMonitor.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateMonitor.kt new file mode 100644 index 000000000000..f7cc34457079 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateMonitor.kt @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2023 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.common.data.repository + +import android.content.Context +import android.os.Handler +import android.os.UserHandle +import com.android.internal.content.PackageMonitor +import com.android.systemui.common.data.shared.model.PackageChangeModel +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach + +/** + * A wrapper around [PackageMonitor] which exposes package updates as a flow. + * + * External clients should use [PackageChangeRepository] instead to ensure only a single callback is + * registered for all of SystemUI. + */ +class PackageUpdateMonitor +@AssistedInject +constructor( + @Assisted private val user: UserHandle, + @Background private val bgDispatcher: CoroutineDispatcher, + @Background private val bgHandler: Handler, + @Application private val context: Context, + @Application private val scope: CoroutineScope, + private val logger: PackageUpdateLogger, +) : PackageMonitor() { + + @AssistedFactory + fun interface Factory { + fun create(user: UserHandle): PackageUpdateMonitor + } + + var isActive = false + private set + + private val _packageChanged = + MutableSharedFlow<PackageChangeModel>(replay = 0, extraBufferCapacity = BUFFER_CAPACITY) + .apply { + // Automatically register/unregister as needed, depending on whether + // there are subscribers to this flow. + subscriptionCount + .map { it > 0 } + .distinctUntilChanged() + .onEach { active -> + if (active) { + register(context, user, bgHandler) + } else if (isActive) { + // Avoid calling unregister if we were not previously active, as this + // will cause an IllegalStateException. + unregister() + } + isActive = active + } + .flowOn(bgDispatcher) + .launchIn(scope) + } + + val packageChanged: Flow<PackageChangeModel> + get() = _packageChanged.onEach(logger::logChange) + + override fun onPackageAdded(packageName: String, uid: Int) { + super.onPackageAdded(packageName, uid) + _packageChanged.tryEmit(PackageChangeModel.Installed(packageName, uid)) + } + + override fun onPackageRemoved(packageName: String, uid: Int) { + super.onPackageRemoved(packageName, uid) + _packageChanged.tryEmit(PackageChangeModel.Uninstalled(packageName, uid)) + } + + override fun onPackageChanged( + packageName: String, + uid: Int, + components: Array<out String> + ): Boolean { + super.onPackageChanged(packageName, uid, components) + _packageChanged.tryEmit(PackageChangeModel.Changed(packageName, uid)) + return false + } + + override fun onPackageUpdateStarted(packageName: String, uid: Int) { + super.onPackageUpdateStarted(packageName, uid) + _packageChanged.tryEmit(PackageChangeModel.UpdateStarted(packageName, uid)) + } + + override fun onPackageUpdateFinished(packageName: String, uid: Int) { + super.onPackageUpdateFinished(packageName, uid) + _packageChanged.tryEmit(PackageChangeModel.UpdateFinished(packageName, uid)) + } + + private companion object { + // This capacity is the number of package changes that we will keep buffered in the shared + // flow. It is unlikely that at any given time there would be this many changes being + // processed by consumers, but this is done just in case that many packages are changed at + // the same time and there is backflow due to consumers processing the changes more slowly + // than they are being emitted. + const val BUFFER_CAPACITY = 100 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/common/data/shared/model/PackageChangeModel.kt b/packages/SystemUI/src/com/android/systemui/common/data/shared/model/PackageChangeModel.kt new file mode 100644 index 000000000000..853eff77b66c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/data/shared/model/PackageChangeModel.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 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.common.data.shared.model + +import android.content.Intent + +/** Represents changes to an installed package. */ +sealed interface PackageChangeModel { + val packageName: String + val packageUid: Int + + /** + * An existing application package was uninstalled. + * + * Equivalent to receiving the [Intent.ACTION_PACKAGE_REMOVED] broadcast with + * [Intent.EXTRA_REPLACING] set to false. + */ + data class Uninstalled(override val packageName: String, override val packageUid: Int) : + PackageChangeModel + + /** + * A new version of an existing application is going to be installed. + * + * Equivalent to receiving the [Intent.ACTION_PACKAGE_REMOVED] broadcast with + * [Intent.EXTRA_REPLACING] set to true. + */ + data class UpdateStarted(override val packageName: String, override val packageUid: Int) : + PackageChangeModel + + /** + * A new version of an existing application package has been installed, replacing the old + * version. + * + * Equivalent to receiving the [Intent.ACTION_PACKAGE_ADDED] broadcast with + * [Intent.EXTRA_REPLACING] set to true. + */ + data class UpdateFinished(override val packageName: String, override val packageUid: Int) : + PackageChangeModel + + /** + * A new application package has been installed. + * + * Equivalent to receiving the [Intent.ACTION_PACKAGE_ADDED] broadcast with + * [Intent.EXTRA_REPLACING] set to false. + */ + data class Installed(override val packageName: String, override val packageUid: Int) : + PackageChangeModel + + /** + * An existing application package has been changed (for example, a component has been enabled + * or disabled). + * + * Equivalent to receiving the [Intent.ACTION_PACKAGE_CHANGED] broadcast. + */ + data class Changed(override val packageName: String, override val packageUid: Int) : + PackageChangeModel +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt index 847b98e82d80..10768ea6122a 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt @@ -16,7 +16,6 @@ package com.android.systemui.communal.dagger -import android.content.Context import com.android.systemui.communal.data.db.CommunalDatabaseModule import com.android.systemui.communal.data.repository.CommunalMediaRepositoryModule import com.android.systemui.communal.data.repository.CommunalRepositoryModule @@ -24,9 +23,8 @@ import com.android.systemui.communal.data.repository.CommunalTutorialRepositoryM import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule import com.android.systemui.communal.widgets.EditWidgetsActivityStarter import com.android.systemui.communal.widgets.EditWidgetsActivityStarterImpl -import com.android.systemui.dagger.qualifiers.Application +import dagger.Binds import dagger.Module -import dagger.Provides @Module( includes = @@ -38,11 +36,9 @@ import dagger.Provides CommunalDatabaseModule::class, ] ) -class CommunalModule { - @Provides - fun provideEditWidgetsActivityStarter( - @Application context: Context - ): EditWidgetsActivityStarter { - return EditWidgetsActivityStarterImpl(context) - } +interface CommunalModule { + @Binds + fun bindEditWidgetsActivityStarter( + starter: EditWidgetsActivityStarterImpl + ): EditWidgetsActivityStarter } 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 d1bbe5743a24..cab8adfc0bd9 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 @@ -38,6 +38,7 @@ import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog import com.android.systemui.settings.UserTracker +import java.util.Optional import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -75,7 +76,7 @@ interface CommunalWidgetRepository { class CommunalWidgetRepositoryImpl @Inject constructor( - private val appWidgetManager: AppWidgetManager, + private val appWidgetManager: Optional<AppWidgetManager>, private val appWidgetHost: AppWidgetHost, @Application private val applicationScope: CoroutineScope, @Background private val bgDispatcher: CoroutineDispatcher, @@ -144,7 +145,7 @@ constructor( override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = isHostActive.flatMapLatest { isHostActive -> - if (!isHostActive) { + if (!isHostActive || !appWidgetManager.isPresent) { return@flatMapLatest flowOf(emptyList()) } communalWidgetDao.getWidgets().map { it.map(::mapToContentModel) } @@ -187,7 +188,7 @@ constructor( val (_, widgetId) = entry.value return CommunalWidgetContentModel( appWidgetId = widgetId, - providerInfo = appWidgetManager.getAppWidgetInfo(widgetId), + providerInfo = appWidgetManager.get().getAppWidgetInfo(widgetId), priority = entry.key.rank, ) } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt index 5793f1043aca..d0d9e3fabc7b 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt @@ -31,6 +31,7 @@ import com.android.systemui.res.R import dagger.Binds import dagger.Module import dagger.Provides +import java.util.Optional import javax.inject.Named @Module @@ -41,8 +42,8 @@ interface CommunalWidgetRepositoryModule { @SysUISingleton @Provides - fun provideAppWidgetManager(@Application context: Context): AppWidgetManager { - return AppWidgetManager.getInstance(context) + fun provideAppWidgetManager(@Application context: Context): Optional<AppWidgetManager> { + return Optional.ofNullable(AppWidgetManager.getInstance(context)) } @SysUISingleton @@ -54,7 +55,7 @@ interface CommunalWidgetRepositoryModule { @SysUISingleton @Provides fun provideCommunalWidgetHost( - appWidgetManager: AppWidgetManager, + appWidgetManager: Optional<AppWidgetManager>, appWidgetHost: AppWidgetHost, @CommunalLog logBuffer: LogBuffer, ): CommunalWidgetHost { 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 086d729e1a60..155de323d3a6 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt @@ -22,6 +22,7 @@ import android.content.ComponentName import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog +import java.util.Optional import javax.inject.Inject /** @@ -31,7 +32,7 @@ import javax.inject.Inject class CommunalWidgetHost @Inject constructor( - private val appWidgetManager: AppWidgetManager, + private val appWidgetManager: Optional<AppWidgetManager>, private val appWidgetHost: AppWidgetHost, @CommunalLog logBuffer: LogBuffer, ) { @@ -56,6 +57,10 @@ constructor( return null } - private fun bindWidget(widgetId: Int, provider: ComponentName): Boolean = - appWidgetManager.bindAppWidgetIdIfAllowed(widgetId, provider) + private fun bindWidget(widgetId: Int, provider: ComponentName): Boolean { + if (appWidgetManager.isPresent) { + return appWidgetManager.get().bindAppWidgetIdIfAllowed(widgetId, provider) + } + return false + } } 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 577e40412e4e..c34a8df1bed7 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 @@ -20,6 +20,7 @@ import android.content.ComponentName import android.os.PowerManager import android.os.SystemClock import android.view.MotionEvent +import android.widget.RemoteViews import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.model.CommunalSceneKey @@ -101,4 +102,7 @@ abstract class BaseCommunalViewModel( /** Called as the UI requests opening the widget editor. */ open fun onOpenWidgetEditor() {} + + /** Gets the interaction handler used to handle taps on a remote view */ + abstract fun getInteractionHandler(): RemoteViews.InteractionHandler } 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 368df9ea3693..da7bd34950df 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 @@ -17,6 +17,7 @@ package com.android.systemui.communal.ui.viewmodel import android.os.PowerManager +import android.widget.RemoteViews import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.dagger.SysUISingleton @@ -49,4 +50,9 @@ constructor( override fun onReorderWidgets(widgetIdToPriorityMap: Map<Int, Int>) = communalInteractor.updateWidgetOrder(widgetIdToPriorityMap) + + override fun getInteractionHandler(): RemoteViews.InteractionHandler { + // Ignore all interactions in edit mode. + return RemoteViews.InteractionHandler { _, _, _ -> false } + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index abf198637911..2fae8b533857 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -17,9 +17,11 @@ package com.android.systemui.communal.ui.viewmodel import android.os.PowerManager +import android.widget.RemoteViews import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalTutorialInteractor import com.android.systemui.communal.domain.model.CommunalContentModel +import com.android.systemui.communal.widgets.WidgetInteractionHandler import com.android.systemui.dagger.SysUISingleton import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.media.dagger.MediaModule @@ -39,6 +41,7 @@ class CommunalViewModel @Inject constructor( private val communalInteractor: CommunalInteractor, + private val interactionHandler: WidgetInteractionHandler, tutorialInteractor: CommunalTutorialInteractor, shadeViewController: Provider<ShadeViewController>, powerManager: PowerManager, @@ -60,4 +63,6 @@ constructor( } override fun onOpenWidgetEditor() = communalInteractor.showWidgetEditor() + + override fun getInteractionHandler(): RemoteViews.InteractionHandler = interactionHandler } 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 c936c636f5fc..0f94a92dd7ce 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt @@ -23,6 +23,7 @@ import android.os.Bundle import android.os.RemoteException import android.util.Log import android.view.IWindowManager +import android.view.WindowInsets import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult @@ -79,6 +80,10 @@ constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val windowInsetsController = window.decorView.windowInsetsController + windowInsetsController?.hide(WindowInsets.Type.systemBars()) + window.setDecorFitsSystemWindows(false) + setCommunalEditWidgetActivityContent( activity = this, viewModel = communalViewModel, diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarter.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarter.kt index 846e3000284f..55acad0e9ed5 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarter.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarter.kt @@ -19,17 +19,26 @@ package com.android.systemui.communal.widgets import android.content.Context import android.content.Intent import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.plugins.ActivityStarter +import javax.inject.Inject interface EditWidgetsActivityStarter { fun startActivity() } -class EditWidgetsActivityStarterImpl(@Application private val applicationContext: Context) : - EditWidgetsActivityStarter { +class EditWidgetsActivityStarterImpl +@Inject +constructor( + @Application private val applicationContext: Context, + private val activityStarter: ActivityStarter, +) : EditWidgetsActivityStarter { + override fun startActivity() { - applicationContext.startActivity( + activityStarter.startActivityDismissingKeyguard( Intent(applicationContext, EditWidgetsActivity::class.java) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK), + /* onlyProvisioned = */ true, + /* dismissShade = */ true, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt new file mode 100644 index 000000000000..c8db70bbc313 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt @@ -0,0 +1,50 @@ +/* + * 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.PendingIntent +import android.view.View +import android.widget.RemoteViews +import com.android.systemui.plugins.ActivityStarter +import javax.inject.Inject + +class WidgetInteractionHandler +@Inject +constructor( + private val activityStarter: ActivityStarter, +) : RemoteViews.InteractionHandler { + override fun onInteraction( + view: View, + pendingIntent: PendingIntent, + response: RemoteViews.RemoteResponse + ): Boolean = + when { + pendingIntent.isActivity -> startActivity(pendingIntent) + else -> + RemoteViews.startPendingIntent(view, pendingIntent, response.getLaunchOptions(view)) + } + + private fun startActivity(pendingIntent: PendingIntent): Boolean { + activityStarter.startPendingIntentMaybeDismissingKeyguard( + /* intent = */ pendingIntent, + /* intentSentUiThreadCallback = */ null, + // TODO(b/318758390): Properly animate activities started from widgets. + /* animationController = */ null + ) + return true + } +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt index 5df26b3176ff..a6b432019486 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt @@ -28,27 +28,26 @@ import javax.inject.Inject /** * Factory to create dialogs for consenting to show app panels for specific apps. * - * [internalDialogFactory] is for facilitating testing. + * [dialogFactory] is for facilitating testing. */ -class PanelConfirmationDialogFactory( - private val internalDialogFactory: (Context) -> SystemUIDialog +class PanelConfirmationDialogFactory @Inject constructor( + private val dialogFactory: SystemUIDialog.Factory ) { - @Inject constructor() : this({ SystemUIDialog(it) }) /** * Creates a dialog to show to the user. [response] will be true if an only if the user responds * affirmatively. */ fun createConfirmationDialog( - context: Context, - appName: CharSequence, - response: Consumer<Boolean> + context: Context, + appName: CharSequence, + response: Consumer<Boolean> ): Dialog { val listener = DialogInterface.OnClickListener { _, which -> response.accept(which == DialogInterface.BUTTON_POSITIVE) } - return internalDialogFactory(context).apply { + return dialogFactory.create(context).apply { setTitle(this.context.getString(R.string.controls_panel_authorization_title, appName)) setMessage(this.context.getString(R.string.controls_panel_authorization, appName)) setCanceledOnTouchOutside(true) diff --git a/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt b/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt index 0218f452a13b..20bfbc9c2ab8 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt @@ -26,6 +26,8 @@ import android.os.UserManager import androidx.annotation.WorkerThread import com.android.systemui.CoreStartable import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.common.data.shared.model.PackageChangeModel +import com.android.systemui.common.data.repository.PackageChangeRepository import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.management.ControlsListingController @@ -33,8 +35,16 @@ import com.android.systemui.controls.panels.AuthorizedPanelsRepository import com.android.systemui.controls.panels.SelectedComponentRepository import com.android.systemui.controls.ui.SelectedItem import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.settings.UserTracker +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import java.util.concurrent.Executor import javax.inject.Inject @@ -53,13 +63,16 @@ import javax.inject.Inject class ControlsStartable @Inject constructor( - @Background private val executor: Executor, - private val controlsComponent: ControlsComponent, - private val userTracker: UserTracker, - private val authorizedPanelsRepository: AuthorizedPanelsRepository, - private val selectedComponentRepository: SelectedComponentRepository, - private val userManager: UserManager, - private val broadcastDispatcher: BroadcastDispatcher, + @Application private val scope: CoroutineScope, + @Background private val bgDispatcher: CoroutineDispatcher, + @Background private val executor: Executor, + private val controlsComponent: ControlsComponent, + private val userTracker: UserTracker, + private val authorizedPanelsRepository: AuthorizedPanelsRepository, + private val selectedComponentRepository: SelectedComponentRepository, + private val packageChangeRepository: PackageChangeRepository, + private val userManager: UserManager, + private val broadcastDispatcher: BroadcastDispatcher, ) : CoreStartable { // These two controllers can only be accessed after `start` method once we've checked if the @@ -78,6 +91,8 @@ constructor( } } + private var packageJob: Job? = null + override fun start() {} override fun onBootCompleted() { @@ -94,6 +109,21 @@ constructor( controlsListingController.forceReload() selectDefaultPanelIfNecessary() bindToPanel() + monitorPackageUninstall() + } + + private fun monitorPackageUninstall() { + packageJob?.cancel() + packageJob = packageChangeRepository.packageChanged(userTracker.userHandle) + .filter { + val selectedPackage = + selectedComponentRepository.getSelectedComponent()?.componentName?.packageName + // Selected package was uninstalled + (it is PackageChangeModel.Uninstalled) && (it.packageName == selectedPackage) + } + .onEach { selectedComponentRepository.removeSelectedComponent() } + .flowOn(bgDispatcher) + .launchIn(scope) } private fun selectDefaultPanelIfNecessary() { diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt index 2ad6014fd7cd..e42a4a6af0de 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt @@ -25,20 +25,21 @@ import com.android.systemui.statusbar.phone.SystemUIDialog import java.util.function.Consumer import javax.inject.Inject -class ControlsDialogsFactory(private val internalDialogFactory: (Context) -> SystemUIDialog) { +class ControlsDialogsFactory @Inject constructor( + private val dialogFactory: SystemUIDialog.Factory +) { - @Inject constructor() : this({ SystemUIDialog(it) }) fun createRemoveAppDialog( - context: Context, - appName: CharSequence, - response: Consumer<Boolean> + context: Context, + appName: CharSequence, + response: Consumer<Boolean> ): Dialog { val listener = DialogInterface.OnClickListener { _, which -> response.accept(which == DialogInterface.BUTTON_POSITIVE) } - return internalDialogFactory(context).apply { + return dialogFactory.create(context).apply { setTitle(context.getString(R.string.controls_panel_remove_app_authorization, appName)) setCanceledOnTouchOutside(true) setOnCancelListener { response.accept(false) } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt index e71007ba55dd..8831dc61e452 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -484,7 +484,9 @@ class ControlsUiControllerImpl @Inject constructor ( } }, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, - null, + ActivityOptions.makeBasic() + .setPendingIntentCreatorBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED).toBundle(), userTracker.userHandle ) diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index ac71664e5590..87a736d926b5 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -54,6 +54,7 @@ import com.android.systemui.statusbar.phone.StatusBarHeadsUpChangeListener import com.android.systemui.stylus.StylusUsiPowerStartable import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import com.android.systemui.theme.ThemeOverlayController +import com.android.systemui.unfold.DisplaySwitchLatencyTracker import com.android.systemui.usb.StorageNotification import com.android.systemui.util.NotificationChannels import com.android.systemui.util.StartBinderLoggerModule @@ -141,6 +142,12 @@ abstract class SystemUICoreStartableModule { @ClassKey(LatencyTester::class) abstract fun bindLatencyTester(sysui: LatencyTester): CoreStartable + /** Inject into DisplaySwitchLatencyTracker. */ + @Binds + @IntoMap + @ClassKey(DisplaySwitchLatencyTracker::class) + abstract fun bindDisplaySwitchLatencyTracker(sysui: DisplaySwitchLatencyTracker): CoreStartable + /** Inject into NotificationChannels. */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index a25c78871115..92300efdc930 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -112,6 +112,7 @@ import com.android.systemui.statusbar.notification.people.PeopleHubModule; import com.android.systemui.statusbar.notification.row.dagger.ExpandableNotificationRowComponent; import com.android.systemui.statusbar.notification.row.dagger.NotificationRowComponent; import com.android.systemui.statusbar.phone.CentralSurfaces; +import com.android.systemui.statusbar.phone.ConfigurationControllerModule; import com.android.systemui.statusbar.phone.LetterboxModule; import com.android.systemui.statusbar.phone.NotificationIconAreaControllerModule; import com.android.systemui.statusbar.pipeline.dagger.StatusBarPipelineModule; @@ -178,6 +179,7 @@ import javax.inject.Named; ClockRegistryModule.class, CommunalModule.class, CommonDataLayerModule.class, + ConfigurationControllerModule.class, ConnectivityModule.class, ControlsModule.class, CoroutinesModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt index b91541813d62..71b5ab2d61ed 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt @@ -1,6 +1,7 @@ package com.android.systemui.deviceentry import com.android.systemui.deviceentry.data.repository.DeviceEntryRepositoryModule +import com.android.systemui.deviceentry.data.repository.FaceWakeUpTriggersConfigModule import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition import dagger.Module import dagger.multibindings.Multibinds @@ -9,6 +10,7 @@ import dagger.multibindings.Multibinds includes = [ DeviceEntryRepositoryModule::class, + FaceWakeUpTriggersConfigModule::class, ], ) abstract class DeviceEntryModule { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt index 17d78365417b..7a70c4ac9fab 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.keyguard.data.repository +package com.android.systemui.deviceentry.data.repository import android.app.StatusBarManager import android.content.Context @@ -22,7 +22,6 @@ import android.hardware.face.FaceManager import android.os.CancellationSignal import com.android.internal.logging.InstanceId import com.android.internal.logging.UiEventLogger -import com.android.keyguard.FaceAuthUiEvent import com.android.systemui.Dumpable import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor @@ -32,19 +31,27 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.deviceentry.shared.FaceAuthUiEvent +import com.android.systemui.deviceentry.shared.model.AcquiredFaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.FaceDetectionStatus +import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository +import com.android.systemui.keyguard.data.repository.BiometricType +import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.data.repository.FaceAuthTableLog +import com.android.systemui.keyguard.data.repository.FaceDetectTableLog +import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.keyguard.data.repository.TrustRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor -import com.android.systemui.keyguard.shared.model.AcquiredFaceAuthenticationStatus -import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus -import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus -import com.android.systemui.keyguard.shared.model.FaceDetectionStatus -import com.android.systemui.keyguard.shared.model.FailedFaceAuthenticationStatus -import com.android.systemui.keyguard.shared.model.HelpFaceAuthenticationStatus import com.android.systemui.keyguard.shared.model.KeyguardState -import com.android.systemui.keyguard.shared.model.SuccessFaceAuthenticationStatus import com.android.systemui.keyguard.shared.model.SysUiFaceAuthenticateOptions import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.log.FaceAuthenticationLogger diff --git a/packages/SystemUI/src/com/android/keyguard/FaceWakeUpTriggersConfig.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/FaceWakeUpTriggersConfig.kt index 84a6b098f239..9d6718be96f6 100644 --- a/packages/SystemUI/src/com/android/keyguard/FaceWakeUpTriggersConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/FaceWakeUpTriggersConfig.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,28 +14,35 @@ * limitations under the License. */ -package com.android.keyguard +package com.android.systemui.deviceentry.data.repository import android.content.res.Resources import android.os.Build import android.os.PowerManager import com.android.systemui.Dumpable -import com.android.systemui.res.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.res.R import com.android.systemui.util.settings.GlobalSettings +import dagger.Binds +import dagger.Module import java.io.PrintWriter import java.util.stream.Collectors import javax.inject.Inject /** Determines which device wake-ups should trigger passive authentication. */ +interface FaceWakeUpTriggersConfig { + fun shouldTriggerFaceAuthOnWakeUpFrom(@PowerManager.WakeReason pmWakeReason: Int): Boolean + fun shouldTriggerFaceAuthOnWakeUpFrom(wakeReason: WakeSleepReason): Boolean +} + @SysUISingleton -class FaceWakeUpTriggersConfig +class FaceWakeUpTriggersConfigImpl @Inject constructor(@Main resources: Resources, globalSettings: GlobalSettings, dumpManager: DumpManager) : - Dumpable { + Dumpable, FaceWakeUpTriggersConfig { private val defaultTriggerFaceAuthOnWakeUpFrom: Set<Int> = resources.getIntArray(R.array.config_face_auth_wake_up_triggers).toSet() private val triggerFaceAuthOnWakeUpFrom: Set<Int> @@ -65,11 +72,13 @@ constructor(@Main resources: Resources, globalSettings: GlobalSettings, dumpMana dumpManager.registerDumpable(this) } - fun shouldTriggerFaceAuthOnWakeUpFrom(@PowerManager.WakeReason pmWakeReason: Int): Boolean { + override fun shouldTriggerFaceAuthOnWakeUpFrom( + @PowerManager.WakeReason pmWakeReason: Int + ): Boolean { return triggerFaceAuthOnWakeUpFrom.contains(pmWakeReason) } - fun shouldTriggerFaceAuthOnWakeUpFrom(wakeReason: WakeSleepReason): Boolean = + override fun shouldTriggerFaceAuthOnWakeUpFrom(wakeReason: WakeSleepReason): Boolean = wakeSleepReasonsToTriggerFaceAuth.contains(wakeReason) override fun dump(pw: PrintWriter, args: Array<out String>) { @@ -87,3 +96,8 @@ constructor(@Main resources: Resources, globalSettings: GlobalSettings, dumpMana ?: default } } + +@Module +interface FaceWakeUpTriggersConfigModule { + @Binds fun repository(impl: FaceWakeUpTriggersConfigImpl): FaceWakeUpTriggersConfig +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/NoopDeviceEntryFaceAuthRepository.kt index f4a74f03696c..6695b182d085 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/NoopDeviceEntryFaceAuthRepository.kt @@ -1,25 +1,25 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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 + * 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. + * 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.keyguard.data.repository +package com.android.systemui.deviceentry.data.repository -import com.android.keyguard.FaceAuthUiEvent import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus -import com.android.systemui.keyguard.shared.model.FaceDetectionStatus +import com.android.systemui.deviceentry.shared.FaceAuthUiEvent +import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.FaceDetectionStatus import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricAuthInteractor.kt index 1a6bd0427db5..569917630e50 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricAuthInteractor.kt @@ -18,13 +18,14 @@ package com.android.systemui.deviceentry.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.shared.DeviceEntryBiometricMode +import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository -import com.android.systemui.keyguard.shared.model.FailedFaceAuthenticationStatus import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map @@ -62,7 +63,9 @@ constructor( val faceOnlyFaceFailure: Flow<FailedFaceAuthenticationStatus> = faceOnly.flatMapLatest { faceOnly -> if (faceOnly) { - deviceEntryFaceAuthInteractor.faceFailure + deviceEntryFaceAuthInteractor.authenticationStatus.filterIsInstance< + FailedFaceAuthenticationStatus + >() } else { emptyFlow() } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt index 70716c6c91fe..99bd25bf0e52 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,19 +16,80 @@ package com.android.systemui.deviceentry.domain.interactor -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository -import com.android.systemui.keyguard.shared.model.FailedFaceAuthenticationStatus -import javax.inject.Inject +import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.FaceDetectionStatus import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filterIsInstance - -@SysUISingleton -class DeviceEntryFaceAuthInteractor -@Inject -constructor( - repository: DeviceEntryFaceAuthRepository, -) { - val faceFailure: Flow<FailedFaceAuthenticationStatus> = - repository.authenticationStatus.filterIsInstance<FailedFaceAuthenticationStatus>() + +/** + * Interactor that exposes API to get the face authentication status and handle any events that can + * cause face authentication to run for device entry. + */ +interface DeviceEntryFaceAuthInteractor { + + /** Current authentication status */ + val authenticationStatus: Flow<FaceAuthenticationStatus> + + /** Current detection status */ + val detectionStatus: Flow<FaceDetectionStatus> + + /** Can face auth be run right now */ + fun canFaceAuthRun(): Boolean + + /** Whether face auth is currently running or not. */ + fun isRunning(): Boolean + + /** Whether face auth is in lock out state. */ + fun isLockedOut(): Boolean + + /** Whether face auth is enrolled and enabled for the current user */ + fun isFaceAuthEnabledAndEnrolled(): Boolean + + /** Whether the current user is authenticated successfully with face auth */ + fun isAuthenticated(): Boolean + /** + * Register listener for use from code that cannot use [authenticationStatus] or + * [detectionStatus] + */ + fun registerListener(listener: FaceAuthenticationListener) + + /** Unregister previously registered listener */ + fun unregisterListener(listener: FaceAuthenticationListener) + + fun onUdfpsSensorTouched() + fun onAssistantTriggeredOnLockScreen() + fun onDeviceLifted() + fun onQsExpansionStared() + fun onNotificationPanelClicked() + fun onSwipeUpOnBouncer() + fun onPrimaryBouncerUserInput() + fun onAccessibilityAction() + fun onWalletLaunched() + + /** Whether face auth is considered class 3 */ + fun isFaceAuthStrong(): Boolean +} + +/** + * Listener that can be registered with the [DeviceEntryFaceAuthInteractor] to receive updates about + * face authentication & detection updates. + * + * This is present to make it easier for use the new face auth API for code that cannot use + * [DeviceEntryFaceAuthInteractor.authenticationStatus] or + * [DeviceEntryFaceAuthInteractor.detectionStatus] flows. + */ +interface FaceAuthenticationListener { + /** Receive face isAuthenticated updates */ + fun onAuthenticatedChanged(isAuthenticated: Boolean) + + /** Receive face authentication status updates */ + fun onAuthenticationStatusChanged(status: FaceAuthenticationStatus) + + /** Receive status updates whenever face detection runs */ + fun onDetectionStatusChanged(status: FaceDetectionStatus) + + fun onLockoutStateChanged(isLockedOut: Boolean) + + fun onRunningStateChanged(isRunning: Boolean) + + fun onAuthEnrollmentStateChanged(enrolled: Boolean) } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt index efa1c0a07490..684627ba27bf 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt @@ -19,6 +19,7 @@ package com.android.systemui.deviceentry.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterIsInstance @@ -31,4 +32,11 @@ constructor( ) { val fingerprintFailure: Flow<FailFingerprintAuthenticationStatus> = repository.authenticationStatus.filterIsInstance<FailFingerprintAuthenticationStatus>() + + /** Whether fingerprint authentication is currently running or not */ + val isRunning: Flow<Boolean> = repository.isRunning + + /** Provide the current status of fingerprint authentication. */ + val authenticationStatus: Flow<FingerprintAuthenticationStatus> = + repository.authenticationStatus } 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 f6a9570fc94c..2680328d5d7e 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 @@ -20,8 +20,8 @@ import com.android.systemui.authentication.domain.interactor.AuthenticationInter 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.DeviceEntryFaceAuthRepository import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository -import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository import com.android.systemui.keyguard.data.repository.TrustRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import com.android.systemui.scene.domain.interactor.SceneInteractor diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt index cd6ab31f3908..3b9416690a85 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt @@ -1,24 +1,24 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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 + * 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. + * 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.keyguard.domain.interactor +package com.android.systemui.deviceentry.domain.interactor import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus -import com.android.systemui.keyguard.shared.model.FaceDetectionStatus +import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.FaceDetectionStatus import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow @@ -27,10 +27,10 @@ import kotlinx.coroutines.flow.emptyFlow * Implementation of the interactor that noops all face auth operations. * * This is required for SystemUI variants that do not support face authentication but still inject - * other SysUI components that depend on [KeyguardFaceAuthInteractor] + * other SysUI components that depend on [DeviceEntryFaceAuthInteractor] */ @SysUISingleton -class NoopKeyguardFaceAuthInteractor @Inject constructor() : KeyguardFaceAuthInteractor { +class NoopDeviceEntryFaceAuthInteractor @Inject constructor() : DeviceEntryFaceAuthInteractor { override val authenticationStatus: Flow<FaceAuthenticationStatus> get() = emptyFlow() override val detectionStatus: Flow<FaceDetectionStatus> diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt index e3f47397eca3..98130eb10f3d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt @@ -14,14 +14,12 @@ * limitations under the License. */ -package com.android.systemui.keyguard.domain.interactor +package com.android.systemui.deviceentry.domain.interactor import android.app.trust.TrustManager import android.content.Context import android.hardware.biometrics.BiometricFaceConstants import android.hardware.biometrics.BiometricSourceType -import com.android.keyguard.FaceAuthUiEvent -import com.android.keyguard.FaceWakeUpTriggersConfig import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.CoreStartable import com.android.systemui.biometrics.data.repository.FacePropertyRepository @@ -32,11 +30,14 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository +import com.android.systemui.deviceentry.data.repository.FaceWakeUpTriggersConfig +import com.android.systemui.deviceentry.shared.FaceAuthUiEvent +import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository -import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository -import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus -import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.log.FaceAuthenticationLogger import com.android.systemui.power.domain.interactor.PowerInteractor @@ -65,7 +66,7 @@ import kotlinx.coroutines.yield * SystemUI Keyguard. */ @SysUISingleton -class SystemUIKeyguardFaceAuthInteractor +class SystemUIDeviceEntryFaceAuthInteractor @Inject constructor( private val context: Context, @@ -84,7 +85,7 @@ constructor( private val powerInteractor: PowerInteractor, private val biometricSettingsRepository: BiometricSettingsRepository, private val trustManager: TrustManager, -) : CoreStartable, KeyguardFaceAuthInteractor { +) : CoreStartable, DeviceEntryFaceAuthInteractor { private val listeners: MutableList<FaceAuthenticationListener> = mutableListOf() @@ -310,7 +311,7 @@ constructor( } companion object { - const val TAG = "KeyguardFaceAuthInteractor" + const val TAG = "DeviceEntryFaceAuthInteractor" } } diff --git a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/shared/FaceAuthReason.kt index 2abb7a41ef08..ee220d5fb713 100644 --- a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/shared/FaceAuthReason.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,58 +14,55 @@ * limitations under the License. */ -package com.android.keyguard +package com.android.systemui.deviceentry.shared import android.annotation.StringDef import android.os.PowerManager import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger -import com.android.keyguard.FaceAuthApiRequestReason.Companion.ACCESSIBILITY_ACTION -import com.android.keyguard.FaceAuthApiRequestReason.Companion.NOTIFICATION_PANEL_CLICKED -import com.android.keyguard.FaceAuthApiRequestReason.Companion.PICK_UP_GESTURE_TRIGGERED -import com.android.keyguard.FaceAuthApiRequestReason.Companion.QS_EXPANDED -import com.android.keyguard.FaceAuthApiRequestReason.Companion.SWIPE_UP_ON_BOUNCER -import com.android.keyguard.FaceAuthApiRequestReason.Companion.UDFPS_POINTER_DOWN -import com.android.keyguard.InternalFaceAuthReasons.ALL_AUTHENTICATORS_REGISTERED -import com.android.keyguard.InternalFaceAuthReasons.ALTERNATE_BIOMETRIC_BOUNCER_SHOWN -import com.android.keyguard.InternalFaceAuthReasons.ASSISTANT_VISIBILITY_CHANGED -import com.android.keyguard.InternalFaceAuthReasons.AUTH_REQUEST_DURING_CANCELLATION -import com.android.keyguard.InternalFaceAuthReasons.BIOMETRIC_ENABLED -import com.android.keyguard.InternalFaceAuthReasons.CAMERA_LAUNCHED -import com.android.keyguard.InternalFaceAuthReasons.DEVICE_WOKEN_UP_ON_REACH_GESTURE -import com.android.keyguard.InternalFaceAuthReasons.DISPLAY_OFF -import com.android.keyguard.InternalFaceAuthReasons.DREAM_STARTED -import com.android.keyguard.InternalFaceAuthReasons.DREAM_STOPPED -import com.android.keyguard.InternalFaceAuthReasons.ENROLLMENTS_CHANGED -import com.android.keyguard.InternalFaceAuthReasons.FACE_AUTHENTICATED -import com.android.keyguard.InternalFaceAuthReasons.FACE_AUTH_STOPPED_ON_USER_INPUT -import com.android.keyguard.InternalFaceAuthReasons.FACE_CANCEL_NOT_RECEIVED -import com.android.keyguard.InternalFaceAuthReasons.FACE_LOCKOUT_RESET -import com.android.keyguard.InternalFaceAuthReasons.FINISHED_GOING_TO_SLEEP -import com.android.keyguard.InternalFaceAuthReasons.FP_AUTHENTICATED -import com.android.keyguard.InternalFaceAuthReasons.FP_LOCKED_OUT -import com.android.keyguard.InternalFaceAuthReasons.GOING_TO_SLEEP -import com.android.keyguard.InternalFaceAuthReasons.KEYGUARD_GOING_AWAY -import com.android.keyguard.InternalFaceAuthReasons.KEYGUARD_INIT -import com.android.keyguard.InternalFaceAuthReasons.KEYGUARD_OCCLUSION_CHANGED -import com.android.keyguard.InternalFaceAuthReasons.KEYGUARD_RESET -import com.android.keyguard.InternalFaceAuthReasons.KEYGUARD_VISIBILITY_CHANGED -import com.android.keyguard.InternalFaceAuthReasons.NON_STRONG_BIOMETRIC_ALLOWED_CHANGED -import com.android.keyguard.InternalFaceAuthReasons.OCCLUDING_APP_REQUESTED -import com.android.keyguard.InternalFaceAuthReasons.POSTURE_CHANGED -import com.android.keyguard.InternalFaceAuthReasons.PRIMARY_BOUNCER_SHOWN -import com.android.keyguard.InternalFaceAuthReasons.PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN -import com.android.keyguard.InternalFaceAuthReasons.RETRY_AFTER_HW_UNAVAILABLE -import com.android.keyguard.InternalFaceAuthReasons.STARTED_WAKING_UP -import com.android.keyguard.InternalFaceAuthReasons.STRONG_AUTH_ALLOWED_CHANGED -import com.android.keyguard.InternalFaceAuthReasons.TRUST_DISABLED -import com.android.keyguard.InternalFaceAuthReasons.TRUST_ENABLED -import com.android.keyguard.InternalFaceAuthReasons.USER_SWITCHING +import com.android.systemui.deviceentry.shared.FaceAuthApiRequestReason.Companion.ACCESSIBILITY_ACTION +import com.android.systemui.deviceentry.shared.FaceAuthApiRequestReason.Companion.NOTIFICATION_PANEL_CLICKED +import com.android.systemui.deviceentry.shared.FaceAuthApiRequestReason.Companion.PICK_UP_GESTURE_TRIGGERED +import com.android.systemui.deviceentry.shared.FaceAuthApiRequestReason.Companion.QS_EXPANDED +import com.android.systemui.deviceentry.shared.FaceAuthApiRequestReason.Companion.SWIPE_UP_ON_BOUNCER +import com.android.systemui.deviceentry.shared.FaceAuthApiRequestReason.Companion.UDFPS_POINTER_DOWN +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.ALL_AUTHENTICATORS_REGISTERED +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.ALTERNATE_BIOMETRIC_BOUNCER_SHOWN +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.ASSISTANT_VISIBILITY_CHANGED +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.AUTH_REQUEST_DURING_CANCELLATION +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.BIOMETRIC_ENABLED +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.CAMERA_LAUNCHED +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.DEVICE_WOKEN_UP_ON_REACH_GESTURE +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.DISPLAY_OFF +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.DREAM_STARTED +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.DREAM_STOPPED +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.ENROLLMENTS_CHANGED +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.FACE_AUTHENTICATED +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.FACE_AUTH_STOPPED_ON_USER_INPUT +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.FACE_CANCEL_NOT_RECEIVED +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.FACE_LOCKOUT_RESET +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.FINISHED_GOING_TO_SLEEP +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.FP_AUTHENTICATED +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.FP_LOCKED_OUT +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.GOING_TO_SLEEP +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.KEYGUARD_GOING_AWAY +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.KEYGUARD_INIT +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.KEYGUARD_OCCLUSION_CHANGED +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.KEYGUARD_RESET +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.KEYGUARD_VISIBILITY_CHANGED +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.NON_STRONG_BIOMETRIC_ALLOWED_CHANGED +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.OCCLUDING_APP_REQUESTED +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.POSTURE_CHANGED +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.PRIMARY_BOUNCER_SHOWN +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.RETRY_AFTER_HW_UNAVAILABLE +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.STARTED_WAKING_UP +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.STRONG_AUTH_ALLOWED_CHANGED +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.TRUST_DISABLED +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.TRUST_ENABLED +import com.android.systemui.deviceentry.shared.InternalFaceAuthReasons.USER_SWITCHING -/** - * List of reasons why face auth is requested by clients through - * [KeyguardUpdateMonitor.requestFaceAuth]. - */ +/** List of reasons why face auth is requested by clients. */ @Retention(AnnotationRetention.SOURCE) @StringDef( SWIPE_UP_ON_BOUNCER, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/shared/model/FaceAuthenticationModels.kt index 3de3666fdc3c..f006b3484033 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/shared/model/FaceAuthenticationModels.kt @@ -14,14 +14,14 @@ * limitations under the License. */ -package com.android.systemui.keyguard.shared.model +package com.android.systemui.deviceentry.shared.model import android.hardware.face.FaceManager import android.os.SystemClock.elapsedRealtime /** * Authentication status provided by - * [com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository] + * [com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository] */ sealed class FaceAuthenticationStatus diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java index 4c4aa5ce1911..8776ec5496c8 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java @@ -118,6 +118,11 @@ public interface DozeHost { * Called when the dozing state may have been updated. */ default void onDozingChanged(boolean isDozing) {} + + /** + * Called when fingerprint acquisition has started and screen state might need updating. + */ + default void onSideFingerprintAcquisitionStarted() {} } interface PulseCallback { diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java index 5b90ef2bb806..424bd0a3e23b 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java @@ -514,6 +514,7 @@ public class DozeLog implements Dumpable { case REASON_SENSOR_TAP: return "tap"; case REASON_SENSOR_UDFPS_LONG_PRESS: return "udfps"; case REASON_SENSOR_QUICK_PICKUP: return "quickPickup"; + case PULSE_REASON_FINGERPRINT_ACTIVATED: return "fingerprint-triggered"; default: throw new IllegalArgumentException("invalid reason: " + pulseReason); } } @@ -542,7 +543,9 @@ public class DozeLog implements Dumpable { PULSE_REASON_SENSOR_SIGMOTION, REASON_SENSOR_PICKUP, REASON_SENSOR_DOUBLE_TAP, PULSE_REASON_SENSOR_LONG_PRESS, PULSE_REASON_DOCKING, REASON_SENSOR_WAKE_UP_PRESENCE, PULSE_REASON_SENSOR_WAKE_REACH, REASON_SENSOR_TAP, - REASON_SENSOR_UDFPS_LONG_PRESS, REASON_SENSOR_QUICK_PICKUP}) + REASON_SENSOR_UDFPS_LONG_PRESS, REASON_SENSOR_QUICK_PICKUP, + PULSE_REASON_FINGERPRINT_ACTIVATED + }) public @interface Reason {} public static final int PULSE_REASON_NONE = -1; public static final int PULSE_REASON_INTENT = 0; @@ -557,6 +560,7 @@ public class DozeLog implements Dumpable { public static final int REASON_SENSOR_TAP = 9; public static final int REASON_SENSOR_UDFPS_LONG_PRESS = 10; public static final int REASON_SENSOR_QUICK_PICKUP = 11; + public static final int PULSE_REASON_FINGERPRINT_ACTIVATED = 12; - public static final int TOTAL_REASONS = 12; + public static final int TOTAL_REASONS = 13; } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java index 795c3d4528c5..93111874c69b 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java @@ -265,6 +265,10 @@ public class DozeTriggers implements DozeMachine.Part { mDozeLog.traceNotificationPulse(); } + private void onSideFingerprintAcquisitionStarted() { + requestPulse(DozeLog.PULSE_REASON_FINGERPRINT_ACTIVATED, false, null); + } + private static void runIfNotNull(Runnable runnable) { if (runnable != null) { runnable.run(); @@ -690,5 +694,10 @@ public class DozeTriggers implements DozeMachine.Part { public void onNotificationAlerted(Runnable onPulseSuppressedListener) { onNotification(onPulseSuppressedListener); } + + @Override + public void onSideFingerprintAcquisitionStarted() { + DozeTriggers.this.onSideFingerprintAcquisitionStarted(); + } }; } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index bb0c2733e511..38c7c6ac67cb 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -444,29 +444,10 @@ object Flags { // TODO(b/254512728): Tracking Bug @JvmField val NEW_BACK_AFFORDANCE = releasedFlag("new_back_affordance") - // TODO(b/255854141): Tracking Bug - @JvmField - val WM_ENABLE_PREDICTIVE_BACK_SYSUI = - unreleasedFlag("persist.wm.debug.predictive_back_sysui_enable", teamfood = true) // TODO(b/270987164): Tracking Bug @JvmField val TRACKPAD_GESTURE_FEATURES = releasedFlag("trackpad_gesture_features") - // TODO(b/263826204): Tracking Bug - @JvmField - val WM_ENABLE_PREDICTIVE_BACK_BOUNCER_ANIM = - unreleasedFlag("persist.wm.debug.predictive_back_bouncer_anim", teamfood = true) - - // TODO(b/238475428): Tracking Bug - @JvmField - val WM_SHADE_ALLOW_BACK_GESTURE = - sysPropBooleanFlag("persist.wm.debug.shade_allow_back_gesture", default = false) - - // TODO(b/238475428): Tracking Bug - @JvmField - val WM_SHADE_ANIMATE_BACK_GESTURE = - unreleasedFlag("persist.wm.debug.shade_animate_back_gesture", teamfood = false) - // TODO(b/265639042): Tracking Bug @JvmField val WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM = diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt index 6cb68bade9a9..89bfd96d2408 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt @@ -16,6 +16,7 @@ package com.android.systemui.haptics.slider +import android.view.MotionEvent import androidx.annotation.FloatRange /** Configuration parameters of a [SliderHapticFeedbackProvider] */ @@ -38,6 +39,8 @@ data class SliderHapticFeedbackConfig( val numberOfLowTicks: Int = 5, /** Maximum velocity allowed for vibration scaling. This is not expected to change. */ val maxVelocityToScale: Float = 2000f, /* In pixels/sec */ + /** Axis to use when computing velocity. Must be the same as the slider's axis of movement */ + val velocityAxis: Int = MotionEvent.AXIS_X, /** Vibration scale at the upper bookend of the slider */ @FloatRange(from = 0.0, to = 1.0) val upperBookendScale: Float = 1f, /** Vibration scale at the lower bookend of the slider */ diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt index 9e6245ae7f21..6f28ab7f414c 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt @@ -162,27 +162,33 @@ class SliderHapticFeedbackProvider( override fun onLowerBookend() { if (!hasVibratedAtLowerBookend) { - velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale) - vibrateOnEdgeCollision(abs(velocityTracker.xVelocity)) + vibrateOnEdgeCollision(abs(getTrackedVelocity())) hasVibratedAtLowerBookend = true } } override fun onUpperBookend() { if (!hasVibratedAtUpperBookend) { - velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale) - vibrateOnEdgeCollision(abs(velocityTracker.xVelocity)) + vibrateOnEdgeCollision(abs(getTrackedVelocity())) hasVibratedAtUpperBookend = true } } override fun onProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) { - velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale) - vibrateDragTexture(abs(velocityTracker.xVelocity), progress) + vibrateDragTexture(abs(getTrackedVelocity()), progress) hasVibratedAtUpperBookend = false hasVibratedAtLowerBookend = false } + private fun getTrackedVelocity(): Float { + velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale) + return if (velocityTracker.isAxisSupported(config.velocityAxis)) { + velocityTracker.getAxisVelocity(config.velocityAxis) + } else { + 0f + } + } + override fun onProgressJump(@FloatRange(from = 0.0, to = 1.0) progress: Float) {} override fun onSelectAndArrow(@FloatRange(from = 0.0, to = 1.0) progress: Float) {} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java index aa4c88af4690..e23ec894f8f3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java @@ -403,6 +403,7 @@ public class KeyguardIndicationRotateTextViewController extends public static final int INDICATION_TYPE_REVERSE_CHARGING = 10; public static final int INDICATION_TYPE_BIOMETRIC_MESSAGE = 11; public static final int INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP = 12; + public static final int INDICATION_IS_DISMISSIBLE = 13; @IntDef({ INDICATION_TYPE_NONE, @@ -417,7 +418,8 @@ public class KeyguardIndicationRotateTextViewController extends INDICATION_TYPE_USER_LOCKED, INDICATION_TYPE_REVERSE_CHARGING, INDICATION_TYPE_BIOMETRIC_MESSAGE, - INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP + INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP, + INDICATION_IS_DISMISSIBLE }) @Retention(RetentionPolicy.SOURCE) public @interface IndicationType{} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index edf964815ca1..e2ab20e29e2d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -593,6 +593,13 @@ public class KeyguardService extends Service { mKeyguardViewMediator.doKeyguardTimeout(options); } + // Binder interface + public void showDismissibleKeyguard() { + trace("showDismissibleKeyguard"); + checkPermission(); + mKeyguardViewMediator.showDismissibleKeyguard(); + } + @Override // Binder interface public void setSwitchingUser(boolean switching) { trace("setSwitchingUser switching=" + switching); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index 50836fe9ee51..afef8751b065 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -46,6 +46,7 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel import com.android.systemui.plugins.FalsingManager import com.android.systemui.res.R +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.NotificationShadeWindowView import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.KeyguardIndicationController @@ -111,7 +112,9 @@ constructor( bindKeyguardRootView() initializeViews() - KeyguardBlueprintViewBinder.bind(keyguardRootView, keyguardBlueprintViewModel) + if (!SceneContainerFlag.isEnabled) { + KeyguardBlueprintViewBinder.bind(keyguardRootView, keyguardBlueprintViewModel) + } keyguardBlueprintCommandListener.start() } @@ -144,6 +147,10 @@ constructor( } private fun bindKeyguardRootView() { + if (SceneContainerFlag.isEnabled) { + return + } + rootViewHandle?.dispose() rootViewHandle = KeyguardRootViewBinder.bind( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index d7a1906f68e4..a34730e2d71f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -107,6 +107,7 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.app.animation.Interpolators; +import com.android.internal.foldables.FoldGracePeriodProvider; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.jank.InteractionJankMonitor.Configuration; import com.android.internal.logging.UiEventLogger; @@ -311,6 +312,11 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, */ public static final String OPTION_FORCE_SHOW = "force_show"; public static final String SYS_BOOT_REASON_PROP = "sys.boot.reason.last"; + /** + * Boolean option for showKeyguard, when set to true, can show the keyguard without immediately + * locking. + */ + public static final String OPTION_SHOW_DISMISSIBLE = "show_dismissible"; public static final String REBOOT_MAINLINE_UPDATE = "reboot,mainline_update"; private final DreamOverlayStateController mDreamOverlayStateController; private final JavaAdapter mJavaAdapter; @@ -1321,7 +1327,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private DozeParameters mDozeParameters; private SelectedUserInteractor mSelectedUserInteractor; private KeyguardInteractor mKeyguardInteractor; - + @VisibleForTesting + protected FoldGracePeriodProvider mFoldGracePeriodProvider = + new FoldGracePeriodProvider(); private final KeyguardStateController mKeyguardStateController; private final KeyguardStateController.Callback mKeyguardStateControllerCallback = new KeyguardStateController.Callback() { @@ -1995,7 +2003,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mNeedToReshowWhenReenabled = false; updateInputRestrictedLocked(); - showLocked(null); + showKeyguard(null); // block until we know the keyguard is done drawing (and post a message // to unblock us after a timeout, so we don't risk blocking too long @@ -2170,6 +2178,24 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } /** + * Only available if the fold grace period feature is enabled. + * Used by PhoneWindowManager to show the keyguard immediately without locking the device. + * This method shows the keyguard whether there's a screen lock configured or not (including + * screen lock SWIPE or NONE). + * This must be safe to call from any thread and with any window manager locks held. + */ + public void showDismissibleKeyguard() { + if (mFoldGracePeriodProvider.isEnabled()) { + Bundle showKeyguardUnlocked = new Bundle(); + showKeyguardUnlocked.putBoolean(OPTION_SHOW_DISMISSIBLE, true); + showKeyguard(showKeyguardUnlocked); + } else { + Log.e(TAG, "fold grace period feature isn't enabled, but showKeyguard() method is" + + " being called", new Throwable()); + } + } + + /** * Given the state of the keyguard, is the input restricted? * Input is restricted when the keyguard is showing, or when the keyguard * was suppressed by an app that disabled the keyguard or we haven't been provisioned yet. @@ -2273,7 +2299,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } if (DEBUG) Log.d(TAG, "doKeyguard: showing the lock screen"); - showLocked(options); + showKeyguard(options); } private void lockProfile(int userId) { @@ -2335,18 +2361,18 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } /** - * Send message to keyguard telling it to show itself + * Send message to keyguard telling it to show itself. * @see #handleShow */ - private void showLocked(Bundle options) { - Trace.beginSection("KeyguardViewMediator#showLocked acquiring mShowKeyguardWakeLock"); - if (DEBUG) Log.d(TAG, "showLocked"); + private void showKeyguard(Bundle options) { + Trace.beginSection("KeyguardViewMediator#showKeyguard acquiring mShowKeyguardWakeLock"); + if (DEBUG) Log.d(TAG, "showKeyguard"); // ensure we stay awake until we are finished displaying the keyguard mShowKeyguardWakeLock.acquire(); Message msg = mHandler.obtainMessage(SHOW, options); // Treat these messages with priority - This call can originate from #doKeyguardTimeout, - // meaning the device should lock as soon as possible and not wait for other messages on - // the thread to process first. + // meaning the device may lock, so it shouldn't wait for other messages on the thread to + // process first. mHandler.sendMessageAtFrontOfQueue(msg); Trace.endSection(); } @@ -2724,13 +2750,18 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } /** - * Handle message sent by {@link #showLocked}. + * Handle message sent by {@link #showKeyguard}. * @see #SHOW */ private void handleShow(Bundle options) { Trace.beginSection("KeyguardViewMediator#handleShow"); + final boolean showUnlocked = options != null + && options.getBoolean(OPTION_SHOW_DISMISSIBLE, false); final int currentUser = mSelectedUserInteractor.getSelectedUserId(); - if (mLockPatternUtils.isSecure(currentUser)) { + if (showUnlocked) { + // tell KeyguardUpdateMonitor to keep the device unlocked until the next lock signal + mUpdateMonitor.tryForceIsDismissibleKeyguard(); + } else if (mLockPatternUtils.isSecure(currentUser)) { mLockPatternUtils.getDevicePolicyManager().reportKeyguardSecured(currentUser); } synchronized (KeyguardViewMediator.this) { @@ -2755,7 +2786,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, + ", which means we're showing in the middle of hiding."); } - // Force if we we're showing in the middle of unlocking, to ensure we end up in the + // Force if we're showing in the middle of unlocking, to ensure we end up in the // correct state. setShowingLocked(true, hidingOrGoingAway /* force */); mHiding = false; diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardFaceAuthNotSupportedModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardFaceAuthNotSupportedModule.kt index d8eb81caa76c..f29b6576217c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardFaceAuthNotSupportedModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardFaceAuthNotSupportedModule.kt @@ -16,10 +16,10 @@ package com.android.systemui.keyguard.dagger -import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository -import com.android.systemui.keyguard.data.repository.NoopDeviceEntryFaceAuthRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor -import com.android.systemui.keyguard.domain.interactor.NoopKeyguardFaceAuthInteractor +import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository +import com.android.systemui.deviceentry.data.repository.NoopDeviceEntryFaceAuthRepository +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.NoopDeviceEntryFaceAuthInteractor import dagger.Binds import dagger.Module @@ -33,7 +33,9 @@ import dagger.Module @Module interface KeyguardFaceAuthNotSupportedModule { @Binds - fun keyguardFaceAuthInteractor(impl: NoopKeyguardFaceAuthInteractor): KeyguardFaceAuthInteractor + fun keyguardFaceAuthInteractor( + impl: NoopDeviceEntryFaceAuthInteractor + ): DeviceEntryFaceAuthInteractor @Binds fun deviceEntryFaceAuthRepository( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt index ee0eb2d90aa7..b373f8520254 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt @@ -19,8 +19,10 @@ package com.android.systemui.keyguard.data.repository import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor -import com.android.systemui.keyguard.domain.interactor.SystemUIKeyguardFaceAuthInteractor +import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository +import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepositoryImpl +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.SystemUIDeviceEntryFaceAuthInteractor import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.TableLogBufferFactory import dagger.Binds @@ -38,13 +40,13 @@ interface KeyguardFaceAuthModule { @Binds @IntoMap - @ClassKey(SystemUIKeyguardFaceAuthInteractor::class) - fun bind(impl: SystemUIKeyguardFaceAuthInteractor): CoreStartable + @ClassKey(SystemUIDeviceEntryFaceAuthInteractor::class) + fun bind(impl: SystemUIDeviceEntryFaceAuthInteractor): CoreStartable @Binds fun keyguardFaceAuthInteractor( - impl: SystemUIKeyguardFaceAuthInteractor - ): KeyguardFaceAuthInteractor + impl: SystemUIDeviceEntryFaceAuthInteractor + ): DeviceEntryFaceAuthInteractor companion object { @Provides diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 31ef100abbcb..2f937bcd3414 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -110,7 +110,7 @@ interface KeyguardRepository { val isKeyguardGoingAway: Flow<Boolean> /** Is the always-on display available to be used? */ - val isAodAvailable: Flow<Boolean> + val isAodAvailable: StateFlow<Boolean> fun setAodAvailable(value: Boolean) @@ -338,7 +338,7 @@ constructor( .distinctUntilChanged() private val _isAodAvailable = MutableStateFlow(false) - override val isAodAvailable: Flow<Boolean> = _isAodAvailable.asStateFlow() + override val isAodAvailable: StateFlow<Boolean> = _isAodAvailable.asStateFlow() override fun setAodAvailable(value: Boolean) { _isAodAvailable.value = value diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt deleted file mode 100644 index 046916aeb277..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2023 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.keyguard.domain.interactor - -import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus -import com.android.systemui.keyguard.shared.model.FaceDetectionStatus -import kotlinx.coroutines.flow.Flow - -/** - * Interactor that exposes API to get the face authentication status and handle any events that can - * cause face authentication to run. - */ -interface KeyguardFaceAuthInteractor { - - /** Current authentication status */ - val authenticationStatus: Flow<FaceAuthenticationStatus> - - /** Current detection status */ - val detectionStatus: Flow<FaceDetectionStatus> - - /** Can face auth be run right now */ - fun canFaceAuthRun(): Boolean - - /** Whether face auth is currently running or not. */ - fun isRunning(): Boolean - - /** Whether face auth is in lock out state. */ - fun isLockedOut(): Boolean - - /** Whether face auth is enrolled and enabled for the current user */ - fun isFaceAuthEnabledAndEnrolled(): Boolean - - /** Whether the current user is authenticated successfully with face auth */ - fun isAuthenticated(): Boolean - /** - * Register listener for use from code that cannot use [authenticationStatus] or - * [detectionStatus] - */ - fun registerListener(listener: FaceAuthenticationListener) - - /** Unregister previously registered listener */ - fun unregisterListener(listener: FaceAuthenticationListener) - - fun onUdfpsSensorTouched() - fun onAssistantTriggeredOnLockScreen() - fun onDeviceLifted() - fun onQsExpansionStared() - fun onNotificationPanelClicked() - fun onSwipeUpOnBouncer() - fun onPrimaryBouncerUserInput() - fun onAccessibilityAction() - fun onWalletLaunched() - - /** Whether face auth is considered class 3 */ - fun isFaceAuthStrong(): Boolean -} - -/** - * Listener that can be registered with the [KeyguardFaceAuthInteractor] to receive updates about - * face authentication & detection updates. - * - * This is present to make it easier for use the new face auth API for code that cannot use - * [KeyguardFaceAuthInteractor.authenticationStatus] or [KeyguardFaceAuthInteractor.detectionStatus] - * flows. - */ -interface FaceAuthenticationListener { - /** Receive face isAuthenticated updates */ - fun onAuthenticatedChanged(isAuthenticated: Boolean) - - /** Receive face authentication status updates */ - fun onAuthenticationStatusChanged(status: FaceAuthenticationStatus) - - /** Receive status updates whenever face detection runs */ - fun onDetectionStatusChanged(status: FaceDetectionStatus) - - fun onLockoutStateChanged(isLockedOut: Boolean) - - fun onRunningStateChanged(isRunning: Boolean) - - fun onAuthEnrollmentStateChanged(enrolled: Boolean) -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 21651ba2cc2b..6eb3b64d4c09 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -104,7 +104,7 @@ constructor( val dozeTimeTick: Flow<Long> = repository.dozeTimeTick /** Whether Always-on Display mode is available. */ - val isAodAvailable: Flow<Boolean> = repository.isAodAvailable + val isAodAvailable: StateFlow<Boolean> = repository.isAodAvailable /** Doze transition information. */ val dozeTransitionModel: Flow<DozeTransitionModel> = repository.dozeTransitionModel diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/SysUiFaceAuthenticateOptions.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/SysUiFaceAuthenticateOptions.kt index 942cd60db479..b3d0f918bb42 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/SysUiFaceAuthenticateOptions.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/SysUiFaceAuthenticateOptions.kt @@ -32,7 +32,7 @@ import android.os.PowerManager import android.os.PowerManager.WAKE_REASON_UNKNOWN import android.util.Log import com.android.internal.logging.UiEventLogger -import com.android.keyguard.FaceAuthUiEvent +import com.android.systemui.deviceentry.shared.FaceAuthUiEvent /** * Wrapper for [FaceAuthenticateOptions] to convert SystemUI values to their corresponding value in diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt index 12775854c737..cf1d2477c9af 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt @@ -32,6 +32,7 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map @@ -89,7 +90,6 @@ constructor( val start = (startTime / transitionDuration).toFloat() val chunks = (transitionDuration / duration).toFloat() logger.logCreate(name, start) - var isComplete = true fun stepToValue(step: TransitionStep): Float? { val value = (step.value - start) * chunks @@ -98,17 +98,13 @@ constructor( // middle, it is possible this animation is being skipped but we need to inform // the ViewModels of the last update STARTED -> { - isComplete = false onStart?.invoke() max(0f, min(1f, value)) } // Always send a final value of 1. Because of rounding, [value] may never be // exactly 1. RUNNING -> - if (isComplete) { - null - } else if (value >= 1f) { - isComplete = true + if (value >= 1f) { 1f } else if (value >= 0f) { value @@ -132,6 +128,7 @@ constructor( value } .filterNotNull() + .distinctUntilChanged() } /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt index 5344696b92fe..24d06026dcf7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt @@ -30,11 +30,11 @@ import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSec import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusViewSection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule -import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeClockSection import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeGuidelines import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeMediaSection import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeNotificationStackScrollLayoutSection +import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeSmartspaceSection import com.android.systemui.util.kotlin.getOrNull import java.util.Optional import javax.inject.Inject @@ -62,7 +62,7 @@ constructor( aodNotificationIconsSection: AodNotificationIconsSection, aodBurnInSection: AodBurnInSection, communalTutorialIndicatorSection: CommunalTutorialIndicatorSection, - smartspaceSection: SmartspaceSection, + smartspaceSection: SplitShadeSmartspaceSection, clockSection: SplitShadeClockSection, mediaSection: SplitShadeMediaSection, ) : KeyguardBlueprint { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeSmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeSmartspaceSection.kt new file mode 100644 index 000000000000..8728adadd8c3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeSmartspaceSection.kt @@ -0,0 +1,45 @@ +/* + * 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.keyguard.ui.view.layout.sections + +import android.content.Context +import com.android.systemui.keyguard.KeyguardUnlockAnimationController +import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel +import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController +import javax.inject.Inject + +/* + * We need this class for the splitShadeBlueprint so `addViews` and `removeViews` will be called + * when switching to and from splitShade. + */ +class SplitShadeSmartspaceSection +@Inject +constructor( + keyguardClockViewModel: KeyguardClockViewModel, + keyguardSmartspaceViewModel: KeyguardSmartspaceViewModel, + context: Context, + smartspaceController: LockscreenSmartspaceController, + keyguardUnlockAnimationController: KeyguardUnlockAnimationController, +) : + SmartspaceSection( + keyguardClockViewModel, + keyguardSmartspaceViewModel, + context, + smartspaceController, + keyguardUnlockAnimationController, + ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt index 528a2eebc9cd..5bb27824753d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.content.Context import androidx.constraintlayout.helper.widget.Layer import com.android.keyguard.KeyguardClockSwitch.LARGE import com.android.keyguard.KeyguardClockSwitch.SMALL @@ -25,6 +26,9 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.shared.model.SettingsClockSize import com.android.systemui.plugins.clocks.ClockController +import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.SplitShadeStateController +import com.android.systemui.util.Utils import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted @@ -36,9 +40,10 @@ import kotlinx.coroutines.flow.stateIn class KeyguardClockViewModel @Inject constructor( - val keyguardInteractor: KeyguardInteractor, - val keyguardClockInteractor: KeyguardClockInteractor, + keyguardInteractor: KeyguardInteractor, + private val keyguardClockInteractor: KeyguardClockInteractor, @Application private val applicationScope: CoroutineScope, + private val splitShadeStateController: SplitShadeStateController, ) { var burnInLayer: Layer? = null val useLargeClock: Boolean @@ -85,4 +90,43 @@ constructor( started = SharingStarted.WhileSubscribed(), initialValue = false ) + + // Needs to use a non application context to get display cutout. + fun getSmallClockTopMargin(context: Context) = + if (splitShadeStateController.shouldUseSplitNotificationShade(context.resources)) { + context.resources.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin) + } else { + context.resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) + + Utils.getStatusBarHeaderHeightKeyguard(context) + } + + fun getLargeClockTopMargin(context: Context): Int { + var largeClockTopMargin = + context.resources.getDimensionPixelSize(R.dimen.status_bar_height) + + context.resources.getDimensionPixelSize( + com.android.systemui.customization.R.dimen.small_clock_padding_top + ) + + context.resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) + largeClockTopMargin += getDimen(context, DATE_WEATHER_VIEW_HEIGHT) + largeClockTopMargin += getDimen(context, ENHANCED_SMARTSPACE_HEIGHT) + if (!useLargeClock) { + largeClockTopMargin -= + context.resources.getDimensionPixelSize( + com.android.systemui.customization.R.dimen.small_clock_height + ) + } + + return largeClockTopMargin + } + + private fun getDimen(context: Context, name: String): Int { + val res = context.packageManager.getResourcesForApplication(context.packageName) + val id = res.getIdentifier(name, "dimen", context.packageName) + return res.getDimensionPixelSize(id) + } + + companion object { + private const val DATE_WEATHER_VIEW_HEIGHT = "date_weather_view_height" + private const val ENHANCED_SMARTSPACE_HEIGHT = "enhanced_smartspace_height" + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt index 1dbf1f14b569..693e3b7506fc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt @@ -28,13 +28,16 @@ import com.android.systemui.biometrics.shared.model.DisplayRotation import com.android.systemui.biometrics.shared.model.isDefaultOrientation import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.DozeServiceHost import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow @@ -43,6 +46,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion @@ -54,9 +58,13 @@ class SideFpsProgressBarViewModel @Inject constructor( private val context: Context, - private val fpAuthRepository: DeviceEntryFingerprintAuthRepository, + private val fpAuthRepository: DeviceEntryFingerprintAuthInteractor, private val sfpsSensorInteractor: SideFpsSensorInteractor, + // todo (b/317432075) Injecting DozeServiceHost directly instead of using it through + // DozeInteractor as DozeServiceHost already depends on DozeInteractor. + private val dozeServiceHost: DozeServiceHost, displayStateInteractor: DisplayStateInteractor, + @Main private val mainDispatcher: CoroutineDispatcher, @Application private val applicationScope: CoroutineScope, ) { private val _progress = MutableStateFlow(0.0f) @@ -168,18 +176,21 @@ constructor( return@collectLatest } animatorJob = - fpAuthRepository.authenticationStatus - .onEach { authStatus -> + combine( + sfpsSensorInteractor.authenticationDuration, + fpAuthRepository.authenticationStatus, + ::Pair + ) + .onEach { (authDuration, authStatus) -> when (authStatus) { is AcquiredFingerprintAuthenticationStatus -> { if (authStatus.fingerprintCaptureStarted) { _visible.value = true + dozeServiceHost.fireSideFpsAcquisitionStarted() _animator?.cancel() _animator = ValueAnimator.ofFloat(0.0f, 1.0f) - .setDuration( - sfpsSensorInteractor.authenticationDuration - ) + .setDuration(authDuration) .apply { addUpdateListener { _progress.value = it.animatedValue as Float @@ -209,6 +220,7 @@ constructor( else -> Unit } } + .flowOn(mainDispatcher) .onCompletion { _animator?.cancel() } .launchIn(applicationScope) } diff --git a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt index 3c2facbc967a..9e6c5520d1b7 100644 --- a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt @@ -2,9 +2,9 @@ package com.android.systemui.log import android.hardware.face.FaceManager import android.hardware.face.FaceSensorPropertiesInternal -import com.android.keyguard.FaceAuthUiEvent import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.FaceAuthUiEvent +import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus import com.android.systemui.log.core.LogLevel.DEBUG import com.android.systemui.log.dagger.FaceAuthLog import com.android.systemui.power.shared.model.WakeSleepReason diff --git a/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt b/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt index 919072a63220..171656a48e58 100644 --- a/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt @@ -108,4 +108,13 @@ class SideFpsLogger @Inject constructor(@BouncerLog private val buffer: LogBuffe } ) } + + fun authDurationChanged(duration: Long) { + buffer.log( + TAG, + LogLevel.DEBUG, + { long1 = duration }, + { "SideFpsSensor auth duration changed: $long1" } + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index 0d5ba641b599..24cb8fff9b67 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -18,7 +18,9 @@ package com.android.systemui.log.dagger; import android.os.Build; +import com.android.systemui.common.data.repository.PackageChangeRepository; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepositoryImpl; import com.android.systemui.log.LogBuffer; import com.android.systemui.log.LogBufferFactory; import com.android.systemui.log.LogcatEchoTracker; @@ -461,7 +463,7 @@ public class LogModule { /** * Provides a {@link LogBuffer} for use by - * {@link com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepositoryImpl}. + * {@link DeviceEntryFaceAuthRepositoryImpl}. */ @Provides @SysUISingleton @@ -600,4 +602,12 @@ public class LogModule { public static LogBuffer provideQBluetoothTileDialogLogBuffer(LogBufferFactory factory) { return factory.create("BluetoothTileDialogLog", 50); } + + /** Provides a {@link LogBuffer} for {@link PackageChangeRepository} */ + @Provides + @SysUISingleton + @PackageChangeRepoLog + public static LogBuffer providePackageChangeRepoLogBuffer(LogBufferFactory factory) { + return factory.create("PackageChangeRepo", 50); + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileUser.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/PackageChangeRepoLog.kt index efc743127418..93b776c2c85a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileUser.kt +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/PackageChangeRepoLog.kt @@ -14,12 +14,14 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.impl.custom.di.bound +package com.android.systemui.log.dagger +import com.android.systemui.common.data.repository.PackageChangeRepository +import com.android.systemui.log.LogBuffer import javax.inject.Qualifier -/** User associated with current custom tile binding. */ +/** A [LogBuffer] for [PackageChangeRepository]. */ @Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) -annotation class CustomTileUser +annotation class PackageChangeRepoLog diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java index a6c623391bb0..7e06f5a21113 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java @@ -87,6 +87,7 @@ import java.util.Locale; import java.util.Objects; import javax.inject.Inject; +import javax.inject.Provider; /** */ @@ -149,6 +150,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { public static final String EXTRA_CONFIRM_ONLY = "extra_confirm_only"; private final Context mContext; + private final SystemUIDialog.Factory mSystemUIDialogFactory; private final NotificationManager mNoMan; private final PowerManager mPowerMan; private final KeyguardManager mKeyguard; @@ -186,11 +188,17 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { /** */ @Inject - public PowerNotificationWarnings(Context context, ActivityStarter activityStarter, - BroadcastSender broadcastSender, Lazy<BatteryController> batteryControllerLazy, - DialogLaunchAnimator dialogLaunchAnimator, UiEventLogger uiEventLogger, - GlobalSettings globalSettings, UserTracker userTracker) { + public PowerNotificationWarnings( + Context context, + ActivityStarter activityStarter, + BroadcastSender broadcastSender, + Lazy<BatteryController> batteryControllerLazy, + DialogLaunchAnimator dialogLaunchAnimator, + UiEventLogger uiEventLogger, + UserTracker userTracker, + SystemUIDialog.Factory systemUIDialogFactory) { mContext = context; + mSystemUIDialogFactory = systemUIDialogFactory; mNoMan = mContext.getSystemService(NotificationManager.class); mPowerMan = (PowerManager) context.getSystemService(Context.POWER_SERVICE); mKeyguard = mContext.getSystemService(KeyguardManager.class); @@ -444,7 +452,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { private void showHighTemperatureDialog() { if (mHighTempDialog != null) return; - final SystemUIDialog d = new SystemUIDialog(mContext); + final SystemUIDialog d = mSystemUIDialogFactory.create(); d.setIconAttribute(android.R.attr.alertDialogIcon); d.setTitle(R.string.high_temp_title); d.setMessage(R.string.high_temp_dialog_message); @@ -479,7 +487,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { private void showThermalShutdownDialog() { if (mThermalShutdownDialog != null) return; - final SystemUIDialog d = new SystemUIDialog(mContext); + final SystemUIDialog d = mSystemUIDialogFactory.create(); d.setIconAttribute(android.R.attr.alertDialogIcon); d.setTitle(R.string.thermal_shutdown_title); d.setMessage(R.string.thermal_shutdown_dialog_message); @@ -643,7 +651,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { private void showStartSaverConfirmation(Bundle extras) { if (mSaverConfirmation != null || mUseExtraSaverConfirmation) return; - final SystemUIDialog d = new SystemUIDialog(mContext); + final SystemUIDialog d = mSystemUIDialogFactory.create(); final boolean confirmOnly = extras.getBoolean(BatterySaverUtils.EXTRA_CONFIRM_TEXT_ONLY); final int batterySaverTriggerMode = extras.getInt(BatterySaverUtils.EXTRA_POWER_SAVE_MODE_TRIGGER, diff --git a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt index faf9fbe3239d..7505566898c0 100644 --- a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt +++ b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt @@ -51,7 +51,10 @@ enum class WakeSleepReason( BIOMETRIC(isTouch = false, PowerManager.WAKE_REASON_BIOMETRIC), /** Something else happened to wake up or sleep the device. */ - OTHER(isTouch = false, PowerManager.WAKE_REASON_UNKNOWN); + OTHER(isTouch = false, PowerManager.WAKE_REASON_UNKNOWN), + + /** Device goes to sleep due to folding of a foldable device. */ + FOLD(isTouch = false, PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD); companion object { fun fromPowerManagerWakeReason(reason: Int): WakeSleepReason { @@ -72,6 +75,7 @@ enum class WakeSleepReason( fun fromPowerManagerSleepReason(reason: Int): WakeSleepReason { return when (reason) { PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON -> POWER_BUTTON + PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD -> FOLD else -> OTHER } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt index 6f35cfbfb4a5..b5def41fb3c7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt @@ -148,7 +148,8 @@ class FgsManagerControllerImpl @Inject constructor( private val deviceConfigProxy: DeviceConfigProxy, private val dialogLaunchAnimator: DialogLaunchAnimator, private val broadcastDispatcher: BroadcastDispatcher, - private val dumpManager: DumpManager + private val dumpManager: DumpManager, + private val systemUIDialogFactory: SystemUIDialog.Factory, ) : Dumpable, FgsManagerController { companion object { @@ -375,7 +376,7 @@ class FgsManagerControllerImpl @Inject constructor( override fun showDialog(expandable: Expandable?) { synchronized(lock) { if (dialog == null) { - val dialog = SystemUIDialog(context) + val dialog = systemUIDialogFactory.create() dialog.setTitle(R.string.fgs_manager_dialog_title) dialog.setMessage(R.string.fgs_manager_dialog_message) diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java index 3e50dd35749f..ac0bd29e22d6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java @@ -49,6 +49,7 @@ import kotlin.jvm.functions.Function1; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.Objects; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -231,32 +232,39 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr if (!collapsedView && mQsTileRevealController != null) { mQsTileRevealController.updateRevealedTiles(tiles); } - boolean shouldChange = false; + boolean shouldChangeAll = false; + // If the new tiles are a prefix of the old tiles, we delete the extra tiles (from the old). + // If not (even if they share a prefix) we remove all and add all the new ones. if (tiles.size() <= mRecords.size()) { int i = 0; + // Iterate through the requested tiles and check if they are the same as the existing + // tiles. for (QSTile tile : tiles) { if (tile != mRecords.get(i).tile) { - shouldChange = true; + shouldChangeAll = true; break; } i++; } - // If the first tiles are the same as the new ones, remove any extras. - if (!shouldChange) { - while (i < mRecords.size()) { - QSPanelControllerBase.TileRecord record = mRecords.get(i); + // If the first tiles are the same as the new ones, we reuse them and remove any extra + // tiles. + if (!shouldChangeAll && i < mRecords.size()) { + List<TileRecord> extraRecords = mRecords.subList(i, mRecords.size()); + for (QSPanelControllerBase.TileRecord record : extraRecords) { mView.removeTile(record); record.tile.removeCallback(record.callback); - i++; } + extraRecords.clear(); mCachedSpecs = getTilesSpecs(); } } else { - shouldChange = true; + shouldChangeAll = true; } - if (shouldChange) { + // If we detected that the existing tiles are different than the requested tiles, clear them + // and add the new tiles. + if (shouldChangeAll) { for (QSPanelControllerBase.TileRecord record : mRecords) { mView.removeTile(record); record.tile.removeCallback(record.callback); diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java index 47b062430ca5..a45d6f63cd81 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java @@ -259,7 +259,11 @@ public class TileQueryHelper { private State getState(Collection<QSTile> tiles, String spec) { for (QSTile tile : tiles) { if (spec.equals(tile.getTileSpec())) { - return tile.getState().copy(); + if (tile.isTileReady()) { + return tile.getState().copy(); + } else { + return null; + } } } return null; diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt index 5d28c8c0b69a..957cb1eb95d1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt @@ -319,7 +319,7 @@ constructor( override fun dumpProto(systemUIProtoDump: SystemUIProtoDump, args: Array<String>) { val data = - currentTiles.value.map { it.tile.state }.mapNotNull { it.toProto() }.toTypedArray() + currentTiles.value.map { it.tile.state }.mapNotNull { it?.toProto() }.toTypedArray() systemUIProtoDump.tiles = data } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java index ccf7afbe7016..c9b002209fa8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java @@ -55,6 +55,7 @@ public class DataSaverTile extends QSTileImpl<BooleanState> implements private final DataSaverController mDataSaverController; private final DialogLaunchAnimator mDialogLaunchAnimator; + private final SystemUIDialog.Factory mSystemUIDialogFactory; @Inject public DataSaverTile( @@ -68,12 +69,14 @@ public class DataSaverTile extends QSTileImpl<BooleanState> implements ActivityStarter activityStarter, QSLogger qsLogger, DataSaverController dataSaverController, - DialogLaunchAnimator dialogLaunchAnimator + DialogLaunchAnimator dialogLaunchAnimator, + SystemUIDialog.Factory systemUIDialogFactory ) { super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, statusBarStateController, activityStarter, qsLogger); mDataSaverController = dataSaverController; mDialogLaunchAnimator = dialogLaunchAnimator; + mSystemUIDialogFactory = systemUIDialogFactory; mDataSaverController.observe(getLifecycle(), this); } @@ -98,7 +101,7 @@ public class DataSaverTile extends QSTileImpl<BooleanState> implements // Show a dialog to confirm first. Dialogs shown by the DialogLaunchAnimator must be created // and shown on the main thread, so we post it to the UI handler. mUiHandler.post(() -> { - SystemUIDialog dialog = new SystemUIDialog(mContext); + SystemUIDialog dialog = mSystemUIDialogFactory.create(); dialog.setTitle(com.android.internal.R.string.data_saver_enable_title); dialog.setMessage(com.android.internal.R.string.data_saver_description); dialog.setPositiveButton(com.android.internal.R.string.data_saver_enable_button, diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt index 840db260a8d6..fc06090750ec 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt @@ -63,6 +63,10 @@ constructor(private val activityStarter: ActivityStarter) : QSTileIntentUserInpu InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE, ) } - activityStarter.postStartActivityDismissingKeyguard(pendingIntent, animationController) + activityStarter.startPendingIntentMaybeDismissingKeyguard( + pendingIntent, + null, + animationController + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt index 0a9a6d3c4eba..bc016bd221e9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt @@ -158,6 +158,33 @@ constructor( ) } + fun logError( + tileSpec: TileSpec, + message: String, + error: Throwable, + ) { + tileSpec + .getLogBuffer() + .log( + tileSpec.getLogTag(), + LogLevel.ERROR, + {}, + { message }, + error, + ) + } + + fun logCustomTileUserActionDelivered(tileSpec: TileSpec) { + tileSpec + .getLogBuffer() + .log( + tileSpec.getLogTag(), + LogLevel.DEBUG, + {}, + { "user action delivered to the service" }, + ) + } + private fun TileSpec.getLogTag(): String = "${TAG_FORMAT_PREFIX}_${this.spec}" private fun TileSpec.getLogBuffer(): LogBuffer = diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt index 382cfe267aae..6c9a8a4f3a3e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt @@ -56,7 +56,7 @@ constructor( override fun createTile(tileSpec: String): QSTile? { val viewModel: QSTileViewModel = when (val spec = TileSpec.create(tileSpec)) { - is TileSpec.CustomTileSpec -> null + is TileSpec.CustomTileSpec -> createCustomTileViewModel(spec) is TileSpec.PlatformTileSpec -> tileMap[tileSpec]?.get() is TileSpec.Invalid -> null } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileInteractor.kt deleted file mode 100644 index 14bf25d10d88..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileInteractor.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) 2023 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.qs.tiles.impl.custom - -import android.os.UserHandle -import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger -import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor -import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel -import com.android.systemui.qs.tiles.impl.di.QSTileScope -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow - -@QSTileScope -class CustomTileInteractor @Inject constructor() : QSTileDataInteractor<CustomTileDataModel> { - - override fun tileData( - user: UserHandle, - triggers: Flow<DataUpdateTrigger> - ): Flow<CustomTileDataModel> { - TODO("Not yet implemented") - } - - override fun availability(user: UserHandle): Flow<Boolean> { - TODO("Not yet implemented") - } -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileMapper.kt deleted file mode 100644 index e23a5c2f0b6a..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileMapper.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2023 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.qs.tiles.impl.custom - -import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper -import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel -import com.android.systemui.qs.tiles.impl.di.QSTileScope -import com.android.systemui.qs.tiles.viewmodel.QSTileConfig -import com.android.systemui.qs.tiles.viewmodel.QSTileState -import javax.inject.Inject - -@QSTileScope -class CustomTileMapper @Inject constructor() : QSTileDataToStateMapper<CustomTileDataModel> { - - override fun map(config: QSTileConfig, data: CustomTileDataModel): QSTileState { - TODO("Not yet implemented") - } -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileComponent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileComponent.kt index 88bc8fa81e1a..7b099c2cb0c6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileComponent.kt @@ -16,7 +16,10 @@ package com.android.systemui.qs.tiles.impl.custom.di +import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTilePackageUpdatesRepository import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel +import com.android.systemui.qs.tiles.impl.custom.domain.interactor.CustomTileInteractor +import com.android.systemui.qs.tiles.impl.custom.domain.interactor.CustomTileServiceInteractor import com.android.systemui.qs.tiles.impl.di.QSTileComponent import com.android.systemui.qs.tiles.impl.di.QSTileScope import dagger.Subcomponent @@ -25,6 +28,12 @@ import dagger.Subcomponent @Subcomponent(modules = [QSTileConfigModule::class, CustomTileModule::class]) interface CustomTileComponent : QSTileComponent<CustomTileDataModel> { + fun customTileInterfaceInteractor(): CustomTileServiceInteractor + + fun customTileInteractor(): CustomTileInteractor + + fun customTilePackageUpdatesRepository(): CustomTilePackageUpdatesRepository + @Subcomponent.Builder interface Builder { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileModule.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileModule.kt index ba8b23ab795a..196fa12cacc9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileModule.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileModule.kt @@ -20,14 +20,16 @@ import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor import com.android.systemui.qs.tiles.base.viewmodel.QSTileCoroutineScopeFactory -import com.android.systemui.qs.tiles.impl.custom.CustomTileInteractor -import com.android.systemui.qs.tiles.impl.custom.CustomTileMapper -import com.android.systemui.qs.tiles.impl.custom.CustomTileUserActionInteractor import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileDefaultsRepository import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileDefaultsRepositoryImpl +import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTilePackageUpdatesRepository +import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTilePackageUpdatesRepositoryImpl import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileRepository import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileRepositoryImpl +import com.android.systemui.qs.tiles.impl.custom.domain.CustomTileMapper import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel +import com.android.systemui.qs.tiles.impl.custom.domain.interactor.CustomTileDataInteractor +import com.android.systemui.qs.tiles.impl.custom.domain.interactor.CustomTileUserActionInteractor import com.android.systemui.qs.tiles.impl.di.QSTileScope import dagger.Binds import dagger.Module @@ -40,7 +42,7 @@ interface CustomTileModule { @Binds fun bindDataInteractor( - dataInteractor: CustomTileInteractor + dataInteractor: CustomTileDataInteractor ): QSTileDataInteractor<CustomTileDataModel> @Binds @@ -58,6 +60,11 @@ interface CustomTileModule { @Binds fun bindCustomTileRepository(impl: CustomTileRepositoryImpl): CustomTileRepository + @Binds + abstract fun bindCustomTilePackageUpdatesRepository( + impl: CustomTilePackageUpdatesRepositoryImpl + ): CustomTilePackageUpdatesRepository + companion object { @Provides diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/CustomTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/CustomTileMapper.kt new file mode 100644 index 000000000000..875079cae8eb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/CustomTileMapper.kt @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2023 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.qs.tiles.impl.custom.domain + +import android.annotation.SuppressLint +import android.app.IUriGrantsManager +import android.content.Context +import android.graphics.drawable.Drawable +import android.os.UserHandle +import android.service.quicksettings.Tile +import android.widget.Button +import android.widget.Switch +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper +import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel +import com.android.systemui.qs.tiles.viewmodel.QSTileConfig +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import javax.inject.Inject + +@SysUISingleton +class CustomTileMapper +@Inject +constructor( + private val context: Context, + private val uriGrantsManager: IUriGrantsManager, +) : QSTileDataToStateMapper<CustomTileDataModel> { + + override fun map(config: QSTileConfig, data: CustomTileDataModel): QSTileState { + val userContext = context.createContextAsUser(UserHandle(data.user.identifier), 0) + + val iconResult = + getIconProvider( + userContext = userContext, + icon = data.tile.icon, + callingAppUid = data.callingAppUid, + packageName = data.componentName.packageName, + defaultIcon = data.defaultTileIcon, + ) + + return QSTileState.build(iconResult.iconProvider, data.tile.label) { + var tileState: Int = data.tile.state + if (data.hasPendingBind) { + tileState = Tile.STATE_UNAVAILABLE + } + + icon = iconResult.iconProvider + activationState = + if (iconResult.failedToLoad) { + QSTileState.ActivationState.INACTIVE + } else { + QSTileState.ActivationState.valueOf(tileState) + } + + if (!data.tile.subtitle.isNullOrEmpty()) { + secondaryLabel = data.tile.subtitle + } + + contentDescription = data.tile.contentDescription + stateDescription = data.tile.stateDescription + + if (!data.isToggleable) { + sideViewIcon = QSTileState.SideViewIcon.Chevron + } + + supportedActions = + if (tileState == Tile.STATE_UNAVAILABLE) { + setOf(QSTileState.UserAction.LONG_CLICK) + } else { + setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) + } + expandedAccessibilityClass = + if (data.isToggleable) { + Switch::class + } else { + Button::class + } + } + } + + @SuppressLint("MissingPermission") // android.permission.INTERACT_ACROSS_USERS_FULL + private fun getIconProvider( + userContext: Context, + icon: android.graphics.drawable.Icon?, + callingAppUid: Int, + packageName: String, + defaultIcon: android.graphics.drawable.Icon?, + ): IconResult { + var failedToLoad = false + val drawable: Drawable? = + try { + icon?.loadDrawableCheckingUriGrant( + userContext, + uriGrantsManager, + callingAppUid, + packageName, + ) + } catch (e: Exception) { + failedToLoad = true + null + } ?: defaultIcon?.loadDrawable(userContext) + return IconResult( + { + drawable?.constantState?.newDrawable()?.let { + Icon.Loaded(it, contentDescription = null) + } + }, + failedToLoad, + ) + } + + class IconResult( + val iconProvider: () -> Icon?, + val failedToLoad: Boolean, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/entity/CustomTileDataModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/entity/CustomTileDataModel.kt index f095c01126c4..5b6ff1e033fb 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/entity/CustomTileDataModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/entity/CustomTileDataModel.kt @@ -20,16 +20,14 @@ import android.content.ComponentName import android.graphics.drawable.Icon import android.os.UserHandle import android.service.quicksettings.Tile -import com.android.systemui.qs.tiles.impl.custom.di.bound.CustomTileBoundComponent data class CustomTileDataModel( val user: UserHandle, val componentName: ComponentName, val tile: Tile, + val isToggleable: Boolean, val callingAppUid: Int, val hasPendingBind: Boolean, - val shouldShowChevron: Boolean, - val defaultTileLabel: CharSequence?, - val defaultTileIcon: Icon?, - val component: CustomTileBoundComponent, + val defaultTileLabel: CharSequence, + val defaultTileIcon: Icon, ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileDataInteractor.kt new file mode 100644 index 000000000000..cff95d8368a2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileDataInteractor.kt @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2023 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.qs.tiles.impl.custom.domain.interactor + +import android.os.UserHandle +import android.service.quicksettings.Tile +import com.android.systemui.common.coroutine.ConflatedCallbackFlow +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger +import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor +import com.android.systemui.qs.tiles.impl.custom.data.entity.CustomTileDefaults +import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileDefaultsRepository +import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTilePackageUpdatesRepository +import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel +import com.android.systemui.qs.tiles.impl.di.QSTileScope +import com.android.systemui.user.data.repository.UserRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.launch + +@QSTileScope +@OptIn(ExperimentalCoroutinesApi::class) +class CustomTileDataInteractor +@Inject +constructor( + private val tileSpec: TileSpec.CustomTileSpec, + private val defaultsRepository: CustomTileDefaultsRepository, + private val serviceInteractor: CustomTileServiceInteractor, + private val customTileInteractor: CustomTileInteractor, + private val packageUpdatesRepository: CustomTilePackageUpdatesRepository, + userRepository: UserRepository, + @QSTileScope private val tileScope: CoroutineScope, +) : QSTileDataInteractor<CustomTileDataModel> { + + private val mutableUserFlow = MutableStateFlow(userRepository.getSelectedUserInfo().userHandle) + private val bindingFlow = + mutableUserFlow + .flatMapLatest { user -> + ConflatedCallbackFlow.conflatedCallbackFlow { + serviceInteractor.setUser(user) + + // Wait for the CustomTileInteractor to become initialized first, because + // binding + // the service might access it + customTileInteractor.initForUser(user) + // Bind the TileService for not active tile + serviceInteractor.bindOnStart() + + packageUpdatesRepository + .getPackageChangesForUser(user) + .onEach { + defaultsRepository.requestNewDefaults( + user, + tileSpec.componentName, + true + ) + } + .launchIn(this) + + send(Unit) + awaitClose { serviceInteractor.unbind() } + } + } + .shareIn(tileScope, SharingStarted.WhileSubscribed()) + + init { + // Initialize binding once to flush all the pending messages inside + // CustomTileServiceInteractor and then unbind if the tile data isn't observed. This ensures + // that all the interactors are loaded and warmed up before binding. + tileScope.launch { bindingFlow.first() } + } + + override fun tileData( + user: UserHandle, + triggers: Flow<DataUpdateTrigger> + ): Flow<CustomTileDataModel> { + tileScope.launch { mutableUserFlow.emit(user) } + return bindingFlow.combine(triggers) { _, _ -> }.flatMapLatest { dataFlow(user) } + } + + private fun dataFlow(user: UserHandle): Flow<CustomTileDataModel> = + combine( + serviceInteractor.refreshEvents.onStart { emit(Unit) }, + serviceInteractor.callingAppIds, + customTileInteractor.getTiles(user), + defaultsRepository.defaults(user).mapNotNull { it as? CustomTileDefaults.Result }, + ) { _: Unit, callingAppId: Int, tile: Tile, defaults: CustomTileDefaults.Result -> + CustomTileDataModel( + user = user, + componentName = tileSpec.componentName, + tile = tile, + callingAppUid = callingAppId, + hasPendingBind = serviceInteractor.hasPendingBind(), + defaultTileLabel = defaults.label, + defaultTileIcon = defaults.icon, + isToggleable = customTileInteractor.isTileToggleable(), + ) + } + + override fun availability(user: UserHandle): Flow<Boolean> = + with(defaultsRepository) { + requestNewDefaults(user, tileSpec.componentName) + return defaults(user).map { it is CustomTileDefaults.Result } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt index 10b012d30fcd..fd96fc5b6693 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt @@ -19,12 +19,14 @@ package com.android.systemui.qs.tiles.impl.custom.domain.interactor import android.os.UserHandle import android.service.quicksettings.Tile import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileDefaultsRepository import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileRepository import com.android.systemui.qs.tiles.impl.di.QSTileScope import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -32,21 +34,29 @@ import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock /** Manages updates of the [Tile] assigned for the current custom tile. */ @QSTileScope class CustomTileInteractor @Inject constructor( + private val tileSpec: TileSpec.CustomTileSpec, private val defaultsRepository: CustomTileDefaultsRepository, private val customTileRepository: CustomTileRepository, @QSTileScope private val tileScope: CoroutineScope, @Background private val backgroundContext: CoroutineContext, ) { + private val userMutex = Mutex() private val tileUpdates = MutableSharedFlow<Tile>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + private var currentUser: UserHandle? = null + private var updatesJob: Job? = null + /** [Tile] updates. [updateTile] to emit a new one. */ fun getTiles(user: UserHandle): Flow<Tile> = customTileRepository.getTiles(user) @@ -55,7 +65,7 @@ constructor( * * @throws IllegalStateException when the repository stores a tile for another user. This means * the tile hasn't been updated for the current user. Can happen when this is accessed before - * [init] returns. + * [initForUser] returns. */ fun getTile(user: UserHandle): Tile = customTileRepository.getTile(user) @@ -67,45 +77,60 @@ constructor( suspend fun isTileToggleable(): Boolean = customTileRepository.isTileToggleable() /** - * Initializes the repository for the current user. Suspends until it's safe to call [tile] + * Initializes the repository for the current user. Suspends until it's safe to call [getTile] * which needs at least one of the following: * - defaults are loaded; * - receive tile update in [updateTile]; * - restoration happened for a persisted tile. */ suspend fun initForUser(user: UserHandle) { - launchUpdates(user) - customTileRepository.restoreForTheUserIfNeeded(user, customTileRepository.isTileActive()) - // Suspend to make sure it gets the tile from one of the sources: restoration, defaults, or - // tile update. - customTileRepository.getTiles(user).firstOrNull() + userMutex.withLock { + if (currentUser == user) { + return + } + updatesJob?.cancel() + defaultsRepository.requestNewDefaults(user, tileSpec.componentName) + launchUpdates(user) + customTileRepository.restoreForTheUserIfNeeded( + user, + customTileRepository.isTileActive() + ) + // Suspend to make sure it gets the tile from one of the sources: restoration, defaults, + // or + // tile update. + customTileRepository.getTiles(user).firstOrNull() + currentUser = user + } } private fun launchUpdates(user: UserHandle) { - tileUpdates - .onEach { - customTileRepository.updateWithTile( - user, - it, - customTileRepository.isTileActive(), - ) - } - .flowOn(backgroundContext) - .launchIn(tileScope) - defaultsRepository - .defaults(user) - .onEach { - customTileRepository.updateWithDefaults( - user, - it, - customTileRepository.isTileActive(), - ) + updatesJob = + tileScope.launch { + tileUpdates + .onEach { + customTileRepository.updateWithTile( + user, + it, + customTileRepository.isTileActive(), + ) + } + .flowOn(backgroundContext) + .launchIn(this) + defaultsRepository + .defaults(user) + .onEach { + customTileRepository.updateWithDefaults( + user, + it, + customTileRepository.isTileActive(), + ) + } + .flowOn(backgroundContext) + .launchIn(this) } - .flowOn(backgroundContext) - .launchIn(tileScope) } - /** Updates current [Tile]. Emits a new event in [tiles]. */ + /** Updates current [Tile]. Emits a new event in [getTiles]. */ fun updateTile(newTile: Tile) { tileUpdates.tryEmit(newTile) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileServiceInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileServiceInteractor.kt new file mode 100644 index 000000000000..acff40f816a9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileServiceInteractor.kt @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2023 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.qs.tiles.impl.custom.domain.interactor + +import android.app.PendingIntent +import android.content.ComponentName +import android.os.IBinder +import android.os.Process +import android.os.RemoteException +import android.os.UserHandle +import android.service.quicksettings.IQSTileService +import android.service.quicksettings.Tile +import android.service.quicksettings.TileService +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.qs.external.CustomTileInterface +import com.android.systemui.qs.external.TileServiceManager +import com.android.systemui.qs.external.TileServices +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.tiles.base.logging.QSTileLogger +import com.android.systemui.qs.tiles.impl.di.QSTileScope +import com.android.systemui.user.data.repository.UserRepository +import dagger.Lazy +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.produce +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch + +/** + * Communicates with [TileService] via [TileServiceManager] and [IQSTileService]. This interactor is + * also responsible for the binding to the [TileService]. + */ +@OptIn(ExperimentalCoroutinesApi::class) +@QSTileScope +class CustomTileServiceInteractor +@Inject +constructor( + private val tileSpec: TileSpec.CustomTileSpec, + private val activityStarter: ActivityStarter, + private val userActionInteractor: Lazy<CustomTileUserActionInteractor>, + private val customTileInteractor: CustomTileInteractor, + private val userRepository: UserRepository, + private val qsTileLogger: QSTileLogger, + private val tileServices: TileServices, + @QSTileScope private val tileScope: CoroutineScope, +) { + + private val tileReceivingInterface = ReceivingInterface() + private var tileServiceManager: TileServiceManager? = null + private val tileServiceInterface: IQSTileService + get() = getTileServiceManager().tileService + + private var currentUser: UserHandle = userRepository.getSelectedUserInfo().userHandle + private var destructionJob: Job? = null + + val callingAppIds: Flow<Int> + get() = tileReceivingInterface.mutableCallingAppIds + val refreshEvents: Flow<Unit> + get() = tileReceivingInterface.mutableRefreshEvents + + /** Clears all pending binding for an active tile and binds not active one. */ + fun bindOnStart() { + try { + with(getTileServiceManager()) { + if (isActiveTile) { + clearPendingBind() + } else { + setBindRequested(true) + tileServiceInterface.onStartListening() + } + } + } catch (e: RemoteException) { + qsTileLogger.logError(tileSpec, "Binding to the service failed", e) + } + } + + /** Binds active tile WITHOUT CLEARING pending binds. */ + fun bindOnClick() { + try { + with(getTileServiceManager()) { + if (isActiveTile) { + setBindRequested(true) + tileServiceInterface.onStartListening() + } + } + } catch (e: RemoteException) { + qsTileLogger.logError(tileSpec, "Binding to the service on click failed", e) + } + } + + /** Releases resources held by the binding and prepares the interactor to be collected */ + fun unbind() { + try { + with(userActionInteractor.get()) { + clearLastClickedView() + tileServiceInterface.onStopListening() + revokeToken(false) + setShowingDialog(false) + } + getTileServiceManager().setBindRequested(false) + } catch (e: RemoteException) { + qsTileLogger.logError(tileSpec, "Unbinding failed", e) + } + } + + /** + * Checks if [TileServiceManager] has a pending [android.service.quicksettings.TileService] + * bind. + */ + fun hasPendingBind(): Boolean = getTileServiceManager().hasPendingBind() + + /** Sets a [user] for the custom tile to use. User change triggers service rebinding. */ + fun setUser(user: UserHandle) { + if (user == currentUser) { + return + } + currentUser = user + destructionJob?.cancel() + + tileServiceManager = null + } + + /** Sends click event to [TileService] using [IQSTileService.onClick]. */ + fun onClick(token: IBinder) { + tileServiceInterface.onClick(token) + } + + private fun getTileServiceManager(): TileServiceManager = + synchronized(tileServices) { + if (tileServiceManager == null) { + tileServices + .getTileWrapper(tileReceivingInterface) + .also { destructionJob = createDestructionJob() } + .also { tileServiceManager = it } + } else { + tileServiceManager!! + } + } + + /** + * This job used to free the resources when the [QSTileScope] coroutine scope gets cancelled by + * the View Model. + */ + private fun createDestructionJob(): Job = + tileScope.launch { + produce<Unit> { + awaitClose { + userActionInteractor.get().revokeToken(true) + tileServices.freeService(tileReceivingInterface, getTileServiceManager()) + destructionJob = null + } + } + } + + private inner class ReceivingInterface : CustomTileInterface { + + override val user: Int + get() = currentUser.identifier + override val qsTile: Tile + get() = customTileInteractor.getTile(currentUser) + override val component: ComponentName = tileSpec.componentName + + val mutableCallingAppIds = MutableStateFlow(Process.INVALID_UID) + val mutableRefreshEvents = MutableSharedFlow<Unit>() + + override fun getTileSpec(): String = tileSpec.spec + + override fun refreshState() { + tileScope.launch { mutableRefreshEvents.emit(Unit) } + } + + override fun updateTileState(tile: Tile, uid: Int) { + customTileInteractor.updateTile(tile) + mutableCallingAppIds.tryEmit(uid) + } + + override fun onDialogShown() { + userActionInteractor.get().setShowingDialog(true) + } + + override fun onDialogHidden() = + with(userActionInteractor.get()) { + setShowingDialog(false) + revokeToken(true) + } + + override fun startActivityAndCollapse(pendingIntent: PendingIntent) { + userActionInteractor.get().startActivityAndCollapse(pendingIntent) + } + + override fun startUnlockAndRun() { + activityStarter.postQSRunnableDismissingKeyguard { + tileServiceInterface.onUnlockComplete() + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt new file mode 100644 index 000000000000..c3e1feaa6dfe --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2023 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.qs.tiles.impl.custom.domain.interactor + +import android.app.PendingIntent +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.Uri +import android.os.Binder +import android.os.IBinder +import android.os.RemoteException +import android.os.UserHandle +import android.provider.Settings +import android.service.quicksettings.TileService +import android.view.IWindowManager +import android.view.View +import android.view.WindowManager +import androidx.annotation.GuardedBy +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler +import com.android.systemui.qs.tiles.base.interactor.QSTileInput +import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor +import com.android.systemui.qs.tiles.base.logging.QSTileLogger +import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel +import com.android.systemui.qs.tiles.impl.di.QSTileScope +import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction +import com.android.systemui.settings.DisplayTracker +import java.util.concurrent.atomic.AtomicReference +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.withContext + +@QSTileScope +class CustomTileUserActionInteractor +@Inject +constructor( + private val context: Context, + private val tileSpec: TileSpec, + private val qsTileLogger: QSTileLogger, + private val windowManager: IWindowManager, + private val displayTracker: DisplayTracker, + private val qsTileIntentUserInputHandler: QSTileIntentUserInputHandler, + @Background private val backgroundContext: CoroutineContext, + private val serviceInteractor: CustomTileServiceInteractor, +) : QSTileUserActionInteractor<CustomTileDataModel> { + + private val token: IBinder = Binder() + + @GuardedBy("token") private var isTokenGranted: Boolean = false + @GuardedBy("token") private var isShowingDialog: Boolean = false + private val lastClickedView: AtomicReference<View> = AtomicReference<View>() + + override suspend fun handleInput(input: QSTileInput<CustomTileDataModel>) = + with(input) { + when (action) { + is QSTileUserAction.Click -> click(action.view, data.tile.activityLaunchForClick) + is QSTileUserAction.LongClick -> + longClick(user, action.view, data.componentName, data.tile.state) + } + qsTileLogger.logCustomTileUserActionDelivered(tileSpec) + } + + private fun click( + view: View?, + activityLaunchForClick: PendingIntent?, + ) { + grantToken() + try { + // Bind active tile to deliver user action + serviceInteractor.bindOnClick() + if (activityLaunchForClick == null) { + lastClickedView.set(view) + serviceInteractor.onClick(token) + } else { + qsTileIntentUserInputHandler.handle(view, activityLaunchForClick) + } + } catch (e: RemoteException) { + qsTileLogger.logError(tileSpec, "Failed to deliver click", e) + } + } + + fun revokeToken(ignoreShownDialog: Boolean) { + synchronized(token) { + if (isTokenGranted && (ignoreShownDialog || !isShowingDialog)) { + try { + windowManager.removeWindowToken(token, displayTracker.defaultDisplayId) + } catch (e: RemoteException) { + qsTileLogger.logError(tileSpec, "Failed to remove a window token", e) + } + isTokenGranted = false + } + } + } + + fun setShowingDialog(isShowingDialog: Boolean) { + synchronized(token) { this.isShowingDialog = isShowingDialog } + } + + fun startActivityAndCollapse(pendingIntent: PendingIntent) { + if (!pendingIntent.isActivity) { + return + } + if (!isTokenGranted) { + return + } + qsTileIntentUserInputHandler.handle(lastClickedView.getAndSet(null), pendingIntent) + } + + fun clearLastClickedView() = lastClickedView.set(null) + + private fun grantToken() { + synchronized(token) { + if (!isTokenGranted) { + try { + windowManager.addWindowToken( + token, + WindowManager.LayoutParams.TYPE_QS_DIALOG, + displayTracker.defaultDisplayId, + null /* options */ + ) + } catch (e: RemoteException) { + qsTileLogger.logError(tileSpec, "Failed to grant a window token", e) + } + isTokenGranted = true + } + } + } + + private suspend fun longClick( + user: UserHandle, + view: View?, + componentName: ComponentName, + state: Int + ) { + val resolvedIntent: Intent? = + resolveIntent( + Intent(TileService.ACTION_QS_TILE_PREFERENCES).apply { + setPackage(componentName.packageName) + }, + user, + ) + ?.apply { + putExtra(Intent.EXTRA_COMPONENT_NAME, componentName) + putExtra(TileService.EXTRA_STATE, state) + } + if (resolvedIntent == null) { + qsTileIntentUserInputHandler.handle( + view, + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setData( + Uri.fromParts(IntentFilter.SCHEME_PACKAGE, componentName.packageName, null) + ) + ) + } else { + qsTileIntentUserInputHandler.handle(view, resolvedIntent) + } + } + + /** + * Returns an intent resolved by [android.content.pm.PackageManager.resolveActivityAsUser] or + * null. + */ + private suspend fun resolveIntent(intent: Intent, user: UserHandle): Intent? = + withContext(backgroundContext) { + val activityInfo = + context.packageManager + .resolveActivityAsUser(intent, 0, user.identifier) + ?.activityInfo + activityInfo ?: return@withContext null + with(activityInfo) { + Intent(TileService.ACTION_QS_TILE_PREFERENCES).apply { + setClassName(packageName, name) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt index be1b7404314f..b927e41bc97e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt @@ -32,7 +32,7 @@ import kotlin.reflect.KClass * // TODO(b/http://b/299909989): Clean up legacy mappings after the transition */ data class QSTileState( - val icon: () -> Icon, + val icon: () -> Icon?, val label: CharSequence, val activationState: ActivationState, val secondaryLabel: CharSequence?, @@ -60,7 +60,7 @@ data class QSTileState( ) } - fun build(icon: () -> Icon, label: CharSequence, build: Builder.() -> Unit): QSTileState = + fun build(icon: () -> Icon?, label: CharSequence, build: Builder.() -> Unit): QSTileState = Builder(icon, label).apply(build).build() } @@ -108,7 +108,7 @@ data class QSTileState( } class Builder( - var icon: () -> Icon, + var icon: () -> Icon?, var label: CharSequence, ) { var activationState: ActivationState = ActivationState.INACTIVE diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt index ef3df482ea45..226e2fa0549f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt @@ -45,7 +45,10 @@ interface QSTileViewModel { */ fun onUserChanged(user: UserHandle) - /** Triggers the emission of the new [QSTileState] in a [state]. */ + /** + * Triggers the emission of the new [QSTileState] in a [state]. The new value can still be + * skipped if there is no change. + */ fun forceUpdate() /** Notifies underlying logic about user input. */ diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt index 977df81a0783..4780a2e9ccee 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt @@ -37,6 +37,7 @@ import java.util.function.Supplier import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectIndexed +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -60,27 +61,34 @@ constructor( private val listeningClients: MutableCollection<Any> = mutableSetOf() // Cancels the jobs when the adapter is no longer alive - private var availabilityJob: Job? = null + private var tileAdapterJob: Job? = null // Cancels the jobs when clients stop listening private var stateJob: Job? = null init { - availabilityJob = + tileAdapterJob = applicationScope.launch { - qsTileViewModel.isAvailable.collectIndexed { index, isAvailable -> - if (!isAvailable) { - qsHost.removeTile(tileSpec) - } - // qsTileViewModel.isAvailable flow often starts with isAvailable == true. - // That's - // why we only allow isAvailable == true once and throw an exception afterwards. - if (index > 0 && isAvailable) { - // See com.android.systemui.qs.pipeline.domain.model.AutoAddable for - // additional - // guidance on how to auto add your tile - throw UnsupportedOperationException("Turning on tile is not supported now") + launch { + qsTileViewModel.isAvailable.collectIndexed { index, isAvailable -> + if (!isAvailable) { + qsHost.removeTile(tileSpec) + } + // qsTileViewModel.isAvailable flow often starts with isAvailable == true. + // That's + // why we only allow isAvailable == true once and throw an exception + // afterwards. + if (index > 0 && isAvailable) { + // See com.android.systemui.qs.pipeline.domain.model.AutoAddable for + // additional + // guidance on how to auto add your tile + throw UnsupportedOperationException( + "Turning on tile is not supported now" + ) + } } } + // Warm up tile with some initial state + launch { qsTileViewModel.state.first() } } // QSTileHost doesn't call this when userId is initialized @@ -185,7 +193,7 @@ constructor( override fun destroy() { stateJob?.cancel() - availabilityJob?.cancel() + tileAdapterJob?.cancel() qsTileViewModel.destroy() } @@ -222,8 +230,9 @@ constructor( QSTile.BooleanState().apply { spec = config.tileSpec.spec label = viewModelState.label - // This value is synthetic and doesn't have any meaning - value = false + // This value is synthetic and doesn't have any meaning. It's only needed to satisfy + // CTS tests. + value = viewModelState.activationState == QSTileState.ActivationState.ACTIVE secondaryLabel = viewModelState.secondaryLabel handlesLongClick = @@ -233,6 +242,7 @@ constructor( when (val stateIcon = viewModelState.icon()) { is Icon.Loaded -> DrawableIcon(stateIcon.drawable) is Icon.Resource -> ResourceIcon.get(stateIcon.res) + null -> null } } state = viewModelState.activationState.legacyState diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt index acd7510a6c2a..41cd221186fe 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt @@ -23,7 +23,6 @@ import android.content.DialogInterface.BUTTON_NEUTRAL import android.content.Intent import android.provider.Settings import android.view.LayoutInflater -import androidx.annotation.VisibleForTesting import com.android.internal.jank.InteractionJankMonitor import com.android.internal.logging.UiEventLogger import com.android.systemui.res.R @@ -44,31 +43,15 @@ import javax.inject.Provider * Controller for [UserDialog]. */ @SysUISingleton -class UserSwitchDialogController @VisibleForTesting constructor( - private val userDetailViewAdapterProvider: Provider<UserDetailView.Adapter>, - private val activityStarter: ActivityStarter, - private val falsingManager: FalsingManager, - private val dialogLaunchAnimator: DialogLaunchAnimator, - private val uiEventLogger: UiEventLogger, - private val dialogFactory: (Context) -> SystemUIDialog +class UserSwitchDialogController @Inject constructor( + private val userDetailViewAdapterProvider: Provider<UserDetailView.Adapter>, + private val activityStarter: ActivityStarter, + private val falsingManager: FalsingManager, + private val dialogLaunchAnimator: DialogLaunchAnimator, + private val uiEventLogger: UiEventLogger, + private val dialogFactory: SystemUIDialog.Factory ) { - @Inject - constructor( - userDetailViewAdapterProvider: Provider<UserDetailView.Adapter>, - activityStarter: ActivityStarter, - falsingManager: FalsingManager, - dialogLaunchAnimator: DialogLaunchAnimator, - uiEventLogger: UiEventLogger - ) : this( - userDetailViewAdapterProvider, - activityStarter, - falsingManager, - dialogLaunchAnimator, - uiEventLogger, - { SystemUIDialog(it) } - ) - companion object { private const val INTERACTION_JANK_TAG = "switch_user" private val USER_SETTINGS_INTENT = Intent(Settings.ACTION_USER_SETTINGS) @@ -81,7 +64,7 @@ class UserSwitchDialogController @VisibleForTesting constructor( * [userDetailViewAdapterProvider] and show it as launched from [expandable]. */ fun showDialog(context: Context, expandable: Expandable) { - with(dialogFactory(context)) { + with(dialogFactory.create()) { setShowForAllUsers(true) setCanceledOnTouchOutside(true) diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java index f07162377358..9076182def70 100644 --- a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java +++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java @@ -21,8 +21,10 @@ import android.annotation.SuppressLint; import android.annotation.TestApi; import android.content.Context; import android.content.res.Configuration; +import android.content.res.Resources; import android.hardware.devicestate.DeviceStateManager; import android.hardware.devicestate.DeviceStateManagerGlobal; +import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup.LayoutParams; import android.widget.LinearLayout; @@ -72,20 +74,27 @@ public class RearDisplayDialogController implements private DeviceStateManager.DeviceStateCallback mDeviceStateManagerCallback = new DeviceStateManagerCallback(); - private final Context mContext; private final CommandQueue mCommandQueue; private final Executor mExecutor; + private final Resources mResources; + private final LayoutInflater mLayoutInflater; + private final SystemUIDialog.Factory mSystemUIDialogFactory; - @VisibleForTesting - SystemUIDialog mRearDisplayEducationDialog; + private SystemUIDialog mRearDisplayEducationDialog; @Nullable LinearLayout mDialogViewContainer; @Inject - public RearDisplayDialogController(Context context, CommandQueue commandQueue, - @Main Executor executor) { - mContext = context; + public RearDisplayDialogController( + CommandQueue commandQueue, + @Main Executor executor, + @Main Resources resources, + LayoutInflater layoutInflater, + SystemUIDialog.Factory systemUIDialogFactory) { mCommandQueue = commandQueue; mExecutor = executor; + mResources = resources; + mLayoutInflater = layoutInflater; + mSystemUIDialogFactory = systemUIDialogFactory; } @Override @@ -104,8 +113,7 @@ public class RearDisplayDialogController implements if (mRearDisplayEducationDialog != null && mRearDisplayEducationDialog.isShowing() && mDialogViewContainer != null) { // Refresh the dialog view when configuration is changed. - Context dialogContext = mRearDisplayEducationDialog.getContext(); - View dialogView = createDialogView(dialogContext); + View dialogView = createDialogView(mRearDisplayEducationDialog.getContext()); mDialogViewContainer.removeAllViews(); mDialogViewContainer.addView(dialogView); } @@ -114,9 +122,7 @@ public class RearDisplayDialogController implements private void createAndShowDialog() { mServiceNotified = false; Context dialogContext = mRearDisplayEducationDialog.getContext(); - View dialogView = createDialogView(dialogContext); - mDialogViewContainer = new LinearLayout(dialogContext); mDialogViewContainer.setLayoutParams( new LinearLayout.LayoutParams( @@ -133,11 +139,11 @@ public class RearDisplayDialogController implements private View createDialogView(Context context) { View dialogView; + LayoutInflater inflater = mLayoutInflater.cloneInContext(context); if (mStartedFolded) { - dialogView = View.inflate(context, - R.layout.activity_rear_display_education, null); + dialogView = inflater.inflate(R.layout.activity_rear_display_education, null); } else { - dialogView = View.inflate(context, + dialogView = inflater.inflate( R.layout.activity_rear_display_education_opened, null); } LottieAnimationView animationView = dialogView.findViewById( @@ -172,9 +178,9 @@ public class RearDisplayDialogController implements * Ensures we're not using old values from when the dialog may have been shown previously. */ private void initializeValues(int startingBaseState) { - mRearDisplayEducationDialog = new SystemUIDialog(mContext); + mRearDisplayEducationDialog = mSystemUIDialogFactory.create(); if (mFoldedStates == null) { - mFoldedStates = mContext.getResources().getIntArray( + mFoldedStates = mResources.getIntArray( com.android.internal.R.array.config_foldedDeviceStates); } mStartedFolded = isFoldedState(startingBaseState); diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 5abb4dde856b..c96651c1057e 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -44,6 +44,7 @@ import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICA import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING +import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.notification.stack.shared.flexiNotifsEnabled import com.android.systemui.util.asIndenting import com.android.systemui.util.printSection @@ -83,6 +84,7 @@ constructor( private val powerInteractor: PowerInteractor, private val simBouncerInteractor: Lazy<SimBouncerInteractor>, private val authenticationInteractor: Lazy<AuthenticationInteractor>, + private val windowController: NotificationShadeWindowController, ) : CoreStartable { override fun start() { @@ -92,6 +94,7 @@ constructor( automaticallySwitchScenes() hydrateSystemUiState() collectFalsingSignals() + hydrateWindowFocus() } else { sceneLogger.logFrameworkEnabled( isEnabled = false, @@ -348,6 +351,20 @@ constructor( } } + /** Keeps the focus state of the window view up-to-date. */ + private fun hydrateWindowFocus() { + applicationScope.launch { + sceneInteractor.transitionState + .mapNotNull { transitionState -> + (transitionState as? ObservableTransitionState.Idle)?.scene + } + .distinctUntilChanged() + .collect { sceneKey -> + windowController.setNotificationShadeFocusable(sceneKey != SceneKey.Gone) + } + } + } + private fun switchToScene(targetSceneKey: SceneKey, loggingReason: String) { sceneInteractor.changeScene( scene = SceneModel(targetSceneKey), diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 286037ef1961..a5c1cf1af191 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -26,6 +26,7 @@ import static com.android.keyguard.KeyguardClockSwitch.LARGE; import static com.android.keyguard.KeyguardClockSwitch.SMALL; import static com.android.systemui.Flags.keyguardBottomAreaRefactor; import static com.android.systemui.Flags.migrateClocksToBlueprint; +import static com.android.systemui.Flags.predictiveBackAnimateShade; import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK; import static com.android.systemui.classifier.Classifier.GENERIC; import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS; @@ -121,6 +122,7 @@ import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.DisplayId; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor; import com.android.systemui.doze.DozeLog; import com.android.systemui.dump.DumpManager; import com.android.systemui.dump.DumpsysTableLogger; @@ -131,7 +133,6 @@ import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewConfigurator; import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor; -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver; @@ -327,7 +328,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private final PulseExpansionHandler mPulseExpansionHandler; private final KeyguardBypassController mKeyguardBypassController; private final KeyguardUpdateMonitor mUpdateMonitor; - private final KeyguardFaceAuthInteractor mKeyguardFaceAuthInteractor; + private final DeviceEntryFaceAuthInteractor mDeviceEntryFaceAuthInteractor; private final ConversationNotificationManager mConversationNotificationManager; private final AuthController mAuthController; private final MediaHierarchyManager mMediaHierarchyManager; @@ -779,7 +780,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump ActiveNotificationsInteractor activeNotificationsInteractor, ShadeAnimationInteractor shadeAnimationInteractor, KeyguardViewConfigurator keyguardViewConfigurator, - KeyguardFaceAuthInteractor keyguardFaceAuthInteractor, + DeviceEntryFaceAuthInteractor deviceEntryFaceAuthInteractor, SplitShadeStateController splitShadeStateController, PowerInteractor powerInteractor, KeyguardClockPositionAlgorithm keyguardClockPositionAlgorithm, @@ -891,7 +892,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mShadeHeaderController = shadeHeaderController; mLayoutInflater = layoutInflater; mFeatureFlags = featureFlags; - mAnimateBack = mFeatureFlags.isEnabled(Flags.WM_SHADE_ANIMATE_BACK_GESTURE); + mAnimateBack = predictiveBackAnimateShade(); mTrackpadGestureFeaturesEnabled = mFeatureFlags.isEnabled(Flags.TRACKPAD_GESTURE_FEATURES); mFalsingCollector = falsingCollector; mWakeUpCoordinator = coordinator; @@ -936,7 +937,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mScreenOffAnimationController = screenOffAnimationController; mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController; mLastDownEvents = new NPVCDownEventState.Buffer(MAX_DOWN_EVENT_BUFFER_SIZE); - mKeyguardFaceAuthInteractor = keyguardFaceAuthInteractor; + mDeviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor; int currentMode = navigationModeController.addListener( mode -> mIsGestureNavigation = QuickStepContract.isGesturalMode(mode)); @@ -2478,6 +2479,13 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump return 0; } if (!mKeyguardBypassController.getBypassEnabled()) { + if (migrateClocksToBlueprint()) { + View nsslPlaceholder = mView.getRootView().findViewById(R.id.nssl_placeholder); + if (!mSplitShadeEnabled && nsslPlaceholder != null) { + return nsslPlaceholder.getTop(); + } + } + return mClockPositionResult.stackScrollerPadding; } int collapsedPosition = mHeadsUpInset; @@ -2971,9 +2979,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mShadeLog.v("onMiddleClicked on Keyguard, mDozingOnDown: false"); // Try triggering face auth, this "might" run. Check // KeyguardUpdateMonitor#shouldListenForFace to see when face auth won't run. - mKeyguardFaceAuthInteractor.onNotificationPanelClicked(); + mDeviceEntryFaceAuthInteractor.onNotificationPanelClicked(); - if (mKeyguardFaceAuthInteractor.canFaceAuthRun()) { + if (mDeviceEntryFaceAuthInteractor.canFaceAuthRun()) { mUpdateMonitor.requestActiveUnlock( ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT, "lockScreenEmptySpaceTap"); diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java index 8397caae438f..1dff99d53078 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java @@ -66,9 +66,9 @@ import com.android.systemui.DejankUtils; import com.android.systemui.Dumpable; import com.android.systemui.classifier.Classifier; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor; import com.android.systemui.dump.DumpManager; import com.android.systemui.fragments.FragmentHostManager; -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor; import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl; import com.android.systemui.media.controls.pipeline.MediaDataManager; import com.android.systemui.media.controls.ui.MediaHierarchyManager; @@ -152,7 +152,7 @@ public class QuickSettingsController implements Dumpable { private final RecordingController mRecordingController; private final LockscreenGestureLogger mLockscreenGestureLogger; private final ShadeLogger mShadeLog; - private final KeyguardFaceAuthInteractor mKeyguardFaceAuthInteractor; + private final DeviceEntryFaceAuthInteractor mDeviceEntryFaceAuthInteractor; private final CastController mCastController; private final SplitShadeStateController mSplitShadeStateController; private final InteractionJankMonitor mInteractionJankMonitor; @@ -338,7 +338,7 @@ public class QuickSettingsController implements Dumpable { InteractionJankMonitor interactionJankMonitor, ShadeLogger shadeLog, DumpManager dumpManager, - KeyguardFaceAuthInteractor keyguardFaceAuthInteractor, + DeviceEntryFaceAuthInteractor deviceEntryFaceAuthInteractor, ShadeRepository shadeRepository, ShadeInteractor shadeInteractor, ActiveNotificationsInteractor activeNotificationsInteractor, @@ -384,7 +384,7 @@ public class QuickSettingsController implements Dumpable { mLockscreenGestureLogger = lockscreenGestureLogger; mMetricsLogger = metricsLogger; mShadeLog = shadeLog; - mKeyguardFaceAuthInteractor = keyguardFaceAuthInteractor; + mDeviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor; mCastController = castController; mInteractionJankMonitor = interactionJankMonitor; mShadeRepository = shadeRepository; @@ -972,7 +972,7 @@ public class QuickSettingsController implements Dumpable { // When expanding QS, let's authenticate the user if possible, // this will speed up notification actions. if (height == 0 && !mKeyguardStateController.canDismissLockScreen()) { - mKeyguardFaceAuthInteractor.onQsExpansionStared(); + mDeviceEntryFaceAuthInteractor.onQsExpansionStared(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 08415cb5b0cb..d6d3e6791074 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -31,6 +31,7 @@ import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FINGERPR import static com.android.systemui.DejankUtils.whitelistIpcs; import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED; import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.IMPORTANT_MSG_MIN_DURATION; +import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_IS_DISMISSIBLE; import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_ALIGNMENT; import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_BATTERY; import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_BIOMETRIC_MESSAGE; @@ -178,6 +179,7 @@ public class KeyguardIndicationController { private KeyguardInteractor mKeyguardInteractor; private String mPersistentUnlockMessage; private String mAlignmentIndication; + private boolean mForceIsDismissible; private CharSequence mTrustGrantedIndication; private CharSequence mTransientIndication; private CharSequence mBiometricMessage; @@ -442,6 +444,7 @@ public class KeyguardIndicationController { // Update persistent messages. The following methods should only be called if we're on the // lock screen: + updateForceIsDimissibileChanged(); updateLockScreenDisclosureMsg(); updateLockScreenOwnerInfo(); updateLockScreenBatteryMsg(animate); @@ -458,6 +461,22 @@ public class KeyguardIndicationController { updateDeviceEntryIndication(false); } + private void updateForceIsDimissibileChanged() { + if (mForceIsDismissible) { + mRotateTextViewController.updateIndication( + INDICATION_IS_DISMISSIBLE, + new KeyguardIndication.Builder() + .setMessage(mContext.getResources().getString( + com.android.systemui.res.R.string.dismissible_keyguard_swipe) + ) + .setTextColor(mInitialTextColorState) + .build(), + /* updateImmediately */ true); + } else { + mRotateTextViewController.hideIndication(INDICATION_IS_DISMISSIBLE); + } + } + private void updateLockScreenDisclosureMsg() { if (mOrganizationOwnedDevice) { mBackgroundExecutor.execute(() -> { @@ -1311,6 +1330,12 @@ public class KeyguardIndicationController { } @Override + public void onForceIsDismissibleChanged(boolean forceIsDismissible) { + mForceIsDismissible = forceIsDismissible; + updateDeviceEntryIndication(false); + } + + @Override public void onTrustGrantedForCurrentUser( boolean dismissKeyguard, boolean newlyUnlocked, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index c9df317508f9..9b8dd0b75a24 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -275,7 +275,12 @@ public class NotificationLockscreenUserManagerImpl implements updateLockscreenNotificationSetting(); updatePublicMode(); - mPresenter.onUserSwitched(mCurrentUserId); + if (mPresenter != null) { + mPresenter.onUserSwitched(mCurrentUserId); + } else { + Log.w(TAG, "user switch before setup with presenter", + new Exception()); + } for (UserChangedListener listener : mListeners) { listener.onUserChanged(mCurrentUserId); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt index 4b896154c841..9fdd0bcc4ee9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt @@ -18,7 +18,7 @@ package com.android.systemui.statusbar.notification.shelf.domain.interactor import android.os.PowerManager import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository +import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.statusbar.LockscreenShadeTransitionController diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 0f640c9c2608..805b44cc0673 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -4455,9 +4455,13 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mSectionsManager.setHeaderForegroundColors(onSurface, onSurfaceVariant); - mFooterView.updateColors(); + if (mFooterView != null) { + mFooterView.updateColors(); + } - mEmptyShadeView.setTextColors(onSurface, onSurfaceVariant); + if (mEmptyShadeView != null) { + mEmptyShadeView.setTextColors(onSurface, onSurfaceVariant); + } } void goToFullShade(long delay) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 6f5058c8c52e..2e545126c634 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -366,8 +366,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { @Override public void onStatePostChange() { - mView.updateSensitiveness(mStatusBarStateController.goingToFullShade(), - mLockscreenUserManager.isAnyProfilePublicMode()); + updateSensitivenessWithAnimation(mStatusBarStateController.goingToFullShade()); mView.onStatePostChange(mStatusBarStateController.fromShadeLocked()); if (!FooterViewRefactor.isEnabled()) { updateImportantForAccessibility(); @@ -378,7 +377,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { private final UserChangedListener mLockscreenUserChangeListener = new UserChangedListener() { @Override public void onUserChanged(int userId) { - mView.updateSensitiveness(false, mLockscreenUserManager.isAnyProfilePublicMode()); + updateSensitivenessWithAnimation(false); mHistoryEnabled = null; updateFooter(); } @@ -388,7 +387,11 @@ public class NotificationStackScrollLayoutController implements Dumpable { * Recalculate sensitiveness without animation; called when waking up while keyguard occluded. */ public void updateSensitivenessForOccludedWakeup() { - mView.updateSensitiveness(false, mLockscreenUserManager.isAnyProfilePublicMode()); + updateSensitivenessWithAnimation(false); + } + + private void updateSensitivenessWithAnimation(boolean animate) { + mView.updateSensitiveness(animate, mLockscreenUserManager.isAnyProfilePublicMode()); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 57d49b250883..6e3aabf7c754 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -31,6 +31,7 @@ import static com.android.systemui.Flags.lightRevealMigration; import static com.android.systemui.charging.WirelessChargingAnimation.UNKNOWN_BATTERY_LEVEL; import static com.android.systemui.statusbar.NotificationLockscreenUserManager.PERMISSION_SELF; import static com.android.systemui.statusbar.StatusBarState.SHADE; +import static com.android.systemui.Flags.predictiveBackSysui; import android.annotation.Nullable; import android.app.ActivityOptions; @@ -836,7 +837,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mLightRevealScrim = lightRevealScrim; // Based on teamfood flag, turn predictive back dispatch on at runtime. - if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_SYSUI)) { + if (predictiveBackSysui()) { mContext.getApplicationInfo().setEnableOnBackInvokedCallback(true); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerModule.kt new file mode 100644 index 000000000000..fc3456ad6a23 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerModule.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone + +import com.android.systemui.CoreStartable +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +interface ConfigurationControllerModule { + + /** Starts [ConfigurationControllerStartable] */ + @Binds + @IntoMap + @ClassKey(ConfigurationControllerStartable::class) + fun bindConfigControllerStartable(impl: ConfigurationControllerStartable): CoreStartable +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java index 600d4afde935..45005cbc28a5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java @@ -55,11 +55,12 @@ import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; import com.android.systemui.util.Assert; +import dagger.Lazy; + import java.util.ArrayList; import javax.inject.Inject; -import dagger.Lazy; import kotlinx.coroutines.ExperimentalCoroutinesApi; /** @@ -175,6 +176,16 @@ public final class DozeServiceHost implements DozeHost { } } + /** + * Notify the registered callback about SPFS fingerprint acquisition started event. + */ + public void fireSideFpsAcquisitionStarted() { + Assert.isMainThread(); + for (int i = 0; i < mCallbacks.size(); i++) { + mCallbacks.get(i).onSideFingerprintAcquisitionStarted(); + } + } + void fireNotificationPulse(NotificationEntry entry) { Runnable pulseSuppressedListener = () -> { if (NotificationIconContainerRefactor.isEnabled()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt index 32b3ac2ad150..9f0863385ca1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt @@ -28,7 +28,7 @@ import com.android.systemui.CoreStartable import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.Assert @@ -43,13 +43,13 @@ import javax.inject.Inject */ @SysUISingleton class KeyguardLiftController @Inject constructor( - private val context: Context, - private val statusBarStateController: StatusBarStateController, - private val asyncSensorManager: AsyncSensorManager, - private val keyguardUpdateMonitor: KeyguardUpdateMonitor, - private val keyguardFaceAuthInteractor: KeyguardFaceAuthInteractor, - private val dumpManager: DumpManager, - private val selectedUserInteractor: SelectedUserInteractor, + private val context: Context, + private val statusBarStateController: StatusBarStateController, + private val asyncSensorManager: AsyncSensorManager, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor, + private val dumpManager: DumpManager, + private val selectedUserInteractor: SelectedUserInteractor, ) : Dumpable, CoreStartable { private val pickupSensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE) @@ -75,7 +75,7 @@ class KeyguardLiftController @Inject constructor( // Not listening anymore since trigger events unregister themselves isListening = false updateListeningState() - keyguardFaceAuthInteractor.onDeviceLifted() + deviceEntryFaceAuthInteractor.onDeviceLifted() keyguardUpdateMonitor.requestActiveUnlock( ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE, "KeyguardLiftController") @@ -113,7 +113,7 @@ class KeyguardLiftController @Inject constructor( val onKeyguard = keyguardUpdateMonitor.isKeyguardVisible && !statusBarStateController.isDozing - val isFaceEnabled = keyguardFaceAuthInteractor.isFaceAuthEnabledAndEnrolled() + val isFaceEnabled = deviceEntryFaceAuthInteractor.isFaceAuthEnabledAndEnrolled() val shouldListen = (onKeyguard || bouncerVisible) && isFaceEnabled if (shouldListen != isListening) { isListening = shouldListen diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index 4999123247a9..88347ab90606 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.phone; import static android.view.WindowInsets.Type.navigationBars; +import static com.android.systemui.Flags.predictiveBackAnimateBouncer; import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN; import static com.android.systemui.plugins.ActivityStarter.OnDismissAction; import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK; @@ -400,8 +401,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mFoldAodAnimationController = sysUIUnfoldComponent .map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null); mAlternateBouncerInteractor = alternateBouncerInteractor; - mIsBackAnimationEnabled = - featureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_BOUNCER_ANIM); + mIsBackAnimationEnabled = predictiveBackAnimateBouncer(); mUdfpsOverlayInteractor = udfpsOverlayInteractor; mActivityStarter = activityStarter; mKeyguardTransitionInteractor = keyguardTransitionInteractor; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java index af6da3fb6e51..3394eacddbd8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java @@ -149,6 +149,14 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh return create(new DialogDelegate<>(){}, mContext); } + /** Creates a new instance of {@link SystemUIDialog} with no customized behavior. + * + * When you just need a dialog created with a specific {@link Context}, call this. + */ + public SystemUIDialog create(Context context) { + return create(new DialogDelegate<>(){}, context); + } + /** * Creates a new instance of {@link SystemUIDialog} with {@code delegate} as the {@link * Delegate}. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java index b048da492eb1..942d186e7005 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java @@ -16,16 +16,12 @@ package com.android.systemui.statusbar.phone.dagger; -import com.android.systemui.CoreStartable; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.CentralSurfacesImpl; -import com.android.systemui.statusbar.phone.ConfigurationControllerStartable; import dagger.Binds; import dagger.Module; -import dagger.multibindings.ClassKey; -import dagger.multibindings.IntoMap; /** * Dagger Module providing {@link CentralSurfacesImpl}. @@ -38,12 +34,4 @@ public interface StatusBarPhoneModule { @Binds @SysUISingleton CentralSurfaces bindsCentralSurfaces(CentralSurfacesImpl impl); - - /** - * Starts {@link ConfigurationControllerStartable} - */ - @Binds - @IntoMap - @ClassKey(ConfigurationControllerStartable.class) - CoreStartable bindConfigControllerStartable(ConfigurationControllerStartable impl); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java index 0c5472f0ecfb..886010ccbf16 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java @@ -41,15 +41,14 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; -import com.android.systemui.log.core.LogLevel; import com.android.systemui.res.R; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; import dagger.Lazy; import java.io.PrintWriter; -import java.util.ArrayList; import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import javax.inject.Inject; @@ -63,7 +62,8 @@ public class KeyguardStateControllerImpl implements KeyguardStateController, Dum private static final boolean DEBUG_AUTH_WITH_ADB = false; private static final String AUTH_BROADCAST_KEY = "debug_trigger_auth"; - private final ArrayList<Callback> mCallbacks = new ArrayList<>(); + private final ConcurrentHashMap.KeySetView<Callback, Boolean> mCallbacks = + ConcurrentHashMap.<Callback>newKeySet(); private final Context mContext; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private final LockPatternUtils mLockPatternUtils; @@ -157,9 +157,7 @@ public class KeyguardStateControllerImpl implements KeyguardStateController, Dum @Override public void addCallback(@NonNull Callback callback) { Objects.requireNonNull(callback, "Callback must not be null. b/128895449"); - if (!mCallbacks.contains(callback)) { - mCallbacks.add(callback); - } + mCallbacks.add(callback); } @Override @@ -221,18 +219,7 @@ public class KeyguardStateControllerImpl implements KeyguardStateController, Dum } private void invokeForEachCallback(Consumer<Callback> consumer) { - // Copy the list to allow removal during callback. - ArrayList<Callback> copyOfCallbacks = new ArrayList<>(mCallbacks); - for (int i = 0; i < copyOfCallbacks.size(); i++) { - Callback callback = copyOfCallbacks.get(i); - // Temporary fix for b/315731775, callback is null even though only non-null callbacks - // are added to the list by addCallback - if (callback != null) { - consumer.accept(callback); - } else { - mLogger.log("KeyguardStateController callback is null", LogLevel.DEBUG); - } - } + mCallbacks.forEach(consumer); } private void notifyUnlockedChanged() { @@ -506,5 +493,10 @@ public class KeyguardStateControllerImpl implements KeyguardStateController, Dum public void onEnabledTrustAgentsChanged(int userId) { update(false /* updateAlways */); } + + @Override + public void onForceIsDismissibleChanged(boolean keepUnlockedOnFold) { + update(false /* updateAlways */); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java index 8087a8755a6e..550a65c01bfc 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java @@ -48,6 +48,8 @@ import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.util.leak.LeakDetector; +import dagger.Lazy; + import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -87,6 +89,7 @@ public class TunerServiceImpl extends TunerService { // Set of all tunables, used for leak detection. private final HashSet<Tunable> mTunables = LeakDetector.ENABLED ? new HashSet<>() : null; private final Context mContext; + private final Lazy<SystemUIDialog.Factory> mSystemUIDialogFactoryLazy; private final LeakDetector mLeakDetector; private final DemoModeController mDemoModeController; @@ -104,9 +107,11 @@ public class TunerServiceImpl extends TunerService { @Main Handler mainHandler, LeakDetector leakDetector, DemoModeController demoModeController, - UserTracker userTracker) { + UserTracker userTracker, + Lazy<SystemUIDialog.Factory> systemUIDialogFactoryLazy) { super(context); mContext = context; + mSystemUIDialogFactoryLazy = systemUIDialogFactoryLazy; mContentResolver = mContext.getContentResolver(); mLeakDetector = leakDetector; mDemoModeController = demoModeController; @@ -301,7 +306,7 @@ public class TunerServiceImpl extends TunerService { @Override public void showResetRequest(Runnable onDisabled) { - SystemUIDialog dialog = new SystemUIDialog(mContext); + SystemUIDialog dialog = mSystemUIDialogFactoryLazy.get().create(); dialog.setShowForAllUsers(true); dialog.setMessage(R.string.remove_from_settings_prompt); dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mContext.getString(R.string.cancel), diff --git a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyLogger.kt b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyLogger.kt new file mode 100644 index 000000000000..76f7609f81c7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyLogger.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2023 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.unfold + +import com.android.systemui.shared.system.SysUiStatsLog + +class DisplaySwitchLatencyLogger { + + /** + * Based on data present in [displaySwitchLatencyEvent], logs metrics for atom + * [DisplaySwitchLatencyTracked] + */ + fun log(displaySwitchLatencyEvent: DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent) { + with(displaySwitchLatencyEvent) { + SysUiStatsLog.write( + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED, + latencyMs, + fromFoldableDeviceState, + fromState, + fromFocusedAppUid, + fromPipAppUid, + fromVisibleAppsUid.toIntArray(), + fromDensityDpi, + toState, + toFoldableDeviceState, + toFocusedAppUid, + toPipAppUid, + toVisibleAppsUid.toIntArray(), + toDensityDpi, + notificationCount, + externalDisplayCount, + throttlingLevel, + vskinTemperatureC, + hallSensorToFirstHingeAngleChangeMs, + hallSensorToDeviceStateChangeMs, + onScreenTurningOnToOnDrawnMs, + onDrawnToOnScreenTurnedOnMs, + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt new file mode 100644 index 000000000000..92a64a618ba9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2023 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.unfold + +import android.content.Context +import android.util.Log +import com.android.app.tracing.TraceUtils.instantForTrack +import com.android.app.tracing.TraceUtils.traceAsync +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.display.data.repository.DeviceStateRepository +import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.power.shared.model.ScreenPowerState +import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.shared.system.SysUiStatsLog +import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg +import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor +import com.android.systemui.util.Compile +import com.android.systemui.util.Utils.isDeviceFoldable +import com.android.systemui.util.animation.data.repository.AnimationStatusRepository +import com.android.systemui.util.kotlin.pairwise +import com.android.systemui.util.time.SystemClock +import com.android.systemui.util.time.measureTimeMillis +import java.util.concurrent.Executor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch + +/** + * [DisplaySwitchLatencyTracker] tracks latency and related fields for display switch of a foldable + * device. This class populates [DisplaySwitchLatencyEvent] while an ongoing display switch event + */ +@SysUISingleton +class DisplaySwitchLatencyTracker +@Inject +constructor( + private val context: Context, + private val deviceStateRepository: DeviceStateRepository, + private val powerInteractor: PowerInteractor, + private val unfoldTransitionInteractor: UnfoldTransitionInteractor, + private val animationStatusRepository: AnimationStatusRepository, + private val keyguardInteractor: KeyguardInteractor, + @UnfoldSingleThreadBg private val singleThreadBgExecutor: Executor, + @Application private val applicationScope: CoroutineScope, + private val displaySwitchLatencyLogger: DisplaySwitchLatencyLogger, + private val systemClock: SystemClock +) : CoreStartable { + + private val backgroundDispatcher = singleThreadBgExecutor.asCoroutineDispatcher() + + @OptIn(ExperimentalCoroutinesApi::class) + override fun start() { + if (!isDeviceFoldable(context)) { + return + } + applicationScope.launch(backgroundDispatcher) { + deviceStateRepository.state + .pairwise() + .filter { + // Start tracking only when the foldable device is + //folding(UNFOLDED/HALF_FOLDED -> FOLDED) or + //unfolding(FOLDED -> HALF_FOLD/UNFOLDED) + foldableDeviceState -> + foldableDeviceState.previousValue == DeviceState.FOLDED || + foldableDeviceState.newValue == DeviceState.FOLDED + } + .flatMapLatest { foldableDeviceState -> + flow { + var displaySwitchLatencyEvent = DisplaySwitchLatencyEvent() + val toFoldableDeviceState = foldableDeviceState.newValue.toStatsInt() + displaySwitchLatencyEvent = + displaySwitchLatencyEvent.withBeforeFields( + foldableDeviceState.previousValue.toStatsInt() + ) + + val displaySwitchTimeMs = + measureTimeMillis(systemClock) { + traceAsync(TAG, "displaySwitch") { + waitForDisplaySwitch(toFoldableDeviceState) + } + } + + displaySwitchLatencyEvent = + displaySwitchLatencyEvent.withAfterFields( + toFoldableDeviceState, + displaySwitchTimeMs.toInt(), + getCurrentState() + ) + emit(displaySwitchLatencyEvent) + } + } + .collect { displaySwitchLatencyLogger.log(it) } + } + } + + private fun DeviceState.toStatsInt(): Int = + when (this) { + DeviceState.FOLDED -> FOLDABLE_DEVICE_STATE_CLOSED + DeviceState.HALF_FOLDED -> FOLDABLE_DEVICE_STATE_HALF_OPEN + DeviceState.UNFOLDED -> FOLDABLE_DEVICE_STATE_OPEN + DeviceState.CONCURRENT_DISPLAY -> FOLDABLE_DEVICE_STATE_FLIPPED + else -> FOLDABLE_DEVICE_STATE_UNKNOWN + } + + private suspend fun waitForDisplaySwitch(toFoldableDeviceState: Int) { + val isTransitionEnabled = + unfoldTransitionInteractor.isAvailable && + animationStatusRepository.areAnimationsEnabled().first() + if (shouldWaitForScreenOn(toFoldableDeviceState, isTransitionEnabled)) { + waitForScreenTurnedOn() + } else { + traceAsync(TAG, "waitForTransitionStart()") { + unfoldTransitionInteractor.waitForTransitionStart() + } + } + } + + private fun shouldWaitForScreenOn( + toFoldableDeviceState: Int, + isTransitionEnabled: Boolean + ): Boolean = (toFoldableDeviceState == FOLDABLE_DEVICE_STATE_CLOSED || !isTransitionEnabled) + + private suspend fun waitForScreenTurnedOn() { + traceAsync(TAG, "waitForScreenTurnedOn()") { + powerInteractor.screenPowerState.filter { it == ScreenPowerState.SCREEN_ON }.first() + } + } + + private fun getCurrentState(): Int = + when { + isStateAod() -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__AOD + else -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__UNKNOWN + } + + private fun isStateAod(): Boolean { + val lastWakefulnessEvent = powerInteractor.detailedWakefulness.value + val isAodEnabled = keyguardInteractor.isAodAvailable.value + + return (lastWakefulnessEvent.isAsleep() && + (lastWakefulnessEvent.lastSleepReason == WakeSleepReason.FOLD) && + isAodEnabled) + } + + private inline fun log(msg: () -> String) { + if (DEBUG) Log.d(TAG, msg()) + } + + private fun DisplaySwitchLatencyEvent.withBeforeFields( + fromFoldableDeviceState: Int + ): DisplaySwitchLatencyEvent { + log { "fromFoldableDeviceState=$fromFoldableDeviceState" } + instantForTrack(TAG, "fromFoldableDeviceState=$fromFoldableDeviceState") + + return copy(fromFoldableDeviceState = fromFoldableDeviceState) + } + + private fun DisplaySwitchLatencyEvent.withAfterFields( + toFoldableDeviceState: Int, + displaySwitchTimeMs: Int, + toState: Int + ): DisplaySwitchLatencyEvent { + log { + "toFoldableDeviceState=$toFoldableDeviceState, " + + "toState=$toState, " + + "latencyMs=$displaySwitchTimeMs" + } + instantForTrack(TAG, "toFoldableDeviceState=$toFoldableDeviceState, toState=$toState") + + return copy( + toFoldableDeviceState = toFoldableDeviceState, + latencyMs = displaySwitchTimeMs, + toState = toState + ) + } + + /** + * Stores values corresponding to all respective [DisplaySwitchLatencyTrackedField] in a single + * event of display switch for foldable devices. + * + * Once the data is captured in this data class and appropriate to log, it is logged through + * [DisplaySwitchLatencyLogger] + */ + data class DisplaySwitchLatencyEvent( + val latencyMs: Int = VALUE_UNKNOWN, + val fromFoldableDeviceState: Int = FOLDABLE_DEVICE_STATE_UNKNOWN, + val fromState: Int = SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_STATE__UNKNOWN, + val fromFocusedAppUid: Int = VALUE_UNKNOWN, + val fromPipAppUid: Int = VALUE_UNKNOWN, + val fromVisibleAppsUid: Set<Int> = setOf(), + val fromDensityDpi: Int = VALUE_UNKNOWN, + val toFoldableDeviceState: Int = FOLDABLE_DEVICE_STATE_UNKNOWN, + val toState: Int = SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_STATE__UNKNOWN, + val toFocusedAppUid: Int = VALUE_UNKNOWN, + val toPipAppUid: Int = VALUE_UNKNOWN, + val toVisibleAppsUid: Set<Int> = setOf(), + val toDensityDpi: Int = VALUE_UNKNOWN, + val notificationCount: Int = VALUE_UNKNOWN, + val externalDisplayCount: Int = VALUE_UNKNOWN, + val throttlingLevel: Int = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__THROTTLING_LEVEL__NONE, + val vskinTemperatureC: Int = VALUE_UNKNOWN, + val hallSensorToFirstHingeAngleChangeMs: Int = VALUE_UNKNOWN, + val hallSensorToDeviceStateChangeMs: Int = VALUE_UNKNOWN, + val onScreenTurningOnToOnDrawnMs: Int = VALUE_UNKNOWN, + val onDrawnToOnScreenTurnedOnMs: Int = VALUE_UNKNOWN + ) + + companion object { + private const val VALUE_UNKNOWN = -1 + private const val TAG = "DisplaySwitchLatency" + private val DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE) + + private const val FOLDABLE_DEVICE_STATE_UNKNOWN = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_UNKNOWN + const val FOLDABLE_DEVICE_STATE_CLOSED = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_CLOSED + const val FOLDABLE_DEVICE_STATE_HALF_OPEN = + SysUiStatsLog + .DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_HALF_OPENED + private const val FOLDABLE_DEVICE_STATE_OPEN = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_OPENED + private const val FOLDABLE_DEVICE_STATE_FLIPPED = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_FLIPPED + } +} diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTraceLogger.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTraceLogger.kt index 94912bf82377..adf50a1e661b 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTraceLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTraceLogger.kt @@ -22,8 +22,8 @@ import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.unfold.data.repository.FoldStateRepository import com.android.systemui.unfold.system.DeviceStateRepository -import com.android.systemui.unfold.updates.FoldStateRepository import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt index 50515daedc51..8bef53c8c4fb 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt @@ -27,6 +27,8 @@ import com.android.systemui.keyguard.LifecycleScreenStatusProvider import com.android.systemui.unfold.config.UnfoldTransitionConfig import com.android.systemui.unfold.dagger.UnfoldBgProgressFlag import com.android.systemui.unfold.dagger.UnfoldMain +import com.android.systemui.unfold.data.repository.FoldStateRepository +import com.android.systemui.unfold.data.repository.FoldStateRepositoryImpl import com.android.systemui.unfold.data.repository.UnfoldTransitionRepository import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor @@ -168,6 +170,11 @@ class UnfoldTransitionModule { @Provides fun screenStatusProvider(impl: LifecycleScreenStatusProvider): ScreenStatusProvider = impl + @Provides + @Singleton + fun provideDisplaySwitchLatencyLogger(): DisplaySwitchLatencyLogger = + DisplaySwitchLatencyLogger() + @Module interface Bindings { @Binds @@ -178,6 +185,8 @@ class UnfoldTransitionModule { @Binds fun bindRepository(impl: UnfoldTransitionRepositoryImpl): UnfoldTransitionRepository @Binds fun bindInteractor(impl: UnfoldTransitionInteractorImpl): UnfoldTransitionInteractor + + @Binds fun bindFoldStateRepository(impl: FoldStateRepositoryImpl): FoldStateRepository } } diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/FoldStateRepository.kt b/packages/SystemUI/src/com/android/systemui/unfold/data/repository/FoldStateRepository.kt index 61b0b40a55bf..04b00ca58c47 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/FoldStateRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/data/repository/FoldStateRepository.kt @@ -13,9 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.unfold.updates +package com.android.systemui.unfold.data.repository -import com.android.systemui.unfold.updates.FoldStateRepository.FoldUpdate +import com.android.systemui.unfold.data.repository.FoldStateRepository.FoldUpdate +import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED +import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN +import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_HALF_OPEN +import com.android.systemui.unfold.updates.FOLD_UPDATE_START_CLOSING +import com.android.systemui.unfold.updates.FOLD_UPDATE_START_OPENING +import com.android.systemui.unfold.updates.FoldStateProvider import javax.inject.Inject import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.awaitClose @@ -50,7 +56,7 @@ interface FoldStateRepository { FOLD_UPDATE_FINISH_HALF_OPEN -> FINISH_HALF_OPEN FOLD_UPDATE_FINISH_FULL_OPEN -> FINISH_FULL_OPEN FOLD_UPDATE_FINISH_CLOSED -> FINISH_CLOSED - else -> error("FoldUpdateNotFound") + else -> error("Fold update with id $oldId is not supported") } } } diff --git a/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt index a2e77afedea6..3e2e564c307c 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt @@ -15,16 +15,26 @@ */ package com.android.systemui.unfold.domain.interactor -import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionFinished import com.android.systemui.unfold.data.repository.UnfoldTransitionRepository +import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionFinished +import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted import javax.inject.Inject import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first +/** + * Contains business-logic related to fold-unfold transitions while interacting with + * [UnfoldTransitionRepository] + */ interface UnfoldTransitionInteractor { + /** Returns availability of fold/unfold transitions on the device */ val isAvailable: Boolean + /** Suspends and waits for a fold/unfold transition to finish */ suspend fun waitForTransitionFinish() + + /** Suspends and waits for a fold/unfold transition to start */ + suspend fun waitForTransitionStart() } class UnfoldTransitionInteractorImpl @@ -37,4 +47,8 @@ constructor(private val repository: UnfoldTransitionRepository) : UnfoldTransiti override suspend fun waitForTransitionFinish() { repository.transitionStatus.filter { it is TransitionFinished }.first() } + + override suspend fun waitForTransitionStart() { + repository.transitionStatus.filter { it is TransitionStarted }.first() + } } diff --git a/packages/SystemUI/src/com/android/systemui/util/Utils.java b/packages/SystemUI/src/com/android/systemui/util/Utils.java index fa6d0552c9e9..7861ded6cd24 100644 --- a/packages/SystemUI/src/com/android/systemui/util/Utils.java +++ b/packages/SystemUI/src/com/android/systemui/util/Utils.java @@ -82,6 +82,14 @@ public class Utils { } /** + * Returns {@code true} if the device is a foldable device + */ + public static boolean isDeviceFoldable(Context context) { + return context.getResources() + .getIntArray(com.android.internal.R.array.config_foldedDeviceStates).length != 0; + } + + /** * Allow the media player to be shown in the QS area, controlled by 2 flags. * On by default, but can be disabled by setting either flag to 0/false. */ diff --git a/packages/SystemUI/src/com/android/systemui/util/time/MeasureTimeUtil.kt b/packages/SystemUI/src/com/android/systemui/util/time/MeasureTimeUtil.kt new file mode 100644 index 000000000000..f13196857167 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/time/MeasureTimeUtil.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 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.util.time + +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Executes the given [block] and returns elapsed time using provided [systemClock] in milliseconds. + */ +@OptIn(ExperimentalContracts::class) +inline fun measureTimeMillis(systemClock: SystemClock, block: () -> Unit): Long { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + val start = systemClock.currentTimeMillis() + block() + return systemClock.currentTimeMillis() - start +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java index 8d06a8f3afd8..497c4cb070f0 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java @@ -38,6 +38,8 @@ import com.android.systemui.volume.VolumeDialogComponent; import com.android.systemui.volume.VolumeDialogImpl; import com.android.systemui.volume.VolumePanelFactory; import com.android.systemui.volume.VolumeUI; +import com.android.systemui.volume.panel.dagger.VolumePanelComponent; +import com.android.systemui.volume.panel.dagger.factory.VolumePanelComponentFactory; import dagger.Binds; import dagger.Lazy; @@ -48,9 +50,13 @@ import dagger.multibindings.IntoMap; import dagger.multibindings.IntoSet; /** Dagger Module for code in the volume package. */ -@Module +@Module( + subcomponents = { + VolumePanelComponent.class + } +) public interface VolumeModule { - /** Starts VolumeUI. */ + /** Starts VolumeUI. */ @Binds @IntoMap @ClassKey(VolumeUI.class) @@ -61,11 +67,15 @@ public interface VolumeModule { @IntoSet ConfigurationController.ConfigurationListener bindVolumeUIConfigChanges(VolumeUI impl); - /** */ + /** */ @Binds VolumeComponent provideVolumeComponent(VolumeDialogComponent volumeDialogComponent); - /** */ + /** */ + @Binds + VolumePanelComponentFactory bindVolumePanelComponentFactory(VolumePanelComponent.Factory impl); + + /** */ @Provides static VolumeDialog provideVolumeDialog( Context context, diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt new file mode 100644 index 000000000000..22a74d27ba1f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2023 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.volume.panel + +/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */ +typealias VolumePanelComponentKey = String diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/CoroutineModule.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/CoroutineModule.kt new file mode 100644 index 000000000000..3ce0bacaed1a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/CoroutineModule.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 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.volume.panel.dagger + +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import dagger.Module +import dagger.Provides +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob + +/** Provides Volume Panel coroutine tools. */ +@Module +interface CoroutineModule { + + companion object { + + /** + * Provides a coroutine scope to use inside [VolumePanelScope]. + * [com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel] manages the + * lifecycle of this scope. It's cancelled when the View Model is destroyed. This helps to + * free occupied resources when volume panel is not shown. + */ + @VolumePanelScope + @Provides + fun provideCoroutineScope(@Application applicationScope: CoroutineScope): CoroutineScope = + CoroutineScope(applicationScope.coroutineContext + SupervisorJob()) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/DefaultMultibindsModule.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/DefaultMultibindsModule.kt new file mode 100644 index 000000000000..3660ac166b12 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/DefaultMultibindsModule.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 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.volume.panel.dagger + +import com.android.systemui.volume.panel.VolumePanelComponentKey +import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria +import dagger.Module +import dagger.multibindings.Multibinds + +/** + * Provides empty multibinding maps for [ComponentAvailabilityCriteria] and [VolumePanelComponent] + */ +@Module +interface DefaultMultibindsModule { + + @Multibinds fun criteriaMap(): Map<VolumePanelComponentKey, ComponentAvailabilityCriteria> +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/VolumePanelComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/VolumePanelComponent.kt new file mode 100644 index 000000000000..0a057eb5c927 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/VolumePanelComponent.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 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.volume.panel.dagger + +import com.android.systemui.volume.panel.dagger.factory.VolumePanelComponentFactory +import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import com.android.systemui.volume.panel.domain.DomainModule +import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractor +import com.android.systemui.volume.panel.ui.UiModule +import com.android.systemui.volume.panel.ui.viewmodel.ComponentsLayoutManager +import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel +import dagger.BindsInstance +import dagger.Subcomponent +import kotlinx.coroutines.CoroutineScope + +/** + * Core Volume Panel dagger component. It's managed by [VolumePanelViewModel] and lives alongside + * it. + */ +@VolumePanelScope +@Subcomponent( + modules = + [ + // Volume Panel infra modules + CoroutineModule::class, + DefaultMultibindsModule::class, + DomainModule::class, + UiModule::class, + // Components modules + ] +) +interface VolumePanelComponent { + + fun coroutineScope(): CoroutineScope + + fun componentsInteractor(): ComponentsInteractor + + fun componentsLayoutManager(): ComponentsLayoutManager + + @Subcomponent.Factory + interface Factory : VolumePanelComponentFactory { + + override fun create(@BindsInstance viewModel: VolumePanelViewModel): VolumePanelComponent + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/factory/VolumePanelComponentFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/factory/VolumePanelComponentFactory.kt new file mode 100644 index 000000000000..e470c3f25145 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/factory/VolumePanelComponentFactory.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.volume.panel.dagger.factory + +import com.android.systemui.volume.panel.dagger.VolumePanelComponent +import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel +import dagger.BindsInstance + +/** + * Common interface for all [dagger.Subcomponent.Factory] providing [VolumePanelComponent]. + * [VolumePanelViewModel] uses it to create a new instance of the class. + */ +interface VolumePanelComponentFactory { + + fun create(@BindsInstance viewModel: VolumePanelViewModel): VolumePanelComponent +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundScope.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/scope/VolumePanelScope.kt index 4a4ba2bf7ef9..e597d11067b4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundScope.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/scope/VolumePanelScope.kt @@ -14,16 +14,12 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.impl.custom.di.bound +package com.android.systemui.volume.panel.dagger.scope import javax.inject.Scope /** - * Scope annotation for bound custom tile scope. This scope lives when a particular - * [com.android.systemui.qs.external.CustomTile] is listening and bound to the - * [android.service.quicksettings.TileService]. + * Volume Panel dependency injection scope. This scope is created alongside Volume Panel and + * destroyed when it's lo longer present. */ -@MustBeDocumented -@Retention(AnnotationRetention.RUNTIME) -@Scope -annotation class CustomTileBoundScope +@MustBeDocumented @Retention(AnnotationRetention.RUNTIME) @Scope annotation class VolumePanelScope diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/ComponentAvailabilityCriteria.kt index f34704be8bc5..d9702e433a79 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileUserActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/ComponentAvailabilityCriteria.kt @@ -14,19 +14,24 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.impl.custom +package com.android.systemui.volume.panel.domain -import com.android.systemui.qs.tiles.base.interactor.QSTileInput -import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor -import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel -import com.android.systemui.qs.tiles.impl.di.QSTileScope +import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf -@QSTileScope -class CustomTileUserActionInteractor @Inject constructor() : - QSTileUserActionInteractor<CustomTileDataModel> { +interface ComponentAvailabilityCriteria { - override suspend fun handleInput(input: QSTileInput<CustomTileDataModel>) { - TODO("Not yet implemented") - } + /** + * Checks if the controller is currently available. Can be used to filter out unwanted + * components. For example, hide components for the hardware that is temporarily unavailable. + */ + fun isAvailable(): Flow<Boolean> +} + +@VolumePanelScope +class AlwaysAvailableCriteria @Inject constructor() : ComponentAvailabilityCriteria { + + override fun isAvailable(): Flow<Boolean> = flowOf(true) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/DomainModule.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/DomainModule.kt new file mode 100644 index 000000000000..7817630d09d2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/DomainModule.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2023 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.volume.panel.domain + +import com.android.systemui.volume.panel.VolumePanelComponentKey +import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractor +import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractorImpl +import dagger.Binds +import dagger.Module +import dagger.Provides + +/** Domain layer bindings module. */ +@Module +interface DomainModule { + + @Binds fun bindComponentsInteractor(impl: ComponentsInteractorImpl): ComponentsInteractor + + @Binds + fun bindDefaultComponentAvailabilityCriteria( + impl: AlwaysAvailableCriteria + ): ComponentAvailabilityCriteria + + companion object { + + /** + * Enabled components collection. These are the components processed by Volume Panel logic + * and possibly shown in the UI. + * + * There should be a binding in [VolumePanelScope] for [ComponentAvailabilityCriteria] and + * [com.android.systemui.volume.panel.ui.VolumePanelComponent] for each component from this + * collection. + */ + @Provides + @VolumePanelScope + fun provideEnabledComponents(): Collection<VolumePanelComponentKey> = setOf() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractor.kt new file mode 100644 index 000000000000..e5b52ea5eb88 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractor.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2023 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.volume.panel.domain.interactor + +import com.android.systemui.volume.panel.VolumePanelComponentKey +import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria +import com.android.systemui.volume.panel.domain.model.ComponentModel +import javax.inject.Inject +import javax.inject.Provider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.shareIn + +interface ComponentsInteractor { + + /** + * Components collection for the UI layer. Uses [ComponentAvailabilityCriteria] to dynamically + * determine each component availability. + */ + val components: Flow<Collection<ComponentModel>> +} + +@VolumePanelScope +class ComponentsInteractorImpl +@Inject +constructor( + enabledComponents: Collection<VolumePanelComponentKey>, + defaultCriteria: Provider<ComponentAvailabilityCriteria>, + @VolumePanelScope coroutineScope: CoroutineScope, + private val criteriaByKey: + Map< + VolumePanelComponentKey, + @JvmSuppressWildcards + Provider<@JvmSuppressWildcards ComponentAvailabilityCriteria> + >, +) : ComponentsInteractor { + + override val components: Flow<Collection<ComponentModel>> = + combine( + enabledComponents.map { componentKey -> + val componentCriteria = (criteriaByKey[componentKey] ?: defaultCriteria).get() + componentCriteria.isAvailable().map { isAvailable -> + ComponentModel(componentKey, isAvailable = isAvailable) + } + } + ) { + it.asList() + } + .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1) +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/model/ComponentModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/model/ComponentModel.kt new file mode 100644 index 000000000000..9765713f0a50 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/model/ComponentModel.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 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.volume.panel.domain.model + +import com.android.systemui.volume.panel.VolumePanelComponentKey + +/** + * Represents a current state of the Volume Panel component. + * + * @property key identifies the component the entity represents. + * @property isAvailable is true when the component is supported by the device. + */ +data class ComponentModel( + val key: VolumePanelComponentKey, + val isAvailable: Boolean, +) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundModule.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/UiModule.kt index 889424a8e8d3..bfa7ef2e42c2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/UiModule.kt @@ -14,18 +14,16 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.impl.custom.di.bound +package com.android.systemui.volume.panel.ui -import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTilePackageUpdatesRepository -import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTilePackageUpdatesRepositoryImpl +import com.android.systemui.volume.panel.ui.viewmodel.ComponentsLayoutManager +import com.android.systemui.volume.panel.ui.viewmodel.DefaultComponentsLayoutManager import dagger.Binds import dagger.Module +/** UI layer bindings module. */ @Module -interface CustomTileBoundModule { +interface UiModule { - @Binds - fun bindCustomTilePackageUpdatesRepository( - impl: CustomTilePackageUpdatesRepositoryImpl - ): CustomTilePackageUpdatesRepository + @Binds fun bindSorter(impl: DefaultComponentsLayoutManager): ComponentsLayoutManager } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/ComponentState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/ComponentState.kt new file mode 100644 index 000000000000..0a226e28da19 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/ComponentState.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 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.volume.panel.ui.model + +import com.android.systemui.volume.panel.VolumePanelComponentKey + +/** + * State of the [VolumePanelComponent]. + * + * @property key uniquely identifies this component + * @property component is an inflated component obtained be the View Model + * @property isVisible determines component visibility in the UI + */ +data class ComponentState( + val key: VolumePanelComponentKey, + val isVisible: Boolean, +) diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/ComponentsLayout.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/ComponentsLayout.kt new file mode 100644 index 000000000000..5690ac31718f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/ComponentsLayout.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 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.volume.panel.ui.model + +/** Represents components grouping into the layout. */ +data class ComponentsLayout( + val contentComponents: List<ComponentState>, + val bottomBarComponent: ComponentState, +) diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/VolumePanelState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/VolumePanelState.kt new file mode 100644 index 000000000000..399342f749a6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/VolumePanelState.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 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.volume.panel.ui.model + +import android.content.res.Configuration +import android.content.res.Configuration.Orientation + +/** + * State of the Volume Panel itself. + * + * @property orientation is current Volume Panel orientation. + */ +data class VolumePanelState( + @Orientation val orientation: Int, + val isVisible: Boolean, +) { + init { + require( + orientation == Configuration.ORIENTATION_PORTRAIT || + orientation == Configuration.ORIENTATION_LANDSCAPE || + orientation == Configuration.ORIENTATION_UNDEFINED || + orientation == Configuration.ORIENTATION_SQUARE + ) { + "Unknown orientation: $orientation" + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentsLayoutManager.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentsLayoutManager.kt new file mode 100644 index 000000000000..f45401a0dd0a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentsLayoutManager.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 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.volume.panel.ui.viewmodel + +import com.android.systemui.volume.panel.ui.model.ComponentState +import com.android.systemui.volume.panel.ui.model.ComponentsLayout +import com.android.systemui.volume.panel.ui.model.VolumePanelState + +/** + * Lays out components to [ComponentsLayout], that UI uses to render the Volume Panel. + * + * Vertical layout shows the list from top to bottom: + * ``` + * ----- + * | 1 | + * | 2 | + * | 3 | + * | 4 | + * ----- + * ``` + * + * Horizontal layout shows the list in a grid from, filling the columns first: + * ``` + * ---------- + * | 1 || 3 | + * | 2 || 4 | + * ---------- + * ``` + */ +interface ComponentsLayoutManager { + + fun layout( + volumePanelState: VolumePanelState, + components: Collection<ComponentState>, + ): ComponentsLayout +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/DefaultComponentsLayoutManager.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/DefaultComponentsLayoutManager.kt new file mode 100644 index 000000000000..cedfaf36d300 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/DefaultComponentsLayoutManager.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 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.volume.panel.ui.viewmodel + +import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import com.android.systemui.volume.panel.ui.model.ComponentState +import com.android.systemui.volume.panel.ui.model.ComponentsLayout +import com.android.systemui.volume.panel.ui.model.VolumePanelState +import javax.inject.Inject + +/** + * Default [ComponentsLayoutManager]. It places [VolumePanelComponents.BOTTOM_BAR] to + * [ComponentsLayout.bottomBarComponent] and everything else to + * [ComponentsLayout.contentComponents]. + */ +@VolumePanelScope +class DefaultComponentsLayoutManager @Inject constructor() : ComponentsLayoutManager { + + override fun layout( + volumePanelState: VolumePanelState, + components: Collection<ComponentState> + ): ComponentsLayout = TODO("Unimplemented yet") +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt new file mode 100644 index 000000000000..dda361abb378 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2023 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.volume.panel.ui.viewmodel + +import android.content.Context +import android.content.res.Resources +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.statusbar.policy.onConfigChanged +import com.android.systemui.volume.panel.dagger.VolumePanelComponent +import com.android.systemui.volume.panel.dagger.factory.VolumePanelComponentFactory +import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractor +import com.android.systemui.volume.panel.ui.model.ComponentState +import com.android.systemui.volume.panel.ui.model.ComponentsLayout +import com.android.systemui.volume.panel.ui.model.VolumePanelState +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +class VolumePanelViewModel( + resources: Resources, + daggerComponentFactory: VolumePanelComponentFactory, + configurationController: ConfigurationController, +) : ViewModel() { + + private val volumePanelComponent: VolumePanelComponent = daggerComponentFactory.create(this) + + private val scope: CoroutineScope + get() = volumePanelComponent.coroutineScope() + + private val componentsInteractor: ComponentsInteractor + get() = volumePanelComponent.componentsInteractor() + + private val componentsLayoutManager: ComponentsLayoutManager + get() = volumePanelComponent.componentsLayoutManager() + + private val mutablePanelVisibility = MutableStateFlow(true) + + val volumePanelState: StateFlow<VolumePanelState> = + combine( + configurationController.onConfigChanged.distinctUntilChanged(), + mutablePanelVisibility, + ) { configuration, isVisible -> + VolumePanelState(orientation = configuration.orientation, isVisible = isVisible) + } + .stateIn( + volumePanelComponent.coroutineScope(), + SharingStarted.Eagerly, + VolumePanelState( + orientation = resources.configuration.orientation, + isVisible = mutablePanelVisibility.value, + ), + ) + val mComponentsLayout: Flow<ComponentsLayout> = + combine( + componentsInteractor.components, + volumePanelState, + ) { components, scope -> + val componentStates = + components.map { model -> + ComponentState( + model.key, + model.isAvailable, + ) + } + componentsLayoutManager.layout(scope, componentStates) + } + .shareIn( + volumePanelComponent.coroutineScope(), + SharingStarted.Eagerly, + replay = 1, + ) + + fun dismissPanel() { + scope.launch { mutablePanelVisibility.emit(false) } + } + + override fun onCleared() { + scope.cancel() + super.onCleared() + } + + class Factory + @Inject + constructor( + @Application private val context: Context, + private val daggerComponentFactory: VolumePanelComponentFactory, + private val configurationController: ConfigurationController, + ) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun <T : ViewModel> create(modelClass: Class<T>): T { + check(modelClass == VolumePanelViewModel::class.java) + return VolumePanelViewModel( + context.resources, + daggerComponentFactory, + configurationController, + ) + as T + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletActivity.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletActivity.java index 5558aa72bb57..111492c3e227 100644 --- a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletActivity.java +++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletActivity.java @@ -43,7 +43,7 @@ import com.android.settingslib.Utils; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor; +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.res.R; @@ -69,7 +69,7 @@ public class WalletActivity extends ComponentActivity implements private final Executor mExecutor; private final Handler mHandler; private final FalsingManager mFalsingManager; - private final KeyguardFaceAuthInteractor mKeyguardFaceAuthInteractor; + private final DeviceEntryFaceAuthInteractor mDeviceEntryFaceAuthInteractor; private FalsingCollector mFalsingCollector; private final UserTracker mUserTracker; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; @@ -94,7 +94,7 @@ public class WalletActivity extends ComponentActivity implements KeyguardUpdateMonitor keyguardUpdateMonitor, StatusBarKeyguardViewManager keyguardViewManager, UiEventLogger uiEventLogger, - KeyguardFaceAuthInteractor keyguardFaceAuthInteractor) { + DeviceEntryFaceAuthInteractor deviceEntryFaceAuthInteractor) { mKeyguardStateController = keyguardStateController; mKeyguardDismissUtil = keyguardDismissUtil; mActivityStarter = activityStarter; @@ -106,7 +106,7 @@ public class WalletActivity extends ComponentActivity implements mKeyguardUpdateMonitor = keyguardUpdateMonitor; mKeyguardViewManager = keyguardViewManager; mUiEventLogger = uiEventLogger; - mKeyguardFaceAuthInteractor = keyguardFaceAuthInteractor; + mDeviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor; } @Override @@ -213,7 +213,7 @@ public class WalletActivity extends ComponentActivity implements true, Utils.getColorAttrDefaultColor( this, com.android.internal.R.attr.colorAccentPrimary)); - mKeyguardFaceAuthInteractor.onWalletLaunched(); + mDeviceEntryFaceAuthInteractor.onWalletLaunched(); } @Override diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index d03a898cf6d1..d8eb05a43e40 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -30,6 +30,7 @@ import static android.telephony.SubscriptionManager.NAME_SOURCE_CARRIER_ID; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN; import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_STATE_CANCELLING_RESTARTING; +import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_STATE_STOPPED; import static com.android.keyguard.KeyguardUpdateMonitor.DEFAULT_CANCEL_SIGNAL_TIMEOUT; import static com.android.keyguard.KeyguardUpdateMonitor.HAL_POWER_PRESS_TIMEOUT; import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED; @@ -109,6 +110,7 @@ import android.testing.TestableLooper; import android.text.TextUtils; import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.internal.foldables.FoldGracePeriodProvider; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.InstanceId; import com.android.internal.logging.UiEventLogger; @@ -119,16 +121,19 @@ import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardUpdateMonitor.BiometricAuthenticated; import com.android.keyguard.logging.KeyguardUpdateMonitorLogger; import com.android.settingslib.fuelgauge.BatteryStatus; +import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider; import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.deviceentry.data.repository.FaceWakeUpTriggersConfig; +import com.android.systemui.deviceentry.data.repository.FaceWakeUpTriggersConfigImpl; +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor; +import com.android.systemui.deviceentry.domain.interactor.FaceAuthenticationListener; +import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus; +import com.android.systemui.deviceentry.shared.model.FaceDetectionStatus; +import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus; import com.android.systemui.dump.DumpManager; -import com.android.systemui.keyguard.domain.interactor.FaceAuthenticationListener; -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor; -import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus; -import com.android.systemui.keyguard.shared.model.FaceDetectionStatus; -import com.android.systemui.keyguard.shared.model.FailedFaceAuthenticationStatus; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.settings.UserTracker; @@ -218,6 +223,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Mock private BroadcastDispatcher mBroadcastDispatcher; @Mock + private FoldGracePeriodProvider mFoldGracePeriodProvider; + @Mock private TelephonyManager mTelephonyManager; @Mock private SensorPrivacyManager mSensorPrivacyManager; @@ -260,7 +267,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Mock private SelectedUserInteractor mSelectedUserInteractor; @Mock - private KeyguardFaceAuthInteractor mFaceAuthInteractor; + private DeviceEntryFaceAuthInteractor mFaceAuthInteractor; @Captor private ArgumentCaptor<FaceAuthenticationListener> mFaceAuthenticationListener; @@ -314,7 +321,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mContext.getOrCreateTestableResources().addOverride( com.android.systemui.res.R.integer.config_face_auth_supported_posture, DEVICE_POSTURE_UNKNOWN); - mFaceWakeUpTriggersConfig = new FaceWakeUpTriggersConfig( + mFaceWakeUpTriggersConfig = new FaceWakeUpTriggersConfigImpl( mContext.getResources(), mGlobalSettings, mDumpManager @@ -334,6 +341,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { anyInt()); mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext); + mKeyguardUpdateMonitor.mFoldGracePeriodProvider = mFoldGracePeriodProvider; setupBiometrics(mKeyguardUpdateMonitor); mKeyguardUpdateMonitor.setFaceAuthInteractor(mFaceAuthInteractor); verify(mFaceAuthInteractor).registerListener(mFaceAuthenticationListener.capture()); @@ -922,8 +930,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void trustAgentHasTrust() { // WHEN user has trust - mKeyguardUpdateMonitor.onTrustChanged(true, true, - mSelectedUserInteractor.getSelectedUserId(), 0, null); + givenSelectedUserCanSkipBouncerFromTrustedState(); // THEN user is considered as "having trust" and bouncer can be skipped Assert.assertTrue(mKeyguardUpdateMonitor.getUserHasTrust( @@ -947,8 +954,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void trustAgentHasTrust_fingerprintLockout() { // GIVEN user has trust - mKeyguardUpdateMonitor.onTrustChanged(true, true, - mSelectedUserInteractor.getSelectedUserId(), 0, null); + givenSelectedUserCanSkipBouncerFromTrustedState(); Assert.assertTrue(mKeyguardUpdateMonitor.getUserHasTrust( mSelectedUserInteractor.getSelectedUserId())); @@ -1973,6 +1979,61 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test + public void detectFingerprint_onSuccess_biometricStateStopped() { + // GIVEN FP detection is running + givenDetectFingerprintWithClearingFingerprintManagerInvocations(); + + // WHEN detection is successful + ArgumentCaptor<FingerprintManager.FingerprintDetectionCallback> fpDetectCallbackCaptor = + ArgumentCaptor.forClass(FingerprintManager.FingerprintDetectionCallback.class); + verify(mFingerprintManager).detectFingerprint( + any(), fpDetectCallbackCaptor.capture(), any()); + fpDetectCallbackCaptor.getValue().onFingerprintDetected(0, 0, true); + mTestableLooper.processAllMessages(); + + // THEN fingerprint detect state should immediately update to STOPPED + assertThat(mKeyguardUpdateMonitor.mFingerprintRunningState) + .isEqualTo(BIOMETRIC_STATE_STOPPED); + } + + @Test + public void runFpDetectFlagDisabled_sideFps_keyguardDismissible_fingerprintAuthenticateRuns() { + mSetFlagsRule.disableFlags(Flags.FLAG_RUN_FINGERPRINT_DETECT_ON_DISMISSIBLE_KEYGUARD); + + // Clear invocations, since previous setup (e.g. registering BiometricManager callbacks) + // will trigger updateBiometricListeningState(); + clearInvocations(mFingerprintManager); + mKeyguardUpdateMonitor.resetBiometricListeningState(); + + // GIVEN the user can skip the bouncer + givenSelectedUserCanSkipBouncerFromTrustedState(); + when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); + mKeyguardUpdateMonitor.dispatchStartedGoingToSleep(0 /* why */); + mTestableLooper.processAllMessages(); + + // WHEN verify authenticate runs + verifyFingerprintAuthenticateCall(); + } + + @Test + public void sideFps_keyguardDismissible_fingerprintDetectRuns() { + mSetFlagsRule.enableFlags(Flags.FLAG_RUN_FINGERPRINT_DETECT_ON_DISMISSIBLE_KEYGUARD); + // Clear invocations, since previous setup (e.g. registering BiometricManager callbacks) + // will trigger updateBiometricListeningState(); + clearInvocations(mFingerprintManager); + mKeyguardUpdateMonitor.resetBiometricListeningState(); + + // GIVEN the user can skip the bouncer + givenSelectedUserCanSkipBouncerFromTrustedState(); + when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); + mKeyguardUpdateMonitor.dispatchStartedGoingToSleep(0 /* why */); + mTestableLooper.processAllMessages(); + + // WHEN verify detect runs + verifyFingerprintDetectCall(); + } + + @Test public void testFingerprintSensorProperties() throws RemoteException { mFingerprintAuthenticatorsRegisteredCallback.onAllAuthenticatorsRegistered( new ArrayList<>()); @@ -2077,6 +2138,35 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { verify(callback).onBiometricEnrollmentStateChanged(BiometricSourceType.FACE); } + private void givenSelectedUserCanSkipBouncerFromTrustedState() { + mKeyguardUpdateMonitor.onTrustChanged(true, true, + mSelectedUserInteractor.getSelectedUserId(), 0, null); + } + + @Test + public void forceIsDismissibleKeyguard_foldingGracePeriodNotEnabled() { + when(mFoldGracePeriodProvider.isEnabled()).thenReturn(false); + primaryAuthNotRequiredByStrongAuthTracker(); + mKeyguardUpdateMonitor.tryForceIsDismissibleKeyguard(); + Assert.assertFalse(mKeyguardUpdateMonitor.forceIsDismissibleIsKeepingDeviceUnlocked()); + } + + @Test + public void forceIsDismissibleKeyguard() { + when(mFoldGracePeriodProvider.isEnabled()).thenReturn(true); + primaryAuthNotRequiredByStrongAuthTracker(); + mKeyguardUpdateMonitor.tryForceIsDismissibleKeyguard(); + Assert.assertTrue(mKeyguardUpdateMonitor.forceIsDismissibleIsKeepingDeviceUnlocked()); + } + + @Test + public void forceIsDismissibleKeyguard_respectsLockdown() { + when(mFoldGracePeriodProvider.isEnabled()).thenReturn(true); + userDeviceLockDown(); + mKeyguardUpdateMonitor.tryForceIsDismissibleKeyguard(); + Assert.assertFalse(mKeyguardUpdateMonitor.forceIsDismissibleIsKeepingDeviceUnlocked()); + } + private void verifyFingerprintAuthenticateNeverCalled() { verify(mFingerprintManager, never()).authenticate(any(), any(), any(), any(), any()); verify(mFingerprintManager, never()).authenticate(any(), any(), any(), any(), anyInt(), diff --git a/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt index 9fe32f1e378b..b45c8948e763 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt @@ -16,16 +16,21 @@ package com.android.keyguard.mediator -import android.os.Handler import android.os.Looper +import android.platform.test.flag.junit.SetFlagsRule +import android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.unfold.FoldAodAnimationController import com.android.systemui.unfold.SysUIUnfoldComponent import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation import com.android.systemui.util.mockito.capture +import com.android.systemui.utils.os.FakeHandler +import com.android.systemui.utils.os.FakeHandler.Mode.QUEUEING import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor @@ -52,10 +57,13 @@ class ScreenOnCoordinatorTest : SysuiTestCase() { @Captor private lateinit var readyCaptor: ArgumentCaptor<Runnable> - private val testHandler = Handler(Looper.getMainLooper()) + private val testHandler = FakeHandler(Looper.getMainLooper()).apply { setMode(QUEUEING) } private lateinit var screenOnCoordinator: ScreenOnCoordinator + @get:Rule + val setFlagsRule = SetFlagsRule(DEVICE_DEFAULT) + @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -77,7 +85,7 @@ class ScreenOnCoordinatorTest : SysuiTestCase() { onUnfoldOverlayReady() onFoldAodReady() - waitHandlerIdle(testHandler) + waitHandlerIdle() // Should be called when both unfold overlay and keyguard drawn ready verify(runnable).run() @@ -90,7 +98,7 @@ class ScreenOnCoordinatorTest : SysuiTestCase() { onUnfoldOverlayReady() onFoldAodReady() - waitHandlerIdle(testHandler) + waitHandlerIdle() // Should be called when both unfold overlay and keyguard drawn ready verify(runnable).run() @@ -104,7 +112,8 @@ class ScreenOnCoordinatorTest : SysuiTestCase() { onUnfoldOverlayReady() onFoldAodReady() - waitHandlerIdle(testHandler) + waitHandlerIdle() + // Should not be called because this screen turning on call is not valid anymore verify(runnable, never()).run() @@ -112,13 +121,43 @@ class ScreenOnCoordinatorTest : SysuiTestCase() { @Test fun testUnfoldTransitionDisabledDrawnTasksReady_onScreenTurningOn_callsDrawnCallback() { + setFlagsRule.disableFlags(Flags.FLAG_ENABLE_BACKGROUND_KEYGUARD_ONDRAWN_CALLBACK) // Recreate with empty unfoldComponent screenOnCoordinator = ScreenOnCoordinator( Optional.empty(), testHandler ) screenOnCoordinator.onScreenTurningOn(runnable) - waitHandlerIdle(testHandler) + waitHandlerIdle() + + // Should be called when only keyguard drawn + verify(runnable).run() + } + @Test + fun testUnfoldTransitionDisabledDrawnTasksReady_onScreenTurningOn_usesMainHandler() { + setFlagsRule.disableFlags(Flags.FLAG_ENABLE_BACKGROUND_KEYGUARD_ONDRAWN_CALLBACK) + // Recreate with empty unfoldComponent + screenOnCoordinator = ScreenOnCoordinator( + Optional.empty(), + testHandler + ) + screenOnCoordinator.onScreenTurningOn(runnable) + + // Never called as the main handler didn't schedule it yet. + verify(runnable, never()).run() + } + + @Test + fun unfoldTransitionDisabledDrawnTasksReady_onScreenTurningOn_bgCallback_callsDrawnCallback() { + setFlagsRule.enableFlags(Flags.FLAG_ENABLE_BACKGROUND_KEYGUARD_ONDRAWN_CALLBACK) + // Recreate with empty unfoldComponent + screenOnCoordinator = ScreenOnCoordinator( + Optional.empty(), + testHandler + ) + screenOnCoordinator.onScreenTurningOn(runnable) + // No need to wait for the handler to be idle, as it shouldn't be used + // waitHandlerIdle() // Should be called when only keyguard drawn verify(runnable).run() @@ -134,7 +173,7 @@ class ScreenOnCoordinatorTest : SysuiTestCase() { readyCaptor.value.run() } - private fun waitHandlerIdle(handler: Handler) { - handler.runWithScissors({}, /* timeout= */ 0) + private fun waitHandlerIdle() { + testHandler.dispatchQueuedMessages() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt index bfb5485e47b7..c52571188256 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt @@ -120,7 +120,7 @@ class FontScalingDialogDelegateTest : SysuiTestCase() { fontScalingDialogDelegate ) - whenever(dialogFactory.create(any())).thenReturn(dialog) + whenever(dialogFactory.create(any(), any())).thenReturn(dialog) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt index 8693d5c87dce..7c626a141a4a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt @@ -16,6 +16,9 @@ package com.android.systemui.back.domain.interactor +import android.platform.test.annotations.RequiresFlagsDisabled +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.DeviceFlagsValueProvider import android.view.ViewRootImpl import android.window.BackEvent import android.window.BackEvent.EDGE_LEFT @@ -26,9 +29,8 @@ import android.window.WindowOnBackInvokedDispatcher import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.statusbar.IStatusBarService +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest @@ -72,7 +74,6 @@ import org.mockito.junit.MockitoJUnit @OptIn(ExperimentalCoroutinesApi::class) class BackActionInteractorTest : SysuiTestCase() { private val testScope = TestScope() - private val featureFlags = FakeFeatureFlags() private val executor = FakeExecutor(FakeSystemClock()) @JvmField @Rule var mockitoRule = MockitoJUnit.rule() @@ -107,17 +108,17 @@ class BackActionInteractorTest : SysuiTestCase() { statusBarKeyguardViewManager, shadeController, notificationShadeWindowController, - windowRootViewVisibilityInteractor, - featureFlags, + windowRootViewVisibilityInteractor ) .apply { this.setup(qsController, shadeViewController) } } private val powerInteractor = PowerInteractorFactory.create().powerInteractor + @get:Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + @Before fun setUp() { - featureFlags.set(Flags.WM_SHADE_ANIMATE_BACK_GESTURE, false) whenever(notificationShadeWindowController.windowRootView).thenReturn(windowRootView) whenever(windowRootView.viewRootImpl).thenReturn(viewRootImpl) whenever(viewRootImpl.onBackInvokedDispatcher).thenReturn(onBackInvokedDispatcher) @@ -229,9 +230,9 @@ class BackActionInteractorTest : SysuiTestCase() { } @Test + @RequiresFlagsDisabled(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_SHADE) fun animationFlagOff_onBackInvoked_keyguardNotified() { backActionInteractor.start() - featureFlags.set(Flags.WM_SHADE_ANIMATE_BACK_GESTURE, false) windowRootViewVisibilityInteractor.setIsLockscreenOrShadeVisible(true) powerInteractor.setAwakeForTest() val callback = getBackInvokedCallback() @@ -243,8 +244,8 @@ class BackActionInteractorTest : SysuiTestCase() { } @Test + @RequiresFlagsEnabled(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_SHADE) fun animationFlagOn_onBackInvoked_keyguardNotified() { - featureFlags.set(Flags.WM_SHADE_ANIMATE_BACK_GESTURE, true) backActionInteractor.start() windowRootViewVisibilityInteractor.setIsLockscreenOrShadeVisible(true) powerInteractor.setAwakeForTest() @@ -257,8 +258,8 @@ class BackActionInteractorTest : SysuiTestCase() { } @Test + @RequiresFlagsEnabled(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_SHADE) fun animationFlagOn_callbackIsAnimationCallback() { - featureFlags.set(Flags.WM_SHADE_ANIMATE_BACK_GESTURE, true) backActionInteractor.start() windowRootViewVisibilityInteractor.setIsLockscreenOrShadeVisible(true) powerInteractor.setAwakeForTest() @@ -269,8 +270,8 @@ class BackActionInteractorTest : SysuiTestCase() { } @Test + @RequiresFlagsEnabled(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_SHADE) fun onBackProgressed_shadeCannotBeCollapsed_shadeViewControllerNotNotified() { - featureFlags.set(Flags.WM_SHADE_ANIMATE_BACK_GESTURE, true) backActionInteractor.start() windowRootViewVisibilityInteractor.setIsLockscreenOrShadeVisible(true) powerInteractor.setAwakeForTest() @@ -284,8 +285,8 @@ class BackActionInteractorTest : SysuiTestCase() { } @Test + @RequiresFlagsEnabled(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_SHADE) fun onBackProgressed_shadeCanBeCollapsed_shadeViewControllerNotified() { - featureFlags.set(Flags.WM_SHADE_ANIMATE_BACK_GESTURE, true) backActionInteractor.start() windowRootViewVisibilityInteractor.setIsLockscreenOrShadeVisible(true) powerInteractor.setAwakeForTest() diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt index 1f7dd6dd9a9c..c143bc0aa06c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt @@ -123,7 +123,6 @@ class AuthRippleControllerTest : SysuiTestCase() { udfpsControllerProvider, statusBarStateController, displayMetrics, - featureFlags, KeyguardLogger(logcatLogBuffer(AuthRippleController.TAG)), biometricUnlockController, lightRevealScrim, diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt index 86b9b84ab36e..9b0e58d63952 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt @@ -22,7 +22,7 @@ import android.view.accessibility.AccessibilityNodeInfo import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.whenever @@ -42,7 +42,7 @@ import org.mockito.MockitoAnnotations class FaceAuthAccessibilityDelegateTest : SysuiTestCase() { @Mock private lateinit var hostView: View - @Mock private lateinit var faceAuthInteractor: KeyguardFaceAuthInteractor + @Mock private lateinit var faceAuthInteractor: DeviceEntryFaceAuthInteractor private lateinit var underTest: FaceAuthAccessibilityDelegate @Before diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt index cb261789d7bf..54dbd04fb367 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt @@ -57,13 +57,13 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInte import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.bouncer.ui.BouncerView import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.display.data.repository.FakeDisplayRepository import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.FakeTrustRepository import com.android.systemui.keyguard.domain.interactor.DeviceEntrySideFpsOverlayInteractor -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor import com.android.systemui.keyguard.ui.viewmodel.SideFpsProgressBarViewModel import com.android.systemui.log.SideFpsLogger import com.android.systemui.log.logcatLogBuffer @@ -75,6 +75,7 @@ import com.android.systemui.unfold.compat.ScreenSizeFoldProvider import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import java.util.Optional @@ -82,6 +83,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -108,7 +110,7 @@ class SideFpsOverlayViewBinderTest : SysuiTestCase() { @JvmField @Rule var mockitoRule: MockitoRule = MockitoJUnit.rule() @Mock private lateinit var activityTaskManager: ActivityTaskManager @Mock private lateinit var displayManager: DisplayManager - @Mock private lateinit var faceAuthInteractor: KeyguardFaceAuthInteractor + @Mock private lateinit var faceAuthInteractor: DeviceEntryFaceAuthInteractor @Mock private lateinit var fingerprintInteractiveToAuthProvider: FingerprintInteractiveToAuthProvider @Mock private lateinit var fpsUnlockTracker: FpsUnlockTracker @@ -235,15 +237,18 @@ class SideFpsOverlayViewBinderTest : SysuiTestCase() { windowManager, displayStateInteractor, Optional.of(fingerprintInteractiveToAuthProvider), + mock(), SideFpsLogger(logcatLogBuffer("SfpsLogger")) ) sideFpsProgressBarViewModel = SideFpsProgressBarViewModel( mContext, - deviceEntryFingerprintAuthRepository, + mock(), sfpsSensorInteractor, + mock(), displayStateInteractor, + UnconfinedTestDispatcher(), testScope.backgroundScope, ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt index 823b952d9888..1fa60fc9044b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt @@ -55,13 +55,13 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.bouncer.ui.BouncerView import com.android.systemui.classifier.FalsingCollector import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.display.data.repository.FakeDisplayRepository import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.FakeTrustRepository import com.android.systemui.keyguard.domain.interactor.DeviceEntrySideFpsOverlayInteractor -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor import com.android.systemui.keyguard.ui.viewmodel.SideFpsProgressBarViewModel import com.android.systemui.log.SideFpsLogger import com.android.systemui.log.logcatLogBuffer @@ -72,6 +72,7 @@ import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.unfold.compat.ScreenSizeFoldProvider import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat @@ -100,7 +101,7 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { @JvmField @Rule var mockitoRule: MockitoRule = MockitoJUnit.rule() @Mock private lateinit var activityTaskManager: ActivityTaskManager - @Mock private lateinit var faceAuthInteractor: KeyguardFaceAuthInteractor + @Mock private lateinit var faceAuthInteractor: DeviceEntryFaceAuthInteractor @Mock private lateinit var fingerprintInteractiveToAuthProvider: FingerprintInteractiveToAuthProvider @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @@ -238,15 +239,18 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { windowManager, displayStateInteractor, Optional.of(fingerprintInteractiveToAuthProvider), + mock(), SideFpsLogger(logcatLogBuffer("SfpsLogger")) ) sideFpsProgressBarViewModel = SideFpsProgressBarViewModel( mContext, - deviceEntryFingerprintAuthRepository, + mock(), sfpsSensorInteractor, + mock(), displayStateInteractor, + StandardTestDispatcher(), testScope.backgroundScope, ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java index 4022d4388ab1..3ff43c6a3787 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java @@ -28,8 +28,6 @@ import static org.mockito.Mockito.when; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; -import android.view.LayoutInflater; -import android.view.View; import android.widget.Button; import android.widget.TextView; @@ -95,7 +93,7 @@ public class BroadcastDialogDelegateTest extends SysuiTestCase { mFeatureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM, true); when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState); - when(mSystemUIDialogFactory.create(any())).thenReturn(mDialog); + when(mSystemUIDialogFactory.create(any(), any())).thenReturn(mDialog); mBroadcastDialogDelegate = new BroadcastDialogDelegate( mContext, diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt index 45a426e1cb84..e7963031411d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt @@ -36,13 +36,13 @@ import com.android.systemui.bouncer.shared.model.BouncerMessageModel import com.android.systemui.bouncer.ui.BouncerView import com.android.systemui.classifier.FalsingCollector import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.flags.SystemPropertiesHelper import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.FakeTrustRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor import com.android.systemui.keyguard.shared.model.AuthenticationFlags import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown import com.android.systemui.res.R.string.kg_trust_agent_disabled @@ -122,7 +122,7 @@ class BouncerMessageInteractorTest : SysuiTestCase() { fakeTrustRepository, testScope.backgroundScope, mSelectedUserInteractor, - mock(KeyguardFaceAuthInteractor::class.java), + mock(DeviceEntryFaceAuthInteractor::class.java), ) underTest = BouncerMessageInteractor( diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt index dacf23a5a567..ee46f76a94e4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt @@ -32,9 +32,9 @@ import com.android.systemui.bouncer.shared.model.BouncerShowMessageModel import com.android.systemui.bouncer.ui.BouncerView import com.android.systemui.bouncer.ui.BouncerViewDelegate import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.repository.FakeTrustRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR @@ -73,7 +73,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() { @Mock private lateinit var dismissCallbackRegistry: DismissCallbackRegistry @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor - @Mock private lateinit var faceAuthInteractor: KeyguardFaceAuthInteractor + @Mock private lateinit var faceAuthInteractor: DeviceEntryFaceAuthInteractor private lateinit var mainHandler: FakeHandler private lateinit var underTest: PrimaryBouncerInteractor private lateinit var resources: TestableResources diff --git a/packages/SystemUI/tests/src/com/android/systemui/contrast/ContrastDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/contrast/ContrastDialogDelegateTest.kt index 65f68f9df3e1..35ac2ae4ed44 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/contrast/ContrastDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/contrast/ContrastDialogDelegateTest.kt @@ -31,6 +31,7 @@ import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.FeatureFlags import com.android.systemui.model.SysUiState import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.phone.DialogDelegate import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any @@ -69,7 +70,8 @@ class ContrastDialogDelegateTest : SysuiTestCase() { mDependency.injectTestDependency(SysUiState::class.java, sysuiState) mDependency.injectMockDependency(DialogLaunchAnimator::class.java) whenever(sysuiState.setFlag(any(), any())).thenReturn(sysuiState) - whenever(sysuiDialogFactory.create(any())).thenReturn(sysuiDialog) + whenever(sysuiDialogFactory.create(any(SystemUIDialog.Delegate::class.java))) + .thenReturn(sysuiDialog) whenever(sysuiDialog.layoutInflater).thenReturn(LayoutInflater.from(mContext)) whenever(mockUserTracker.userId).thenReturn(context.userId) diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt index 4e8f86615522..7f0ea9a7a6d0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt @@ -17,34 +17,48 @@ package com.android.systemui.controls.management +import android.content.Context import android.content.DialogInterface import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest -import com.android.systemui.res.R import com.android.systemui.SysuiTestCase +import com.android.systemui.res.R import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations +import org.mockito.Mockito.`when` as whenever @SmallTest @RunWith(AndroidTestingRunner::class) class PanelConfirmationDialogFactoryTest : SysuiTestCase() { + @Mock private lateinit var mockDialog : SystemUIDialog + @Mock private lateinit var mockDialogFactory : SystemUIDialog.Factory + private lateinit var factory : PanelConfirmationDialogFactory + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + whenever(mockDialogFactory.create(any(Context::class.java))).thenReturn(mockDialog) + whenever(mockDialog.context).thenReturn(mContext) + factory = PanelConfirmationDialogFactory(mockDialogFactory) + } + @Test fun testDialogHasCorrectInfo() { - val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) } - val factory = PanelConfirmationDialogFactory { mockDialog } val appName = "appName" - factory.createConfirmationDialog(context, appName) {} + factory.createConfirmationDialog(mContext, appName) {} verify(mockDialog).setCanceledOnTouchOutside(true) verify(mockDialog) @@ -55,12 +69,9 @@ class PanelConfirmationDialogFactoryTest : SysuiTestCase() { @Test fun testDialogPositiveButton() { - val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) } - val factory = PanelConfirmationDialogFactory { mockDialog } - var response: Boolean? = null - factory.createConfirmationDialog(context, "") { response = it } + factory.createConfirmationDialog(mContext,"") { response = it } val captor: ArgumentCaptor<DialogInterface.OnClickListener> = argumentCaptor() verify(mockDialog).setPositiveButton(eq(R.string.controls_dialog_ok), capture(captor)) @@ -72,12 +83,9 @@ class PanelConfirmationDialogFactoryTest : SysuiTestCase() { @Test fun testDialogNeutralButton() { - val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) } - val factory = PanelConfirmationDialogFactory { mockDialog } - var response: Boolean? = null - factory.createConfirmationDialog(context, "") { response = it } + factory.createConfirmationDialog(mContext, "") { response = it } val captor: ArgumentCaptor<DialogInterface.OnClickListener> = argumentCaptor() verify(mockDialog).setNeutralButton(eq(R.string.cancel), capture(captor)) @@ -89,12 +97,9 @@ class PanelConfirmationDialogFactoryTest : SysuiTestCase() { @Test fun testDialogCancel() { - val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) } - val factory = PanelConfirmationDialogFactory { mockDialog } - var response: Boolean? = null - factory.createConfirmationDialog(context, "") { response = it } + factory.createConfirmationDialog(mContext, "") { response = it } val captor: ArgumentCaptor<DialogInterface.OnCancelListener> = argumentCaptor() verify(mockDialog).setOnCancelListener(capture(captor)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt index 8f65fc8c3930..bcef67e76ea1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt @@ -24,19 +24,28 @@ import android.content.Intent import android.content.IntentFilter import android.content.pm.ApplicationInfo import android.content.pm.ServiceInfo +import android.os.UserHandle import android.os.UserManager import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.common.data.repository.fakePackageChangeRepository +import com.android.systemui.common.data.repository.packageChangeRepository +import com.android.systemui.common.data.shared.model.PackageChangeModel import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.panels.AuthorizedPanelsRepository import com.android.systemui.controls.panels.FakeSelectedComponentRepository +import com.android.systemui.controls.panels.SelectedComponentRepository import com.android.systemui.controls.ui.SelectedItem +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.settings.UserTracker +import com.android.systemui.testKosmos import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor @@ -48,6 +57,9 @@ import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import java.util.Optional +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -61,10 +73,13 @@ import org.mockito.Mockito.verifyZeroInteractions import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidTestingRunner::class) class ControlsStartableTest : SysuiTestCase() { + private val kosmos = testKosmos() + @Mock private lateinit var controlsController: ControlsController @Mock private lateinit var controlsListingController: ControlsListingController @Mock private lateinit var userTracker: UserTracker @@ -72,7 +87,7 @@ class ControlsStartableTest : SysuiTestCase() { @Mock private lateinit var userManager: UserManager @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher - private val preferredPanelsRepository = FakeSelectedComponentRepository() + private lateinit var preferredPanelsRepository: FakeSelectedComponentRepository private lateinit var fakeExecutor: FakeExecutor @@ -81,8 +96,10 @@ class ControlsStartableTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) whenever(authorizedPanelsRepository.getPreferredPackages()).thenReturn(setOf()) whenever(userManager.isUserUnlocked(anyInt())).thenReturn(true) + whenever(userTracker.userHandle).thenReturn(UserHandle.of(1)) fakeExecutor = FakeExecutor(FakeSystemClock()) + preferredPanelsRepository = FakeSelectedComponentRepository() } @Test @@ -306,6 +323,100 @@ class ControlsStartableTest : SysuiTestCase() { verify(controlsController, never()).setPreferredSelection(any()) } + @Test + fun testSelectedComponentIsUninstalled() = + with(kosmos) { + testScope.runTest { + val selectedComponent = + SelectedComponentRepository.SelectedComponent( + "panel", + TEST_COMPONENT_PANEL, + isPanel = true + ) + preferredPanelsRepository.setSelectedComponent(selectedComponent) + val activeUser = UserHandle.of(100) + whenever(userTracker.userHandle).thenReturn(activeUser) + + createStartable(enabled = true).onBootCompleted() + fakeExecutor.runAllReady() + runCurrent() + + assertThat(preferredPanelsRepository.getSelectedComponent()) + .isEqualTo(selectedComponent) + fakePackageChangeRepository.notifyChange( + PackageChangeModel.Uninstalled( + packageName = TEST_PACKAGE_PANEL, + packageUid = UserHandle.getUid(100, 1) + ) + ) + runCurrent() + + assertThat(preferredPanelsRepository.getSelectedComponent()).isNull() + } + } + + @Test + fun testSelectedComponentIsChanged() = + with(kosmos) { + testScope.runTest { + val selectedComponent = + SelectedComponentRepository.SelectedComponent( + "panel", + TEST_COMPONENT_PANEL, + isPanel = true + ) + preferredPanelsRepository.setSelectedComponent(selectedComponent) + val activeUser = UserHandle.of(100) + whenever(userTracker.userHandle).thenReturn(activeUser) + + createStartable(enabled = true).onBootCompleted() + fakeExecutor.runAllReady() + runCurrent() + + fakePackageChangeRepository.notifyChange( + PackageChangeModel.Changed( + packageName = TEST_PACKAGE_PANEL, + packageUid = UserHandle.getUid(100, 1) + ) + ) + runCurrent() + + assertThat(preferredPanelsRepository.getSelectedComponent()) + .isEqualTo(selectedComponent) + } + } + + @Test + fun testOtherPackageIsUninstalled() = + with(kosmos) { + testScope.runTest { + val selectedComponent = + SelectedComponentRepository.SelectedComponent( + "panel", + TEST_COMPONENT_PANEL, + isPanel = true + ) + preferredPanelsRepository.setSelectedComponent(selectedComponent) + val activeUser = UserHandle.of(100) + whenever(userTracker.userHandle).thenReturn(activeUser) + + createStartable(enabled = true).onBootCompleted() + fakeExecutor.runAllReady() + runCurrent() + + fakePackageChangeRepository.notifyChange( + PackageChangeModel.Uninstalled( + packageName = TEST_PACKAGE, + packageUid = UserHandle.getUid(100, 1) + ) + ) + runCurrent() + + assertThat(preferredPanelsRepository.getSelectedComponent()) + .isEqualTo(selectedComponent) + } + } + private fun setUpControlsListingControls(listings: List<ControlsServiceInfo>) { doAnswer { doReturn(listings).`when`(controlsListingController).getCurrentServices() } .`when`(controlsListingController) @@ -326,11 +437,14 @@ class ControlsStartableTest : SysuiTestCase() { } } return ControlsStartable( + kosmos.applicationCoroutineScope, + kosmos.testDispatcher, fakeExecutor, component, userTracker, authorizedPanelsRepository, preferredPanelsRepository, + kosmos.packageChangeRepository, userManager, broadcastDispatcher, ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt index 8eebceebe874..38c6a0e236ed 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt @@ -17,17 +17,23 @@ package com.android.systemui.controls.ui +import android.content.Context import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.res.R import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.util.FakeSystemUIDialogController +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.any import org.mockito.Mockito.eq import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidTestingRunner::class) @@ -37,18 +43,24 @@ class ControlsDialogsFactoryTest : SysuiTestCase() { const val APP_NAME = "Test App" } - private val fakeDialogController = FakeSystemUIDialogController() + @Mock + private lateinit var mockDialogFactory : SystemUIDialog.Factory + + private val fakeDialogController = FakeSystemUIDialogController(mContext) private lateinit var underTest: ControlsDialogsFactory @Before fun setup() { - underTest = ControlsDialogsFactory { fakeDialogController.dialog } + MockitoAnnotations.initMocks(this) + whenever(mockDialogFactory.create(any(Context::class.java))) + .thenReturn(fakeDialogController.dialog) + underTest = ControlsDialogsFactory(mockDialogFactory) } @Test fun testCreatesRemoveAppDialog() { - val dialog = underTest.createRemoveAppDialog(context, APP_NAME) {} + val dialog = underTest.createRemoveAppDialog(mContext, APP_NAME) {} verify(dialog) .setTitle( @@ -60,7 +72,7 @@ class ControlsDialogsFactoryTest : SysuiTestCase() { @Test fun testPositiveClickRemoveAppDialogWorks() { var dialogResult: Boolean? = null - underTest.createRemoveAppDialog(context, APP_NAME) { dialogResult = it } + underTest.createRemoveAppDialog(mContext, APP_NAME) { dialogResult = it } fakeDialogController.clickPositive() @@ -70,7 +82,7 @@ class ControlsDialogsFactoryTest : SysuiTestCase() { @Test fun testNeutralClickRemoveAppDialogWorks() { var dialogResult: Boolean? = null - underTest.createRemoveAppDialog(context, APP_NAME) { dialogResult = it } + underTest.createRemoveAppDialog(mContext, APP_NAME) { dialogResult = it } fakeDialogController.clickNeutral() @@ -80,7 +92,7 @@ class ControlsDialogsFactoryTest : SysuiTestCase() { @Test fun testCancelRemoveAppDialogWorks() { var dialogResult: Boolean? = null - underTest.createRemoveAppDialog(context, APP_NAME) { dialogResult = it } + underTest.createRemoveAppDialog(mContext, APP_NAME) { dialogResult = it } fakeDialogController.cancel() diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt index 11bd9cb240a5..36ae0c740c48 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt @@ -51,6 +51,7 @@ import com.android.systemui.flags.FeatureFlags import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.FakeSystemUIDialogController import com.android.systemui.util.concurrency.FakeExecutor @@ -97,9 +98,10 @@ class ControlsUiControllerImplTest : SysuiTestCase() { @Mock lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository @Mock lateinit var featureFlags: FeatureFlags @Mock lateinit var packageManager: PackageManager + @Mock lateinit var systemUIDialogFactory: SystemUIDialog.Factory private val preferredPanelRepository = FakeSelectedComponentRepository() - private val fakeDialogController = FakeSystemUIDialogController() + private lateinit var fakeDialogController: FakeSystemUIDialogController private val uiExecutor = FakeExecutor(FakeSystemClock()) private val bgExecutor = FakeExecutor(FakeSystemClock()) @@ -114,6 +116,9 @@ class ControlsUiControllerImplTest : SysuiTestCase() { fun setup() { MockitoAnnotations.initMocks(this) + fakeDialogController = FakeSystemUIDialogController(mContext) + whenever(systemUIDialogFactory.create(any(Context::class.java))) + .thenReturn(fakeDialogController.dialog) controlsSettingsRepository = FakeControlsSettingsRepository() // This way, it won't be cloned every time `LayoutInflater.fromContext` is called, but we @@ -146,10 +151,7 @@ class ControlsUiControllerImplTest : SysuiTestCase() { authorizedPanelsRepository, preferredPanelRepository, featureFlags, - ControlsDialogsFactory { - isRemoveAppDialogCreated = true - fakeDialogController.dialog - }, + ControlsDialogsFactory(systemUIDialogFactory), dumpManager, ) `when`(userTracker.userId).thenReturn(0) diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt index 59bcf01e0eef..d5c3641e75a9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt @@ -24,13 +24,13 @@ import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.domain.interactor.deviceEntryHapticsInteractor +import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus import com.android.systemui.keyevent.data.repository.fakeKeyEventRepository import com.android.systemui.keyguard.data.repository.biometricSettingsRepository import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus -import com.android.systemui.keyguard.shared.model.FailedFaceAuthenticationStatus import com.android.systemui.kosmos.testScope import com.android.systemui.power.data.repository.powerRepository import com.android.systemui.power.shared.model.WakeSleepReason diff --git a/packages/SystemUI/tests/src/com/android/keyguard/FaceWakeUpTriggersConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/FaceWakeUpTriggersConfigTest.kt index 94c34a525547..e9b4bbb5bb1a 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/FaceWakeUpTriggersConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/FaceWakeUpTriggersConfigTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.keyguard +package com.android.systemui.deviceentry.data.repository import android.os.PowerManager import android.testing.AndroidTestingRunner @@ -71,6 +71,6 @@ class FaceWakeUpTriggersConfigTest : SysuiTestCase() { wakeUpTriggers ) - return FaceWakeUpTriggersConfig(mContext.getResources(), globalSettings, dumpManager) + return FaceWakeUpTriggersConfigImpl(mContext.resources, globalSettings, dumpManager) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt index 769cf45da746..368d1d983879 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt @@ -1,21 +1,20 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. + * 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.keyguard.domain.interactor +package com.android.systemui.deviceentry.domain.interactor import android.app.trust.TrustManager import android.content.pm.UserInfo @@ -25,8 +24,6 @@ import android.os.Handler import android.os.PowerManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.keyguard.FaceAuthUiEvent -import com.android.keyguard.FaceWakeUpTriggersConfig import com.android.keyguard.KeyguardSecurityModel import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.SysuiTestCase @@ -42,6 +39,9 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.bouncer.ui.BouncerView import com.android.systemui.classifier.FalsingCollector import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.data.repository.FaceWakeUpTriggersConfig +import com.android.systemui.deviceentry.shared.FaceAuthUiEvent +import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository @@ -49,7 +49,8 @@ import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintA import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.FakeTrustRepository -import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep @@ -87,9 +88,9 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) -class KeyguardFaceAuthInteractorTest : SysuiTestCase() { +class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { - private lateinit var underTest: SystemUIKeyguardFaceAuthInteractor + private lateinit var underTest: SystemUIDeviceEntryFaceAuthInteractor private lateinit var testScope: TestScope private lateinit var bouncerRepository: FakeKeyguardBouncerRepository private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository @@ -133,7 +134,7 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() { fakeBiometricSettingsRepository = FakeBiometricSettingsRepository() underTest = - SystemUIKeyguardFaceAuthInteractor( + SystemUIDeviceEntryFaceAuthInteractor( mContext, testScope.backgroundScope, dispatcher, diff --git a/packages/SystemUI/tests/src/com/android/keyguard/FaceAuthReasonTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/shared/FaceAuthReasonTest.kt index 68d0f41bb284..ef89752dd3d1 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/FaceAuthReasonTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/shared/FaceAuthReasonTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.keyguard + +package com.android.systemui.deviceentry.shared import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase diff --git a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt index ab6bc2ca2dda..66fdf538e284 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt @@ -19,7 +19,6 @@ package com.android.systemui.haptics.slider import android.os.VibrationAttributes import android.os.VibrationEffect import android.view.VelocityTracker -import android.view.animation.AccelerateInterpolator import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -51,8 +50,6 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { private val lowTickDuration = 12 // Mocked duration of a low tick private val dragTextureThresholdMillis = lowTickDuration * config.numberOfLowTicks + config.deltaMillisForDragInterval - private val progressInterpolator = AccelerateInterpolator(config.progressInterpolatorFactor) - private val velocityInterpolator = AccelerateInterpolator(config.velocityInterpolatorFactor) private lateinit var sliderHapticFeedbackProvider: SliderHapticFeedbackProvider @Before @@ -60,7 +57,9 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) whenever(vibratorHelper.getPrimitiveDurations(any())) .thenReturn(intArrayOf(lowTickDuration)) - whenever(velocityTracker.xVelocity).thenReturn(config.maxVelocityToScale) + whenever(velocityTracker.isAxisSupported(config.velocityAxis)).thenReturn(true) + whenever(velocityTracker.getAxisVelocity(config.velocityAxis)) + .thenReturn(config.maxVelocityToScale) sliderHapticFeedbackProvider = SliderHapticFeedbackProvider(vibratorHelper, velocityTracker, config, clock) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index b38c9ecb6287..b57cf53911e9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -72,6 +72,7 @@ import android.view.WindowManager; import androidx.test.filters.SmallTest; +import com.android.internal.foldables.FoldGracePeriodProvider; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.InstanceId; import com.android.internal.logging.UiEventLogger; @@ -324,6 +325,54 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { @Test @TestableLooper.RunWithLooper(setAsMainLooper = true) + public void showKeyguardAfterKeyguardNotEnabled() { + // GIVEN feature is enabled + final FoldGracePeriodProvider mockedFoldGracePeriodProvider = + mock(FoldGracePeriodProvider.class); + mViewMediator.mFoldGracePeriodProvider = mockedFoldGracePeriodProvider; + when(mockedFoldGracePeriodProvider.isEnabled()).thenReturn(true); + + // GIVEN keyguard is not enabled and isn't showing + mViewMediator.onSystemReady(); + mViewMediator.setKeyguardEnabled(false); + TestableLooper.get(this).processAllMessages(); + captureKeyguardUpdateMonitorCallback(); + assertFalse(mViewMediator.isShowingAndNotOccluded()); + + // WHEN showKeyguard is requested + mViewMediator.showDismissibleKeyguard(); + + // THEN keyguard is shown + TestableLooper.get(this).processAllMessages(); + assertTrue(mViewMediator.isShowingAndNotOccluded()); + } + + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) + public void showKeyguardAfterKeyguardNotEnabled_featureNotEnabled() { + // GIVEN feature is NOT enabled + final FoldGracePeriodProvider mockedFoldGracePeriodProvider = + mock(FoldGracePeriodProvider.class); + mViewMediator.mFoldGracePeriodProvider = mockedFoldGracePeriodProvider; + when(mockedFoldGracePeriodProvider.isEnabled()).thenReturn(false); + + // GIVEN keyguard is not enabled and isn't showing + mViewMediator.onSystemReady(); + mViewMediator.setKeyguardEnabled(false); + TestableLooper.get(this).processAllMessages(); + captureKeyguardUpdateMonitorCallback(); + assertFalse(mViewMediator.isShowingAndNotOccluded()); + + // WHEN showKeyguard is requested + mViewMediator.showDismissibleKeyguard(); + + // THEN keyguard is still NOT shown + TestableLooper.get(this).processAllMessages(); + assertFalse(mViewMediator.isShowingAndNotOccluded()); + } + + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) public void doNotHideKeyguard_whenLockdown_onKeyguardNotEnabledExternally() { // GIVEN keyguard is enabled and lockdown occurred so the keyguard is showing mViewMediator.onSystemReady(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractorTest.kt index 027dfa15f812..c4df27c2ccb2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractorTest.kt @@ -29,6 +29,7 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.bouncer.ui.BouncerView import com.android.systemui.classifier.FalsingCollector import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository @@ -62,7 +63,7 @@ import org.mockito.junit.MockitoRule class DeviceEntrySideFpsOverlayInteractorTest : SysuiTestCase() { @JvmField @Rule var mockitoRule: MockitoRule = MockitoJUnit.rule() - @Mock private lateinit var faceAuthInteractor: KeyguardFaceAuthInteractor + @Mock private lateinit var faceAuthInteractor: DeviceEntryFaceAuthInteractor @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt index 1f245f1e86f0..7358b9d5d2d1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt @@ -147,7 +147,7 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { trustRepository, testScope.backgroundScope, mSelectedUserInteractor, - keyguardFaceAuthInteractor = mock(), + deviceEntryFaceAuthInteractor = mock(), ), AlternateBouncerInteractor( statusBarStateController = mock(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt index d4210040faf3..1b4573dafe5e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt @@ -34,6 +34,7 @@ import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockFaceConfig import com.android.systemui.plugins.clocks.ClockFaceController import com.android.systemui.shared.clocks.ClockRegistry +import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat @@ -66,6 +67,8 @@ class KeyguardClockViewModelTest : SysuiTestCase() { @Mock private lateinit var largeClock: ClockFaceController @Mock private lateinit var clockFaceConfig: ClockFaceConfig @Mock private lateinit var eventController: ClockEventController + @Mock private lateinit var splitShadeStateController: SplitShadeStateController + @Before fun setup() { MockitoAnnotations.initMocks(this) @@ -92,6 +95,7 @@ class KeyguardClockViewModelTest : SysuiTestCase() { keyguardInteractor, keyguardClockInteractor, scope.backgroundScope, + splitShadeStateController, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java index 6248bb1009dc..1a303b08b396 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java @@ -55,6 +55,7 @@ import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.settings.UserTracker; +import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.util.NotificationChannels; import com.android.systemui.util.settings.FakeGlobalSettings; @@ -77,7 +78,6 @@ public class PowerNotificationWarningsTest extends SysuiTestCase { public static final String FORMATTED_45M = "0h 45m"; public static final String FORMATTED_HOUR = "1h 0m"; private final NotificationManager mMockNotificationManager = mock(NotificationManager.class); - private final GlobalSettings mGlobalSettings = new FakeGlobalSettings(); private PowerNotificationWarnings mPowerNotificationWarnings; @Mock @@ -90,6 +90,10 @@ public class PowerNotificationWarningsTest extends SysuiTestCase { private UserTracker mUserTracker; @Mock private View mView; + @Mock + private SystemUIDialog.Factory mSystemUIDialogFactory; + @Mock + private SystemUIDialog mSystemUIDialog; private BroadcastReceiver mReceiver; @@ -113,9 +117,16 @@ public class PowerNotificationWarningsTest extends SysuiTestCase { when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser()); when(mUserTracker.getUserHandle()).thenReturn( UserHandle.of(ActivityManager.getCurrentUser())); - mPowerNotificationWarnings = new PowerNotificationWarnings(wrapper, starter, - broadcastSender, () -> mBatteryController, mDialogLaunchAnimator, mUiEventLogger, - mGlobalSettings, mUserTracker); + when(mSystemUIDialogFactory.create()).thenReturn(mSystemUIDialog); + mPowerNotificationWarnings = new PowerNotificationWarnings( + wrapper, + starter, + broadcastSender, + () -> mBatteryController, + mDialogLaunchAnimator, + mUiEventLogger, + mUserTracker, + mSystemUIDialogFactory); BatteryStateSnapshot snapshot = new BatteryStateSnapshot(100, false, false, 1, BatteryManager.BATTERY_HEALTH_GOOD, 5, 15); mPowerNotificationWarnings.updateSnapshot(snapshot); @@ -251,7 +262,7 @@ public class PowerNotificationWarningsTest extends SysuiTestCase { verify(mDialogLaunchAnimator, never()).showFromView(any(), any()); - assertThat(mPowerNotificationWarnings.getSaverConfirmationDialog().isShowing()).isTrue(); + verify(mPowerNotificationWarnings.getSaverConfirmationDialog()).show(); mPowerNotificationWarnings.getSaverConfirmationDialog().dismiss(); } @@ -266,7 +277,7 @@ public class PowerNotificationWarningsTest extends SysuiTestCase { verify(mDialogLaunchAnimator, never()).showFromView(any(), any()); - assertThat(mPowerNotificationWarnings.getSaverConfirmationDialog().isShowing()).isTrue(); + verify(mPowerNotificationWarnings.getSaverConfirmationDialog()).show(); mPowerNotificationWarnings.getSaverConfirmationDialog().dismiss(); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java index f5a3becc7017..698868d67071 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java @@ -27,6 +27,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.app.IActivityManager; import android.app.IForegroundServiceObserver; @@ -53,6 +54,7 @@ import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; import com.android.systemui.settings.UserTracker; +import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.util.DeviceConfigProxyFake; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; @@ -95,6 +97,10 @@ public class FgsManagerControllerTest extends SysuiTestCase { BroadcastDispatcher mBroadcastDispatcher; @Mock DumpManager mDumpManager; + @Mock + SystemUIDialog.Factory mSystemUIDialogFactory; + @Mock + SystemUIDialog mSystemUIDialog; private FgsManagerController mFmc; @@ -114,6 +120,7 @@ public class FgsManagerControllerTest extends SysuiTestCase { mSystemClock = new FakeSystemClock(); mMainExecutor = new FakeExecutor(mSystemClock); mBackgroundExecutor = new FakeExecutor(mSystemClock); + when(mSystemUIDialogFactory.create()).thenReturn(mSystemUIDialog); mUserProfiles = new ArrayList<>(); Mockito.doReturn(mUserProfiles).when(mUserTracker).getUserProfiles(); @@ -325,7 +332,8 @@ public class FgsManagerControllerTest extends SysuiTestCase { mDeviceConfigProxyFake, mDialogLaunchAnimator, mBroadcastDispatcher, - mDumpManager + mDumpManager, + mSystemUIDialogFactory ); fmc.init(); Assert.assertTrue(fmc.getIncludesUserVisibleJobs()); @@ -351,7 +359,8 @@ public class FgsManagerControllerTest extends SysuiTestCase { mDeviceConfigProxyFake, mDialogLaunchAnimator, mBroadcastDispatcher, - mDumpManager + mDumpManager, + mSystemUIDialogFactory ); fmc.init(); Assert.assertFalse(fmc.getIncludesUserVisibleJobs()); @@ -457,7 +466,8 @@ public class FgsManagerControllerTest extends SysuiTestCase { mDeviceConfigProxyFake, mDialogLaunchAnimator, mBroadcastDispatcher, - mDumpManager + mDumpManager, + mSystemUIDialogFactory ); result.init(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java index 018fa9e2bc86..a92111e2c270 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java @@ -394,6 +394,7 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { public void setTiles_differentTiles_extraTileRemoved() { when(mQSHost.getTiles()).thenReturn(List.of(mQSTile, mOtherTile)); mController.setTiles(); + assertEquals(2, mController.mRecords.size()); clearInvocations(mQSPanel); @@ -402,6 +403,7 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { verify(mQSPanel, times(1)).removeTile(any()); verify(mQSPanel, never()).addTile(any()); + assertEquals(1, mController.mRecords.size()); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt index 51e95be3611b..c109a1e95f66 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt @@ -32,7 +32,9 @@ import com.android.systemui.qs.QSHost import com.android.systemui.qs.QsEventLogger import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.tileimpl.QSTileImpl +import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.policy.DataSaverController +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.Before @@ -49,8 +51,6 @@ class DataSaverTileTest : SysuiTestCase() { @Mock private lateinit var mHost: QSHost @Mock private lateinit var mMetricsLogger: MetricsLogger - @Mock private lateinit var mStatusBarStateController: StatusBarStateController - @Mock private lateinit var mActivityStarter: ActivityStarter @Mock private lateinit var mQsLogger: QSLogger private val falsingManager = FalsingManagerFake() @Mock private lateinit var statusBarStateController: StatusBarStateController @@ -58,6 +58,8 @@ class DataSaverTileTest : SysuiTestCase() { @Mock private lateinit var dataSaverController: DataSaverController @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator @Mock private lateinit var uiEventLogger: QsEventLogger + @Mock private lateinit var systemUIDialogFactory: SystemUIDialog.Factory + @Mock private lateinit var systemUIDialog: SystemUIDialog private lateinit var testableLooper: TestableLooper private lateinit var tile: DataSaverTile @@ -67,7 +69,8 @@ class DataSaverTileTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) testableLooper = TestableLooper.get(this) - Mockito.`when`(mHost.context).thenReturn(mContext) + whenever(mHost.context).thenReturn(mContext) + whenever(systemUIDialogFactory.create()).thenReturn(systemUIDialog) tile = DataSaverTile( @@ -81,7 +84,8 @@ class DataSaverTileTest : SysuiTestCase() { activityStarter, mQsLogger, dataSaverController, - dialogLaunchAnimator + dialogLaunchAnimator, + systemUIDialogFactory ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt index 02d40da1c124..ea2b22c0a746 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt @@ -115,7 +115,7 @@ class QSTileViewModelImplTest : SysuiTestCase() { .isEqualTo( "test_spec:\n" + " QSTileState(" + - "icon=() -> com.android.systemui.common.shared.model.Icon, " + + "icon=() -> com.android.systemui.common.shared.model.Icon?, " + "label=test_data, " + "activationState=INACTIVE, " + "secondaryLabel=null, " + diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt index 0a34810f4d3f..945490f1983d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt @@ -36,6 +36,7 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -56,6 +57,8 @@ import org.mockito.MockitoAnnotations class UserSwitchDialogControllerTest : SysuiTestCase() { @Mock + private lateinit var dialogFactory: SystemUIDialog.Factory + @Mock private lateinit var dialog: SystemUIDialog @Mock private lateinit var falsingManager: FalsingManager @@ -80,7 +83,8 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - `when`(dialog.context).thenReturn(mContext) + whenever(dialog.context).thenReturn(mContext) + whenever(dialogFactory.create()).thenReturn(dialog) controller = UserSwitchDialogController( { userDetailViewAdapter }, @@ -88,7 +92,7 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { falsingManager, dialogLaunchAnimator, uiEventLogger, - { dialog } + dialogFactory ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java index 273ce85f89f5..35bf7753358e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java @@ -18,25 +18,42 @@ package com.android.systemui.reardisplay; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotSame; -import static junit.framework.Assert.assertTrue; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.content.res.Configuration; +import android.content.res.Resources; import android.hardware.devicestate.DeviceStateManager; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.view.LayoutInflater; +import android.view.View; import android.widget.TextView; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.res.R; +import com.android.systemui.flags.FakeFeatureFlags; +import com.android.systemui.flags.Flags; +import com.android.systemui.model.SysUiState; +import com.android.systemui.res.R; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidTestingRunner.class) @@ -45,24 +62,49 @@ public class RearDisplayDialogControllerTest extends SysuiTestCase { @Mock private CommandQueue mCommandQueue; + @Mock + private SystemUIDialog.Factory mSystemUIDialogFactory; + @Mock + private SystemUIDialog mSystemUIDialog; + private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); + @Mock + private SysUiState mSysUiState; + @Mock + private Resources mResources; - private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock()); + LayoutInflater mLayoutInflater = LayoutInflater.from(mContext); + private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock()); private static final int CLOSED_BASE_STATE = 0; private static final int OPEN_BASE_STATE = 1; + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mFeatureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM, true); + when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState); + when(mSystemUIDialogFactory.create()).thenReturn(mSystemUIDialog); + when(mSystemUIDialog.getContext()).thenReturn(mContext); + } @Test public void testClosedDialogIsShown() { - RearDisplayDialogController controller = new RearDisplayDialogController(mContext, - mCommandQueue, mFakeExecutor); + RearDisplayDialogController controller = new RearDisplayDialogController( + mCommandQueue, + mFakeExecutor, + mResources, + mLayoutInflater, + mSystemUIDialogFactory); controller.setDeviceStateManagerCallback(new TestDeviceStateManagerCallback()); controller.setFoldedStates(new int[]{0}); controller.setAnimationRepeatCount(0); controller.showRearDisplayDialog(CLOSED_BASE_STATE); - assertTrue(controller.mRearDisplayEducationDialog.isShowing()); - TextView deviceClosedTitleTextView = controller.mRearDisplayEducationDialog.findViewById( + verify(mSystemUIDialog).show(); + + View container = getDialogViewContainer(); + TextView deviceClosedTitleTextView = container.findViewById( R.id.rear_display_title_text_view); assertEquals(deviceClosedTitleTextView.getText().toString(), getContext().getResources().getString( @@ -71,20 +113,28 @@ public class RearDisplayDialogControllerTest extends SysuiTestCase { @Test public void testClosedDialogIsRefreshedOnConfigurationChange() { - RearDisplayDialogController controller = new RearDisplayDialogController(mContext, - mCommandQueue, mFakeExecutor); + RearDisplayDialogController controller = new RearDisplayDialogController( + mCommandQueue, + mFakeExecutor, + mResources, + mLayoutInflater, + mSystemUIDialogFactory); controller.setDeviceStateManagerCallback(new TestDeviceStateManagerCallback()); controller.setFoldedStates(new int[]{0}); controller.setAnimationRepeatCount(0); controller.showRearDisplayDialog(CLOSED_BASE_STATE); - assertTrue(controller.mRearDisplayEducationDialog.isShowing()); - TextView deviceClosedTitleTextView = controller.mRearDisplayEducationDialog.findViewById( + verify(mSystemUIDialog).show(); + View container = getDialogViewContainer(); + TextView deviceClosedTitleTextView = container.findViewById( R.id.rear_display_title_text_view); + reset(mSystemUIDialog); + when(mSystemUIDialog.isShowing()).thenReturn(true); + when(mSystemUIDialog.getContext()).thenReturn(mContext); + controller.onConfigChanged(new Configuration()); - assertTrue(controller.mRearDisplayEducationDialog.isShowing()); - TextView deviceClosedTitleTextView2 = controller.mRearDisplayEducationDialog.findViewById( + TextView deviceClosedTitleTextView2 = container.findViewById( R.id.rear_display_title_text_view); assertNotSame(deviceClosedTitleTextView, deviceClosedTitleTextView2); @@ -92,22 +142,33 @@ public class RearDisplayDialogControllerTest extends SysuiTestCase { @Test public void testOpenDialogIsShown() { - RearDisplayDialogController controller = new RearDisplayDialogController(mContext, - mCommandQueue, mFakeExecutor); + RearDisplayDialogController controller = new RearDisplayDialogController( + mCommandQueue, + mFakeExecutor, + mResources, + mLayoutInflater, + mSystemUIDialogFactory); controller.setDeviceStateManagerCallback(new TestDeviceStateManagerCallback()); controller.setFoldedStates(new int[]{0}); controller.setAnimationRepeatCount(0); controller.showRearDisplayDialog(OPEN_BASE_STATE); - assertTrue(controller.mRearDisplayEducationDialog.isShowing()); - TextView deviceClosedTitleTextView = controller.mRearDisplayEducationDialog.findViewById( + verify(mSystemUIDialog).show(); + View container = getDialogViewContainer(); + TextView deviceClosedTitleTextView = container.findViewById( R.id.rear_display_title_text_view); assertEquals(deviceClosedTitleTextView.getText().toString(), getContext().getResources().getString( R.string.rear_display_unfolded_bottom_sheet_title)); } + private View getDialogViewContainer() { + ArgumentCaptor<View> viewCaptor = ArgumentCaptor.forClass(View.class); + verify(mSystemUIDialog).setView(viewCaptor.capture()); + + return viewCaptor.getValue(); + } /** * Empty device state manager callbacks, so we can verify that the correct * dialogs are being created regardless of device state of the test device. diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 48baeb3aa9d6..313276727caf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -86,6 +86,7 @@ import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository; import com.android.systemui.common.ui.view.LongPressHandlingView; +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor; import com.android.systemui.doze.DozeLog; import com.android.systemui.dump.DumpManager; @@ -98,7 +99,6 @@ import com.android.systemui.keyguard.KeyguardViewConfigurator; import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository; import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor; -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; @@ -337,11 +337,11 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { protected ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener> mEmptySpaceClickListenerCaptor; @Mock protected ActivityStarter mActivityStarter; - @Mock protected KeyguardFaceAuthInteractor mKeyguardFaceAuthInteractor; + @Mock protected DeviceEntryFaceAuthInteractor mDeviceEntryFaceAuthInteractor; @Mock private JavaAdapter mJavaAdapter; @Mock private CastController mCastController; @Mock private SharedNotificationContainerInteractor mSharedNotificationContainerInteractor; - @Mock private ActiveNotificationsInteractor mActiveNotificationsInteractor; + @Mock protected ActiveNotificationsInteractor mActiveNotificationsInteractor; @Mock private KeyguardClockPositionAlgorithm mKeyguardClockPositionAlgorithm; @Mock private NaturalScrollingSettingObserver mNaturalScrollingSettingObserver; @@ -382,7 +382,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { @Before public void setup() { MockitoAnnotations.initMocks(this); - mFeatureFlags.set(Flags.WM_SHADE_ANIMATE_BACK_GESTURE, false); mFeatureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false); mFeatureFlags.set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, false); mFeatureFlags.set(Flags.QS_USER_DETAIL_SHORTCUT, false); @@ -730,7 +729,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mActiveNotificationsInteractor, mShadeAnimationInteractor, mKeyguardViewConfigurator, - mKeyguardFaceAuthInteractor, + mDeviceEntryFaceAuthInteractor, new ResourcesSplitShadeStateController(), mPowerInteractor, mKeyguardClockPositionAlgorithm, @@ -800,7 +799,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mInteractionJankMonitor, mShadeLog, mDumpManager, - mKeyguardFaceAuthInteractor, + mDeviceEntryFaceAuthInteractor, mShadeRepository, mShadeInteractor, mActiveNotificationsInteractor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index 28fe8e4e8d3a..2e8d46a83e1c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -456,11 +456,13 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo enableSplitShade(/* enabled= */ true); when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); mNotificationPanelViewController.updateResources(); assertThat(getConstraintSetLayout(R.id.keyguard_status_view).endToEnd) .isEqualTo(R.id.qs_edge_guideline); when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(false); mNotificationPanelViewController.updateResources(); assertThat(getConstraintSetLayout(R.id.keyguard_status_view).endToEnd) .isEqualTo(ConstraintSet.PARENT_ID); @@ -469,6 +471,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo @Test public void keyguardStatusView_splitShade_dozing_alwaysDozingOn_isCentered() { when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); mStatusBarStateController.setState(KEYGUARD); enableSplitShade(/* enabled= */ true); @@ -480,6 +483,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo @Test public void keyguardStatusView_splitShade_dozing_alwaysDozingOff_isNotCentered() { when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); mStatusBarStateController.setState(KEYGUARD); enableSplitShade(/* enabled= */ true); @@ -491,6 +495,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo @Test public void keyguardStatusView_splitShade_notDozing_alwaysDozingOn_isNotCentered() { when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); mStatusBarStateController.setState(KEYGUARD); enableSplitShade(/* enabled= */ true); @@ -502,6 +507,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo @Test public void keyguardStatusView_splitShade_pulsing_isNotCentered() { when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); when(mNotificationListContainer.hasPulsingNotifications()).thenReturn(true); mStatusBarStateController.setState(KEYGUARD); enableSplitShade(/* enabled= */ true); @@ -514,6 +520,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo @Test public void keyguardStatusView_splitShade_notPulsing_isNotCentered() { when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); when(mNotificationListContainer.hasPulsingNotifications()).thenReturn(false); mStatusBarStateController.setState(KEYGUARD); enableSplitShade(/* enabled= */ true); @@ -529,6 +536,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo // The conditions below would make the clock NOT be centered on split shade. // On single shade it should always be centered though. when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); when(mNotificationListContainer.hasPulsingNotifications()).thenReturn(false); mStatusBarStateController.setState(KEYGUARD); setDozing(/* dozing= */ false, /* dozingAlwaysOn= */ false); @@ -539,6 +547,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo @Test public void keyguardStatusView_willPlayDelayedDoze_isCentered_thenNot() { when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); mStatusBarStateController.setState(KEYGUARD); enableSplitShade(/* enabled= */ true); @@ -553,6 +562,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo @Test public void keyguardStatusView_willPlayDelayedDoze_notifiesKeyguardMediaController() { when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); mStatusBarStateController.setState(KEYGUARD); enableSplitShade(/* enabled= */ true); @@ -564,6 +574,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo @Test public void keyguardStatusView_willPlayDelayedDoze_isCentered_thenStillCenteredIfNoNotifs() { when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(false); mStatusBarStateController.setState(KEYGUARD); enableSplitShade(/* enabled= */ true); @@ -700,10 +711,12 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo mStatusBarStateController.setState(KEYGUARD); when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(false); triggerPositionClockAndNotifications(); verify(mKeyguardStatusViewController).displayClock(LARGE, /* animate */ true); when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); triggerPositionClockAndNotifications(); verify(mKeyguardStatusViewController).displayClock(SMALL, /* animate */ true); } @@ -715,10 +728,12 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo clearInvocations(mKeyguardStatusViewController); when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(false); triggerPositionClockAndNotifications(); verify(mKeyguardStatusViewController).displayClock(LARGE, /* animate */ true); when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); triggerPositionClockAndNotifications(); verify(mKeyguardStatusViewController, times(2)) .displayClock(LARGE, /* animate */ true); @@ -730,6 +745,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo public void testHasNotifications_switchesToLargeClockWhenEnteringSplitShade() { mStatusBarStateController.setState(KEYGUARD); when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); enableSplitShade(/* enabled= */ true); @@ -740,6 +756,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo public void testNoNotifications_switchesToLargeClockWhenEnteringSplitShade() { mStatusBarStateController.setState(KEYGUARD); when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(false); enableSplitShade(/* enabled= */ true); @@ -752,6 +769,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo enableSplitShade(/* enabled= */ true); clearInvocations(mKeyguardStatusViewController); when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); enableSplitShade(/* enabled= */ false); @@ -764,6 +782,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo enableSplitShade(/* enabled= */ true); clearInvocations(mKeyguardStatusViewController); when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(false); enableSplitShade(/* enabled= */ false); @@ -777,6 +796,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo enableSplitShade(/* enabled= */ true); when(mMediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true); when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); clearInvocations(mKeyguardStatusViewController); mNotificationPanelViewController.setDozing(/* dozing= */ true, /* animate= */ false); @@ -791,6 +811,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo enableSplitShade(/* enabled= */ true); when(mMediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true); when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); clearInvocations(mKeyguardStatusViewController); mNotificationPanelViewController.setDozing(/* dozing= */ true, /* animate= */ false); @@ -847,6 +868,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo clearInvocations(mKeyguardStatusViewController); when(mMediaDataManager.hasActiveMedia()).thenReturn(true); when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); mNotificationPanelViewController.setDozing(true, false); @@ -863,6 +885,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo when(mResources.getBoolean(R.bool.force_small_clock_on_lockscreen)).thenReturn(true); when(mMediaDataManager.hasActiveMedia()).thenReturn(false); when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(false); clearInvocations(mKeyguardStatusViewController); enableSplitShade(/* enabled= */ true); @@ -881,6 +904,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo when(mResources.getBoolean(R.bool.force_small_clock_on_lockscreen)).thenReturn(true); when(mMediaDataManager.hasActiveMedia()).thenReturn(false); when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(false); clearInvocations(mKeyguardStatusViewController); enableSplitShade(/* enabled= */ true); @@ -898,11 +922,13 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo // one notification + media player visible when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); triggerPositionClockAndNotifications(); verify(mKeyguardStatusViewController).displayClock(SMALL, /* animate */ true); // only media player visible when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0); + when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(false); triggerPositionClockAndNotifications(); verify(mKeyguardStatusViewController, times(2)).displayClock(SMALL, true); verify(mKeyguardStatusViewController, never()).displayClock(LARGE, /* animate */ true); @@ -1094,7 +1120,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo mTouchHandler.onTouch(mock(View.class), mDownMotionEvent); mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0); - verify(mKeyguardFaceAuthInteractor).onNotificationPanelClicked(); + verify(mDeviceEntryFaceAuthInteractor).onNotificationPanelClicked(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index c21addc444b4..ee7c6c89e4a3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -62,7 +62,7 @@ import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsReposi import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.FakeTrustRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl import com.android.systemui.keyguard.shared.model.TransitionStep @@ -257,7 +257,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { FakeTrustRepository(), testScope.backgroundScope, mSelectedUserInteractor, - mock(KeyguardFaceAuthInteractor::class.java) + mock(DeviceEntryFaceAuthInteractor::class.java) ), facePropertyRepository = FakeFacePropertyRepository(), deviceEntryFingerprintAuthRepository = 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 eb5633b70f61..727a6c3d1adc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java @@ -42,6 +42,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository; import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository; import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor; +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FakeFeatureFlagsClassic; @@ -55,7 +56,6 @@ import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnima import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor; import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor; import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor; -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.media.controls.pipeline.MediaDataManager; @@ -378,7 +378,7 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { mInteractionJankMonitor, mShadeLogger, mDumpManager, - mock(KeyguardFaceAuthInteractor.class), + mock(DeviceEntryFaceAuthInteractor.class), mShadeRepository, mShadeInteractor, mActiveNotificationsInteractor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java index 0c6f456b1e80..757f16cac227 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java @@ -598,6 +598,17 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { @Test @EnableFlags(FLAG_KEYGUARD_PRIVATE_NOTIFICATIONS) + public void testEarlyUserSwitch() { + mLockscreenUserManager = + new TestNotificationLockscreenUserManager(mContext); + mBackgroundExecutor.runAllReady(); + mLockscreenUserManager.mUserChangedCallback.onUserChanging( + mCurrentUser.id, mContext); + // no crash! + } + + @Test + @EnableFlags(FLAG_KEYGUARD_PRIVATE_NOTIFICATIONS) public void testKeyguardManager_noPrivateNotifications() { Mockito.clearInvocations(mDevicePolicyManager); // User allows notifications 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 255cf6fbed63..9b4a100a1d64 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 @@ -31,6 +31,7 @@ import com.android.systemui.statusbar.notification.collection.render.NotifStackC import com.android.systemui.statusbar.notification.collection.render.NotifStats import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor +import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT @@ -80,7 +81,7 @@ class StackCoordinatorTest : SysuiTestCase() { } @Test - @DisableFlags(NotificationIconContainerRefactor.FLAG_NAME) + @DisableFlags(NotificationIconContainerRefactor.FLAG_NAME, FooterViewRefactor.FLAG_NAME) fun testUpdateNotificationIcons() { afterRenderListListener.onAfterRenderList(listOf(entry), stackController) verify(notificationIconAreaController).updateNotificationIcons(eq(listOf(entry))) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index e3396364a85a..316f2b99b27b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -334,15 +334,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { public void setup() throws Exception { MockitoAnnotations.initMocks(this); - // CentralSurfacesImpl's runtime flag check fails if the flag is absent. - // This value is unused, because test manifest is opted in. - mFeatureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_SYSUI, false); // Set default value to avoid IllegalStateException. mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, false); - // For the Shade to respond to Back gesture, we must enable the event routing - mFeatureFlags.set(Flags.WM_SHADE_ALLOW_BACK_GESTURE, true); - // For the Shade to animate during the Back gesture, we must enable the animation flag. - mFeatureFlags.set(Flags.WM_SHADE_ANIMATE_BACK_GESTURE, true); mSetFlagsRule.enableFlags(FLAG_LIGHT_REVEAL_MIGRATION); // Turn AOD on and toggle feature flag for jank fixes mFeatureFlags.set(Flags.ZJ_285570694_LOCKSCREEN_TRANSITION_FROM_AOD, true); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index 225ddb6110c2..8dde9359bdfc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -37,6 +37,9 @@ import static org.mockito.Mockito.when; import static kotlinx.coroutines.test.TestCoroutineDispatchersKt.StandardTestDispatcher; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.service.trust.TrustAgentService; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -98,6 +101,7 @@ import com.android.systemui.user.domain.interactor.SelectedUserInteractor; import com.google.common.truth.Truth; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -165,6 +169,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Captor private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallback; + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); @Before public void setUp() { @@ -175,7 +181,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { when(mBouncerView.getDelegate()).thenReturn(mBouncerViewDelegate); when(mBouncerViewDelegate.getBackCallback()).thenReturn(mBouncerViewDelegateBackCallback); mFeatureFlags = new FakeFeatureFlags(); - mFeatureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_BOUNCER_ANIM, true); mFeatureFlags.set(Flags.REFACTOR_KEYGUARD_DISMISS_INTENT, false); mFeatureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false); mSetFlagsRule.disableFlags(com.android.systemui.Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR); @@ -584,6 +589,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { } @Test + @RequiresFlagsEnabled(com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER) public void testPredictiveBackCallback_registration() { /* verify that a predictive back callback is registered when the bouncer becomes visible */ mBouncerExpansionCallback.onVisibilityChanged(true); @@ -598,6 +604,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { } @Test + @RequiresFlagsEnabled(com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER) public void testPredictiveBackCallback_invocationHidesBouncer() { mBouncerExpansionCallback.onVisibilityChanged(true); /* capture the predictive back callback during registration */ @@ -615,6 +622,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { } @Test + @RequiresFlagsEnabled(com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER) public void testPredictiveBackCallback_noBackAnimationForFullScreenBouncer() { when(mKeyguardSecurityModel.getSecurityMode(anyInt())) .thenReturn(KeyguardSecurityModel.SecurityMode.SimPin); @@ -634,6 +642,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { } @Test + @RequiresFlagsEnabled(com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER) public void testPredictiveBackCallback_forwardsBackDispatches() { mBouncerExpansionCallback.onVisibilityChanged(true); /* capture the predictive back callback during registration */ diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt new file mode 100644 index 000000000000..ee2e5addd0e6 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2023 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.unfold + +import android.content.Context +import android.content.res.Resources +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.internal.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.display.data.repository.DeviceStateRepository +import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.power.shared.model.ScreenPowerState +import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.power.shared.model.WakefulnessModel +import com.android.systemui.power.shared.model.WakefulnessState +import com.android.systemui.shared.system.SysUiStatsLog +import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABLE_DEVICE_STATE_CLOSED +import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABLE_DEVICE_STATE_HALF_OPEN +import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent +import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl +import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractorImpl +import com.android.systemui.util.animation.data.repository.AnimationStatusRepository +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import java.util.Optional +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.TestScope +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.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidTestingRunner::class) +@SmallTest +class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { + private lateinit var displaySwitchLatencyTracker: DisplaySwitchLatencyTracker + @Captor private lateinit var loggerArgumentCaptor: ArgumentCaptor<DisplaySwitchLatencyEvent> + + private val mockContext = mock<Context>() + private val resources = mock<Resources>() + private val foldStateRepository = mock<DeviceStateRepository>() + private val powerInteractor = mock<PowerInteractor>() + private val animationStatusRepository = mock<AnimationStatusRepository>() + private val keyguardInteractor = mock<KeyguardInteractor>() + private val displaySwitchLatencyLogger = mock<DisplaySwitchLatencyLogger>() + + private val nonEmptyClosedDeviceStatesArray: IntArray = IntArray(2) { 0 } + private val testDispatcher: TestDispatcher = StandardTestDispatcher() + private val testScope: TestScope = TestScope(testDispatcher) + private val isAsleep = MutableStateFlow(false) + private val isAodAvailable = MutableStateFlow(false) + private val deviceState = MutableStateFlow(DeviceState.UNFOLDED) + private val screenPowerState = MutableStateFlow(ScreenPowerState.SCREEN_ON) + private val areAnimationEnabled = MutableStateFlow(true) + private val lastWakefulnessEvent = MutableStateFlow(WakefulnessModel()) + private val systemClock = FakeSystemClock() + private val unfoldTransitionProgressProvider = TestUnfoldTransitionProvider() + private val unfoldTransitionRepository = + UnfoldTransitionRepositoryImpl(Optional.of(unfoldTransitionProgressProvider)) + private val unfoldTransitionInteractor = + UnfoldTransitionInteractorImpl(unfoldTransitionRepository) + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + whenever(mockContext.resources).thenReturn(resources) + whenever(resources.getIntArray(R.array.config_foldedDeviceStates)) + .thenReturn(nonEmptyClosedDeviceStatesArray) + whenever(foldStateRepository.state).thenReturn(deviceState) + whenever(powerInteractor.isAsleep).thenReturn(isAsleep) + whenever(animationStatusRepository.areAnimationsEnabled()).thenReturn(areAnimationEnabled) + whenever(powerInteractor.screenPowerState).thenReturn(screenPowerState) + whenever(keyguardInteractor.isAodAvailable).thenReturn(isAodAvailable) + whenever(powerInteractor.detailedWakefulness).thenReturn(lastWakefulnessEvent) + + displaySwitchLatencyTracker = + DisplaySwitchLatencyTracker( + mockContext, + foldStateRepository, + powerInteractor, + unfoldTransitionInteractor, + animationStatusRepository, + keyguardInteractor, + testDispatcher.asExecutor(), + testScope.backgroundScope, + displaySwitchLatencyLogger, + systemClock + ) + } + + @Test + fun unfold_logsLatencyTillTransitionStarted() { + testScope.runTest { + areAnimationEnabled.emit(true) + + displaySwitchLatencyTracker.start() + deviceState.emit(DeviceState.FOLDED) + screenPowerState.emit(ScreenPowerState.SCREEN_OFF) + systemClock.advanceTime(50) + runCurrent() + deviceState.emit(DeviceState.HALF_FOLDED) + runCurrent() + systemClock.advanceTime(50) + screenPowerState.emit(ScreenPowerState.SCREEN_ON) + systemClock.advanceTime(200) + unfoldTransitionProgressProvider.onTransitionStarted() + runCurrent() + deviceState.emit(DeviceState.UNFOLDED) + + verify(displaySwitchLatencyLogger).log(capture(loggerArgumentCaptor)) + val loggedEvent = loggerArgumentCaptor.value + val expectedLoggedEvent = + DisplaySwitchLatencyEvent( + latencyMs = 250, + fromFoldableDeviceState = FOLDABLE_DEVICE_STATE_CLOSED, + toFoldableDeviceState = FOLDABLE_DEVICE_STATE_HALF_OPEN + ) + assertThat(loggedEvent).isEqualTo(expectedLoggedEvent) + } + } + + @Test + fun unfold_progressUnavailable_logsLatencyTillScreenTurnedOn() { + testScope.runTest { + val unfoldTransitionInteractorWithEmptyProgressProvider = + UnfoldTransitionInteractorImpl(UnfoldTransitionRepositoryImpl(Optional.empty())) + displaySwitchLatencyTracker = + DisplaySwitchLatencyTracker( + mockContext, + foldStateRepository, + powerInteractor, + unfoldTransitionInteractorWithEmptyProgressProvider, + animationStatusRepository, + keyguardInteractor, + testDispatcher.asExecutor(), + testScope.backgroundScope, + displaySwitchLatencyLogger, + systemClock + ) + areAnimationEnabled.emit(true) + + displaySwitchLatencyTracker.start() + deviceState.emit(DeviceState.FOLDED) + screenPowerState.emit(ScreenPowerState.SCREEN_OFF) + systemClock.advanceTime(50) + runCurrent() + deviceState.emit(DeviceState.HALF_FOLDED) + systemClock.advanceTime(50) + runCurrent() + screenPowerState.emit(ScreenPowerState.SCREEN_ON) + systemClock.advanceTime(50) + runCurrent() + systemClock.advanceTime(200) + unfoldTransitionProgressProvider.onTransitionStarted() + runCurrent() + deviceState.emit(DeviceState.UNFOLDED) + + verify(displaySwitchLatencyLogger).log(capture(loggerArgumentCaptor)) + val loggedEvent = loggerArgumentCaptor.value + val expectedLoggedEvent = + DisplaySwitchLatencyEvent( + latencyMs = 50, + fromFoldableDeviceState = FOLDABLE_DEVICE_STATE_CLOSED, + toFoldableDeviceState = FOLDABLE_DEVICE_STATE_HALF_OPEN + ) + assertThat(loggedEvent).isEqualTo(expectedLoggedEvent) + } + } + + @Test + fun unfold_animationDisabled_logsLatencyTillScreenTurnedOn() { + testScope.runTest { + areAnimationEnabled.emit(false) + + displaySwitchLatencyTracker.start() + deviceState.emit(DeviceState.FOLDED) + screenPowerState.emit(ScreenPowerState.SCREEN_OFF) + systemClock.advanceTime(50) + runCurrent() + deviceState.emit(DeviceState.HALF_FOLDED) + systemClock.advanceTime(50) + runCurrent() + screenPowerState.emit(ScreenPowerState.SCREEN_ON) + systemClock.advanceTime(50) + runCurrent() + unfoldTransitionProgressProvider.onTransitionStarted() + systemClock.advanceTime(200) + runCurrent() + deviceState.emit(DeviceState.UNFOLDED) + + verify(displaySwitchLatencyLogger).log(capture(loggerArgumentCaptor)) + val loggedEvent = loggerArgumentCaptor.value + val expectedLoggedEvent = + DisplaySwitchLatencyEvent( + latencyMs = 50, + fromFoldableDeviceState = FOLDABLE_DEVICE_STATE_CLOSED, + toFoldableDeviceState = FOLDABLE_DEVICE_STATE_HALF_OPEN + ) + assertThat(loggedEvent).isEqualTo(expectedLoggedEvent) + } + } + + @Test + fun foldWhileStayingAwake_logsLatency() { + testScope.runTest { + areAnimationEnabled.emit(true) + deviceState.emit(DeviceState.UNFOLDED) + screenPowerState.emit(ScreenPowerState.SCREEN_ON) + + displaySwitchLatencyTracker.start() + deviceState.emit(DeviceState.HALF_FOLDED) + systemClock.advanceTime(50) + runCurrent() + deviceState.emit(DeviceState.FOLDED) + screenPowerState.emit(ScreenPowerState.SCREEN_OFF) + runCurrent() + systemClock.advanceTime(200) + screenPowerState.emit(ScreenPowerState.SCREEN_ON) + runCurrent() + + verify(displaySwitchLatencyLogger).log(capture(loggerArgumentCaptor)) + val loggedEvent = loggerArgumentCaptor.value + val expectedLoggedEvent = + DisplaySwitchLatencyEvent( + latencyMs = 200, + fromFoldableDeviceState = FOLDABLE_DEVICE_STATE_HALF_OPEN, + toFoldableDeviceState = FOLDABLE_DEVICE_STATE_CLOSED + ) + assertThat(loggedEvent).isEqualTo(expectedLoggedEvent) + } + } + + @Test + fun foldToAod_capturesToStateAsAod() { + testScope.runTest { + areAnimationEnabled.emit(true) + deviceState.emit(DeviceState.UNFOLDED) + isAodAvailable.emit(true) + + displaySwitchLatencyTracker.start() + deviceState.emit(DeviceState.HALF_FOLDED) + systemClock.advanceTime(50) + runCurrent() + deviceState.emit(DeviceState.FOLDED) + lastWakefulnessEvent.emit( + WakefulnessModel( + internalWakefulnessState = WakefulnessState.ASLEEP, + lastSleepReason = WakeSleepReason.FOLD + ) + ) + screenPowerState.emit(ScreenPowerState.SCREEN_OFF) + runCurrent() + systemClock.advanceTime(200) + screenPowerState.emit(ScreenPowerState.SCREEN_ON) + runCurrent() + + verify(displaySwitchLatencyLogger).log(capture(loggerArgumentCaptor)) + val loggedEvent = loggerArgumentCaptor.value + val expectedLoggedEvent = + DisplaySwitchLatencyEvent( + latencyMs = 200, + fromFoldableDeviceState = FOLDABLE_DEVICE_STATE_HALF_OPEN, + toFoldableDeviceState = FOLDABLE_DEVICE_STATE_CLOSED, + toState = SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__AOD + ) + assertThat(loggedEvent).isEqualTo(expectedLoggedEvent) + } + } + + @Test + fun fold_notAFoldable_shouldNotLogLatency() { + testScope.runTest { + areAnimationEnabled.emit(true) + deviceState.emit(DeviceState.UNFOLDED) + whenever(resources.getIntArray(R.array.config_foldedDeviceStates)) + .thenReturn(IntArray(0)) + + displaySwitchLatencyTracker.start() + deviceState.emit(DeviceState.HALF_FOLDED) + systemClock.advanceTime(50) + runCurrent() + deviceState.emit(DeviceState.FOLDED) + screenPowerState.emit(ScreenPowerState.SCREEN_OFF) + runCurrent() + systemClock.advanceTime(200) + screenPowerState.emit(ScreenPowerState.SCREEN_ON) + runCurrent() + + verify(displaySwitchLatencyLogger, never()).log(any()) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/FoldStateRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldStateRepositoryTest.kt index 065132300564..ab779a75518e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/FoldStateRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldStateRepositoryTest.kt @@ -13,13 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.unfold.updates +package com.android.systemui.unfold import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.unfold.updates.FoldStateRepository.FoldUpdate +import com.android.systemui.unfold.data.repository.FoldStateRepository.FoldUpdate +import com.android.systemui.unfold.data.repository.FoldStateRepositoryImpl +import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED +import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN +import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_HALF_OPEN +import com.android.systemui.unfold.updates.FOLD_UPDATE_START_CLOSING +import com.android.systemui.unfold.updates.FOLD_UPDATE_START_OPENING +import com.android.systemui.unfold.updates.FoldStateProvider import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.mock diff --git a/packages/SystemUI/tests/utils/src/com/android/keyguard/TrustManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/keyguard/TrustManagerKosmos.kt new file mode 100644 index 000000000000..187935bb26c4 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/keyguard/TrustManagerKosmos.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.keyguard + +import android.app.trust.TrustManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +val Kosmos.trustManager by Kosmos.Fixture { mock<TrustManager>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java index 29e737eac99b..d23dae9c762c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java @@ -33,6 +33,7 @@ import android.testing.TestWithLooperRule; import android.testing.TestableLooper; import android.util.Log; +import androidx.annotation.NonNull; import androidx.core.animation.AndroidXAnimatorIsolationRule; import androidx.test.InstrumentationRegistry; import androidx.test.uiautomator.UiDevice; @@ -68,8 +69,20 @@ public abstract class SysuiTestCase { public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); @Rule - public SysuiTestableContext mContext = new SysuiTestableContext( - InstrumentationRegistry.getContext(), getLeakCheck()); + public SysuiTestableContext mContext = createTestableContext(); + + @NonNull + private SysuiTestableContext createTestableContext() { + SysuiTestableContext context = new SysuiTestableContext( + InstrumentationRegistry.getContext(), getLeakCheck()); + if (isRobolectricTest()) { + // Manually associate a Display to context for Robolectric test. Similar to b/214297409 + return context.createDefaultDisplayContext(); + } else { + return context; + } + } + @Rule public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule = new DexmakerShareClassLoaderRule(); @@ -84,10 +97,6 @@ public abstract class SysuiTestCase { @Before public void SysuiSetup() throws Exception { - // Manually associate a Display to context for Robolectric test. Similar to b/214297409 - if (isRobolectricTest()) { - mContext = mContext.createDefaultDisplayContext(); - } mSysuiDependency = new SysuiTestDependency(mContext, shouldFailOnLeakedReceiver()); mDependency = mSysuiDependency.install(); mRealInstrumentation = InstrumentationRegistry.getInstrumentation(); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt new file mode 100644 index 000000000000..6ef74194fd85 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt @@ -0,0 +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 com.android.systemui.biometrics.data.repository + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture + +val Kosmos.facePropertyRepository by Fixture { FakeFacePropertyRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt new file mode 100644 index 000000000000..60f0448e89b3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2023 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.common.data.repository + +import android.os.UserHandle +import com.android.systemui.common.data.shared.model.PackageChangeModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.filter + +class FakePackageChangeRepository : PackageChangeRepository { + + private var _packageChanged = MutableSharedFlow<PackageChangeModel>() + + override fun packageChanged(user: UserHandle) = + _packageChanged.filter { + user == UserHandle.ALL || user == UserHandle.getUserHandleForUid(it.packageUid) + } + + suspend fun notifyChange(model: PackageChangeModel) { + _packageChanged.emit(model) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/PackageChangeRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/PackageChangeRepositoryKosmos.kt new file mode 100644 index 000000000000..adc05e0ae6a2 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/PackageChangeRepositoryKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 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.common.data.repository + +import com.android.systemui.kosmos.Kosmos + +var Kosmos.packageChangeRepository: PackageChangeRepository by + Kosmos.Fixture { fakePackageChangeRepository } +val Kosmos.fakePackageChangeRepository by Kosmos.Fixture { FakePackageChangeRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FaceWakeUpTriggersConfigKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FaceWakeUpTriggersConfigKosmos.kt new file mode 100644 index 000000000000..21cff0dbde2d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FaceWakeUpTriggersConfigKosmos.kt @@ -0,0 +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 com.android.systemui.deviceentry.data.repository + +import com.android.systemui.kosmos.Kosmos + +var Kosmos.faceWakeUpTriggersConfig: FaceWakeUpTriggersConfig by + Kosmos.Fixture { FakeFaceWakeUpTriggersConfig() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeFaceWakeUpTriggersConfig.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeFaceWakeUpTriggersConfig.kt new file mode 100644 index 000000000000..af617b741df3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeFaceWakeUpTriggersConfig.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.deviceentry.data.repository + +import com.android.systemui.power.shared.model.WakeSleepReason + +class FakeFaceWakeUpTriggersConfig : FaceWakeUpTriggersConfig { + private val triggerFaceAuthOnWakeUpFrom: MutableSet<Int> = mutableSetOf() + private val wakeSleepReasonsToTriggerFaceAuth: MutableSet<WakeSleepReason> = mutableSetOf() + override fun shouldTriggerFaceAuthOnWakeUpFrom(pmWakeReason: Int): Boolean { + return triggerFaceAuthOnWakeUpFrom.contains(pmWakeReason) + } + + override fun shouldTriggerFaceAuthOnWakeUpFrom(wakeReason: WakeSleepReason): Boolean { + return wakeSleepReasonsToTriggerFaceAuth.contains(wakeReason) + } + + fun setTriggerFaceAuthOnWakeUpFrom(pmWakeReasons: Set<Int>) { + triggerFaceAuthOnWakeUpFrom.clear() + triggerFaceAuthOnWakeUpFrom.addAll(pmWakeReasons) + + wakeSleepReasonsToTriggerFaceAuth.clear() + wakeSleepReasonsToTriggerFaceAuth.addAll( + pmWakeReasons.map { WakeSleepReason.fromPowerManagerWakeReason(it) } + ) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt index d2dff7834b40..0b1fb4074226 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt @@ -18,13 +18,45 @@ package com.android.systemui.deviceentry.domain.interactor +import android.content.applicationContext +import com.android.keyguard.keyguardUpdateMonitor +import com.android.keyguard.trustManager +import com.android.systemui.biometrics.data.repository.facePropertyRepository +import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor +import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor +import com.android.systemui.deviceentry.data.repository.faceWakeUpTriggersConfig +import com.android.systemui.keyguard.data.repository.biometricSettingsRepository import com.android.systemui.keyguard.data.repository.deviceEntryFaceAuthRepository +import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.log.FaceAuthenticationLogger +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.user.data.repository.userRepository +import com.android.systemui.util.mockito.mock import kotlinx.coroutines.ExperimentalCoroutinesApi +val Kosmos.faceAuthLogger by Kosmos.Fixture { mock<FaceAuthenticationLogger>() } val Kosmos.deviceEntryFaceAuthInteractor by Kosmos.Fixture { - DeviceEntryFaceAuthInteractor( + SystemUIDeviceEntryFaceAuthInteractor( + context = applicationContext, + applicationScope = applicationCoroutineScope, + mainDispatcher = testDispatcher, repository = deviceEntryFaceAuthRepository, + primaryBouncerInteractor = { primaryBouncerInteractor }, + alternateBouncerInteractor = alternateBouncerInteractor, + keyguardTransitionInteractor = keyguardTransitionInteractor, + faceAuthenticationLogger = faceAuthLogger, + keyguardUpdateMonitor = keyguardUpdateMonitor, + deviceEntryFingerprintAuthRepository = deviceEntryFingerprintAuthRepository, + userRepository = userRepository, + facePropertyRepository = facePropertyRepository, + faceWakeUpTriggersConfig = faceWakeUpTriggersConfig, + powerInteractor = powerInteractor, + biometricSettingsRepository = biometricSettingsRepository, + trustManager = trustManager, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryKosmos.kt index 3d729675f7ab..599b51422cdb 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.data.repository +import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository import com.android.systemui.kosmos.Kosmos var Kosmos.deviceEntryFaceAuthRepository: DeviceEntryFaceAuthRepository by diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt index a1b6587be0b8..e96aeada0212 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt @@ -16,10 +16,11 @@ package com.android.systemui.keyguard.data.repository -import com.android.keyguard.FaceAuthUiEvent import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus -import com.android.systemui.keyguard.shared.model.FaceDetectionStatus +import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository +import com.android.systemui.deviceentry.shared.FaceAuthUiEvent +import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.FaceDetectionStatus import dagger.Binds import dagger.Module import javax.inject.Inject diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 4200f05ad64b..975db3b179ac 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -80,7 +80,7 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { override val lastDozeTapToWakePosition = _lastDozeTapToWakePosition.asStateFlow() private val _isAodAvailable = MutableStateFlow(false) - override val isAodAvailable: Flow<Boolean> = _isAodAvailable + override val isAodAvailable: StateFlow<Boolean> = _isAodAvailable private val _isDreaming = MutableStateFlow(false) override val isDreaming: Flow<Boolean> = _isDreaming diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt index 3cabf0c07423..5f5d42850619 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt @@ -27,6 +27,7 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInte import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.bouncer.ui.BouncerView import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository @@ -73,7 +74,7 @@ object KeyguardDismissInteractorFactory { trustRepository, testScope.backgroundScope, mock(SelectedUserInteractor::class.java), - mock(KeyguardFaceAuthInteractor::class.java), + mock(DeviceEntryFaceAuthInteractor::class.java), ) val alternateBouncerInteractor = AlternateBouncerInteractor( diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt index d8786830f536..5ca0439c1313 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt @@ -20,6 +20,7 @@ import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.statusbar.policy.splitShadeStateController val Kosmos.keyguardClockViewModel by Kosmos.Fixture { @@ -27,5 +28,6 @@ val Kosmos.keyguardClockViewModel by keyguardInteractor = keyguardInteractor, keyguardClockInteractor = keyguardClockInteractor, applicationScope = applicationCoroutineScope, + splitShadeStateController = splitShadeStateController, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt index d70524840145..14f28fedc5e4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt @@ -33,6 +33,7 @@ val Kosmos.customTileStatePersister: FakeCustomTileStatePersister by val Kosmos.customTileRepository: FakeCustomTileRepository by Kosmos.Fixture { FakeCustomTileRepository( + tileSpec, customTileStatePersister, packageManagerAdapterFacade, testScope.testScheduler, @@ -46,4 +47,4 @@ val Kosmos.customTilePackagesUpdatesRepository: FakeCustomTilePackageUpdatesRepo Kosmos.Fixture { FakeCustomTilePackageUpdatesRepository() } val Kosmos.packageManagerAdapterFacade: FakePackageManagerAdapterFacade by - Kosmos.Fixture { FakePackageManagerAdapterFacade(tileSpec) } + Kosmos.Fixture { FakePackageManagerAdapterFacade(tileSpec.componentName) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt index ba803d8baef0..c110da057a4e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt @@ -19,11 +19,13 @@ package com.android.systemui.qs.tiles.impl.custom.data.repository import android.os.UserHandle import android.service.quicksettings.Tile import com.android.systemui.qs.external.FakeCustomTileStatePersister +import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.tiles.impl.custom.data.entity.CustomTileDefaults import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.flow.Flow class FakeCustomTileRepository( + tileSpec: TileSpec.CustomTileSpec, customTileStatePersister: FakeCustomTileStatePersister, private val packageManagerAdapterFacade: FakePackageManagerAdapterFacade, testBackgroundContext: CoroutineContext, @@ -31,12 +33,16 @@ class FakeCustomTileRepository( private val realDelegate: CustomTileRepository = CustomTileRepositoryImpl( - packageManagerAdapterFacade.tileSpec, + tileSpec, customTileStatePersister, packageManagerAdapterFacade.packageManagerAdapter, testBackgroundContext, ) + init { + require(tileSpec.componentName == packageManagerAdapterFacade.componentName) + } + override suspend fun restoreForTheUserIfNeeded(user: UserHandle, isPersistable: Boolean) = realDelegate.restoreForTheUserIfNeeded(user, isPersistable) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt index c9a7655b5571..634d121a556c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt @@ -16,17 +16,27 @@ package com.android.systemui.qs.tiles.impl.custom.data.repository +import android.content.ComponentName import android.content.pm.ServiceInfo import android.os.Bundle import com.android.systemui.qs.external.PackageManagerAdapter -import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +/** + * Facade for [PackageManagerAdapter] to provide a fake-like behaviour. You can create this class + * and then get [packageManagerAdapter] to use in your test code. + * + * This allows to mock [PackageManagerAdapter] to provide a custom behaviour for + * [CustomTileRepository.isTileActive], [CustomTileRepository.isTileToggleable], + * [com.android.systemui.qs.external.TileServiceManager.isToggleableTile] or + * [com.android.systemui.qs.external.TileServiceManager.isActiveTile] when the real objects are + * used. + */ class FakePackageManagerAdapterFacade( - val tileSpec: TileSpec.CustomTileSpec, + val componentName: ComponentName, val packageManagerAdapter: PackageManagerAdapter = mock {}, ) { @@ -34,22 +44,21 @@ class FakePackageManagerAdapterFacade( private var isActive: Boolean = false init { - whenever(packageManagerAdapter.getServiceInfo(eq(tileSpec.componentName), any())) - .thenAnswer { - ServiceInfo().apply { - metaData = - Bundle().apply { - putBoolean( - android.service.quicksettings.TileService.META_DATA_TOGGLEABLE_TILE, - isToggleable - ) - putBoolean( - android.service.quicksettings.TileService.META_DATA_ACTIVE_TILE, - isActive - ) - } - } + whenever(packageManagerAdapter.getServiceInfo(eq(componentName), any())).thenAnswer { + ServiceInfo().apply { + metaData = + Bundle().apply { + putBoolean( + android.service.quicksettings.TileService.META_DATA_TOGGLEABLE_TILE, + isToggleable + ) + putBoolean( + android.service.quicksettings.TileService.META_DATA_ACTIVE_TILE, + isActive + ) + } } + } } fun setIsActive(isActive: Boolean) { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt index 9f71161e4452..09ab6557c663 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt @@ -52,20 +52,20 @@ import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.communal.data.repository.FakeCommunalRepository import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory +import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository 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.doze.DozeLogger import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags -import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository import com.android.systemui.keyguard.data.repository.FakeCommandQueue import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.FakeTrustRepository import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.data.repository.TrustRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.kosmos.Kosmos @@ -266,14 +266,14 @@ class SceneTestUtils( fun bouncerInteractor( authenticationInteractor: AuthenticationInteractor, - keyguardFaceAuthInteractor: KeyguardFaceAuthInteractor = mock(), + deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor = mock(), ): BouncerInteractor { return BouncerInteractor( applicationScope = applicationScope(), applicationContext = context, repository = bouncerRepository, authenticationInteractor = authenticationInteractor, - keyguardFaceAuthInteractor = keyguardFaceAuthInteractor, + deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor, falsingInteractor = falsingInteractor(), powerInteractor = powerInteractor(), simBouncerInteractor = simBouncerInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt index 0c9ce0f145f1..697b5087a865 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt @@ -17,6 +17,7 @@ package com.android.systemui.util +import android.content.Context import android.content.DialogInterface import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.util.mockito.any @@ -27,13 +28,15 @@ import org.mockito.Mockito.doAnswer import org.mockito.Mockito.verify import org.mockito.stubbing.Stubber -class FakeSystemUIDialogController { +class FakeSystemUIDialogController(context: Context) { val dialog: SystemUIDialog = mock() + private val clickListeners: MutableMap<Int, DialogInterface.OnClickListener> = mutableMapOf() init { + whenever(dialog.context).thenReturn(context) saveListener(DialogInterface.BUTTON_POSITIVE) .whenever(dialog) .setPositiveButton(any(), any()) diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt index f7fb01465a40..1b7e71a42c22 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt @@ -27,8 +27,6 @@ import com.android.systemui.unfold.progress.PhysicsBasedUnfoldTransitionProgress import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder import com.android.systemui.unfold.updates.DeviceFoldStateProvider import com.android.systemui.unfold.updates.FoldStateProvider -import com.android.systemui.unfold.updates.FoldStateRepository -import com.android.systemui.unfold.updates.FoldStateRepositoryImpl import com.android.systemui.unfold.updates.RotationChangeProvider import com.android.systemui.unfold.updates.hinge.EmptyHingeAngleProvider import com.android.systemui.unfold.updates.hinge.HingeAngleProvider @@ -68,10 +66,6 @@ class UnfoldSharedModule { fun unfoldKeyguardVisibilityManager( impl: UnfoldKeyguardVisibilityManagerImpl ): UnfoldKeyguardVisibilityManager = impl - - @Provides - @Singleton - fun foldStateRepository(impl: FoldStateRepositoryImpl): FoldStateRepository = impl } @Module diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/UnfoldRemoteFilter.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/UnfoldRemoteFilter.kt index 843cc3b78031..54d805409c51 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/UnfoldRemoteFilter.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/UnfoldRemoteFilter.kt @@ -41,8 +41,6 @@ class UnfoldRemoteFilter( if (inProgress) { logCounter({ "$TAG#filtered_progress" }, newProgress) listener.onTransitionProgress(newProgress) - } else { - Log.e(TAG, "Filtered progress received received while animation not in progress.") } field = newProgress } diff --git a/packages/overlays/Android.bp b/packages/overlays/Android.bp new file mode 100644 index 000000000000..5e001fba6aa1 --- /dev/null +++ b/packages/overlays/Android.bp @@ -0,0 +1,44 @@ +// Copyright (C) 2019 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 { + // See: http://go/android-license-faq + default_applicable_licenses: [ + "frameworks_base_license", + ], +} + +phony { + name: "frameworks-base-overlays", + required: [ + "DisplayCutoutEmulationCornerOverlay", + "DisplayCutoutEmulationDoubleOverlay", + "DisplayCutoutEmulationHoleOverlay", + "DisplayCutoutEmulationTallOverlay", + "DisplayCutoutEmulationWaterfallOverlay", + "FontNotoSerifSourceOverlay", + "NavigationBarMode3ButtonOverlay", + "NavigationBarModeGesturalOverlay", + "NavigationBarModeGesturalOverlayNarrowBack", + "NavigationBarModeGesturalOverlayWideBack", + "NavigationBarModeGesturalOverlayExtraWideBack", + "TransparentNavigationBarOverlay", + "NotesRoleEnabledOverlay", + "preinstalled-packages-platform-overlays.xml", + ], +} + +phony { + name: "frameworks-base-overlays-debug", +} diff --git a/packages/overlays/Android.mk b/packages/overlays/Android.mk deleted file mode 100644 index a41d0e57cd21..000000000000 --- a/packages/overlays/Android.mk +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (C) 2019 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. - -LOCAL_PATH:= $(call my-dir) -include $(CLEAR_VARS) - -LOCAL_MODULE := frameworks-base-overlays -LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0 -LOCAL_LICENSE_CONDITIONS := notice -LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../NOTICE -LOCAL_REQUIRED_MODULES := \ - DisplayCutoutEmulationCornerOverlay \ - DisplayCutoutEmulationDoubleOverlay \ - DisplayCutoutEmulationHoleOverlay \ - DisplayCutoutEmulationTallOverlay \ - DisplayCutoutEmulationWaterfallOverlay \ - FontNotoSerifSourceOverlay \ - NavigationBarMode3ButtonOverlay \ - NavigationBarModeGesturalOverlay \ - NavigationBarModeGesturalOverlayNarrowBack \ - NavigationBarModeGesturalOverlayWideBack \ - NavigationBarModeGesturalOverlayExtraWideBack \ - TransparentNavigationBarOverlay \ - NotesRoleEnabledOverlay \ - preinstalled-packages-platform-overlays.xml - -include $(BUILD_PHONY_PACKAGE) -include $(CLEAR_VARS) - -LOCAL_MODULE := frameworks-base-overlays-debug -LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0 -LOCAL_LICENSE_CONDITIONS := notice -LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../NOTICE - -include $(BUILD_PHONY_PACKAGE) -include $(call first-makefiles-under,$(LOCAL_PATH)) diff --git a/services/Android.bp b/services/Android.bp index 5cb8ec628c38..0b484f473d36 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -148,6 +148,9 @@ filegroup { java_library { name: "Slogf", srcs: ["core/java/com/android/server/utils/Slogf.java"], + lint: { + baseline_filename: "lint-baseline.xml", + }, } // merge all required services into one jar @@ -220,6 +223,9 @@ java_library { required: [ "libukey2_jni_shared", ], + lint: { + baseline_filename: "lint-baseline.xml", + }, // Uncomment to enable output of certain warnings (deprecated, unchecked) //javacflags: ["-Xlint"], diff --git a/services/accessibility/Android.bp b/services/accessibility/Android.bp index e2488a51bba1..a3546716d5ca 100644 --- a/services/accessibility/Android.bp +++ b/services/accessibility/Android.bp @@ -21,6 +21,7 @@ java_library_static { ], lint: { error_checks: ["MissingPermissionAnnotation"], + baseline_filename: "lint-baseline.xml", }, srcs: [ ":services.accessibility-sources", @@ -49,6 +50,9 @@ java_library_static { libs: [ "androidx.annotation_annotation", ], + lint: { + baseline_filename: "lint-baseline.xml", + }, } aconfig_declarations { diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig index a19920f4fc02..993b2544f110 100644 --- a/services/accessibility/accessibility.aconfig +++ b/services/accessibility/accessibility.aconfig @@ -59,13 +59,6 @@ flag { } flag { - name: "reduce_touch_exploration_sensitivity" - namespace: "accessibility" - description: "Reduces touch exploration sensitivity by only sending a hover event when the ifnger has moved the amount of pixels defined by the system's touch slop." - bug: "303677860" -} - -flag { name: "scan_packages_without_lock" namespace: "accessibility" description: "Scans packages for accessibility service/activity info without holding the A11yMS lock" diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java index fc8d4f89e6a7..c4184854e690 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java @@ -882,22 +882,10 @@ public class TouchExplorer extends BaseEventStreamTransformation final int pointerIndex = event.findPointerIndex(pointerId); switch (event.getPointerCount()) { case 1: - // Touch exploration. + // Touch exploration. sendTouchExplorationGestureStartAndHoverEnterIfNeeded(policyFlags); - if (Flags.reduceTouchExplorationSensitivity() - && mState.getLastInjectedHoverEvent() != null) { - final MotionEvent lastEvent = mState.getLastInjectedHoverEvent(); - final float deltaX = lastEvent.getX() - rawEvent.getX(); - final float deltaY = lastEvent.getY() - rawEvent.getY(); - final double moveDelta = Math.hypot(deltaX, deltaY); - if (moveDelta > mTouchSlop) { - mDispatcher.sendMotionEvent( - event, ACTION_HOVER_MOVE, rawEvent, pointerIdBits, policyFlags); - } - } else { - mDispatcher.sendMotionEvent( - event, ACTION_HOVER_MOVE, rawEvent, pointerIdBits, policyFlags); - } + mDispatcher.sendMotionEvent( + event, ACTION_HOVER_MOVE, rawEvent, pointerIdBits, policyFlags); break; case 2: if (mGestureDetector.isMultiFingerGesturesEnabled() diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java index 6b0fdb5f7fe0..cf414d145db1 100644 --- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java +++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java @@ -240,10 +240,20 @@ public final class PresentationStatsEventLogger { mEventInternal = Optional.of(new PresentationStatsEventInternal()); } + /** + * Set request_id + */ public void maybeSetRequestId(int requestId) { mEventInternal.ifPresent(event -> event.mRequestId = requestId); } + /** + * Set is_credential_request + */ + public void maybeSetIsCredentialRequest(boolean isCredentialRequest) { + mEventInternal.ifPresent(event -> event.mIsCredentialRequest = isCredentialRequest); + } + public void maybeSetNoPresentationEventReason(@NotShownReason int reason) { mEventInternal.ifPresent(event -> { if (event.mCountShown == 0) { @@ -567,7 +577,8 @@ public final class PresentationStatsEventLogger { + " mSelectedDatasetPickedReason=" + event.mSelectedDatasetPickedReason + " mDetectionPreference=" + event.mDetectionPreference + " mFieldClassificationRequestId=" + event.mFieldClassificationRequestId - + " mAppPackageUid=" + mCallingAppUid); + + " mAppPackageUid=" + mCallingAppUid + + " mIsCredentialRequest=" + event.mIsCredentialRequest); } // TODO(b/234185326): Distinguish empty responses from other no presentation reasons. @@ -606,7 +617,8 @@ public final class PresentationStatsEventLogger { event.mSelectedDatasetPickedReason, event.mDetectionPreference, event.mFieldClassificationRequestId, - mCallingAppUid); + mCallingAppUid, + event.mIsCredentialRequest); mEventInternal = Optional.empty(); } @@ -640,6 +652,7 @@ public final class PresentationStatsEventLogger { @DatasetPickedReason int mSelectedDatasetPickedReason = PICK_REASON_UNKNOWN; @DetectionPreference int mDetectionPreference = DETECTION_PREFER_UNKNOWN; int mFieldClassificationRequestId = -1; + boolean mIsCredentialRequest = false; PresentationStatsEventInternal() {} } diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index a49f9dbe7d91..007be05ff929 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -1290,7 +1290,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState Slog.v(TAG, "Requesting structure for request #" + ordinal + " ,requestId=" + requestId + ", flags=" + flags); } + boolean isCredmanRequested = (flags & FLAG_VIEW_REQUESTS_CREDMAN_SERVICE) != 0; mPresentationStatsEventLogger.maybeSetRequestId(requestId); + mPresentationStatsEventLogger.maybeSetIsCredentialRequest(isCredmanRequested); mPresentationStatsEventLogger.maybeSetFieldClassificationRequestId( mFieldClassificationIdSnapshot); mFillRequestEventLogger.maybeSetRequestId(requestId); @@ -4360,8 +4362,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } if (viewState.getResponse() != null) { + boolean isCredmanRequested = (flags & FLAG_VIEW_REQUESTS_CREDMAN_SERVICE) != 0; FillResponse response = viewState.getResponse(); mPresentationStatsEventLogger.maybeSetRequestId(response.getRequestId()); + mPresentationStatsEventLogger.maybeSetIsCredentialRequest(isCredmanRequested); mPresentationStatsEventLogger.maybeSetFieldClassificationRequestId( mFieldClassificationIdSnapshot); mPresentationStatsEventLogger.maybeSetAvailableCount( diff --git a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java index 52a8f9ed10d4..a6ed8464128a 100644 --- a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java +++ b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java @@ -286,32 +286,33 @@ class AssociationRequestsProcessor { selfManaged, /* notifyOnDeviceNearby */ false, /* revoked */ false, timestamp, Long.MAX_VALUE, /* systemDataSyncFlags */ 0); - if (deviceProfile != null) { - // If the "Device Profile" is specified, make the companion application a holder of the - // corresponding role. - addRoleHolderForAssociation(mService.getContext(), association, success -> { - if (success) { - addAssociationToStore(association, deviceProfile); - - sendCallbackAndFinish(association, callback, resultReceiver); - } else { - Slog.e(TAG, "Failed to add u" + userId + "\\" + packageName - + " to the list of " + deviceProfile + " holders."); - - sendCallbackAndFinish(null, callback, resultReceiver); - } - }); - } else { - addAssociationToStore(association, null); - - sendCallbackAndFinish(association, callback, resultReceiver); - } + // Add role holder for association (if specified) and add new association to store. + maybeGrantRoleAndStoreAssociation(association, callback, resultReceiver); // Don't need to update the mRevokedAssociationsPendingRoleHolderRemoval since // maybeRemoveRoleHolderForAssociation in PackageInactivityListener will handle the case // that there are other devices with the same profile, so the role holder won't be removed. } + public void maybeGrantRoleAndStoreAssociation(@NonNull AssociationInfo association, + @Nullable IAssociationRequestCallback callback, + @Nullable ResultReceiver resultReceiver) { + // If the "Device Profile" is specified, make the companion application a holder of the + // corresponding role. + // If it is null, then the operation will succeed without granting any role. + addRoleHolderForAssociation(mService.getContext(), association, success -> { + if (success) { + addAssociationToStore(association); + sendCallbackAndFinish(association, callback, resultReceiver); + } else { + Slog.e(TAG, "Failed to add u" + association.getUserId() + + "\\" + association.getPackageName() + + " to the list of " + association.getDeviceProfile() + " holders."); + sendCallbackAndFinish(null, callback, resultReceiver); + } + }); + } + public void enableSystemDataSync(int associationId, int flags) { AssociationInfo association = mAssociationStore.getAssociationById(associationId); AssociationInfo updated = (new AssociationInfo.Builder(association)) @@ -326,15 +327,14 @@ class AssociationRequestsProcessor { mAssociationStore.updateAssociation(updated); } - private void addAssociationToStore(@NonNull AssociationInfo association, - @Nullable String deviceProfile) { + private void addAssociationToStore(@NonNull AssociationInfo association) { Slog.i(TAG, "New CDM association created=" + association); mAssociationStore.addAssociation(association); mService.updateSpecialAccessPermissionForAssociatedPackage(association); - logCreateAssociation(deviceProfile); + logCreateAssociation(association.getDeviceProfile()); } private void sendCallbackAndFinish(@Nullable AssociationInfo association, diff --git a/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java b/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java new file mode 100644 index 000000000000..a7dbd1c15aec --- /dev/null +++ b/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java @@ -0,0 +1,241 @@ +/* + * 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.companion; + +import static android.os.UserHandle.getCallingUserId; + +import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.annotation.UserIdInt; +import android.companion.AssociationInfo; +import android.companion.Flags; +import android.companion.datatransfer.SystemDataTransferRequest; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManagerInternal; +import android.util.Log; +import android.util.Slog; + +import com.android.internal.util.CollectionUtils; +import com.android.server.companion.datatransfer.SystemDataTransferRequestStore; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; + +@SuppressLint("LongLogTag") +class BackupRestoreProcessor { + static final String TAG = "CDM_BackupRestoreProcessor"; + private static final int BACKUP_AND_RESTORE_VERSION = 0; + + @NonNull + private final CompanionDeviceManagerService mService; + @NonNull + private final PackageManagerInternal mPackageManager; + @NonNull + private final AssociationStoreImpl mAssociationStore; + @NonNull + private final PersistentDataStore mPersistentStore; + @NonNull + private final SystemDataTransferRequestStore mSystemDataTransferRequestStore; + @NonNull + private final AssociationRequestsProcessor mAssociationRequestsProcessor; + + BackupRestoreProcessor(@NonNull CompanionDeviceManagerService service, + @NonNull AssociationStoreImpl associationStore, + @NonNull PersistentDataStore persistentStore, + @NonNull SystemDataTransferRequestStore systemDataTransferRequestStore, + @NonNull AssociationRequestsProcessor associationRequestsProcessor) { + mService = service; + mPackageManager = service.mPackageManagerInternal; + mAssociationStore = associationStore; + mPersistentStore = persistentStore; + mSystemDataTransferRequestStore = systemDataTransferRequestStore; + mAssociationRequestsProcessor = associationRequestsProcessor; + } + + /** + * Generate CDM state payload to be backed up. + * Backup payload is formatted as following: + * | (4) payload version | (4) AssociationInfo length | AssociationInfo XML + * | (4) SystemDataTransferRequest length | SystemDataTransferRequest XML (without userId)| + */ + byte[] getBackupPayload(int userId) { + // Persist state first to generate an up-to-date XML file + mService.persistStateForUser(userId); + byte[] associationsPayload = mPersistentStore.getBackupPayload(userId); + int associationsPayloadLength = associationsPayload.length; + + // System data transfer requests are persisted up-to-date already + byte[] requestsPayload = mSystemDataTransferRequestStore.getBackupPayload(userId); + int requestsPayloadLength = requestsPayload.length; + + int payloadSize = /* 3 integers */ 12 + + associationsPayloadLength + + requestsPayloadLength; + + return ByteBuffer.allocate(payloadSize) + .putInt(BACKUP_AND_RESTORE_VERSION) + .putInt(associationsPayloadLength) + .put(associationsPayload) + .putInt(requestsPayloadLength) + .put(requestsPayload) + .array(); + } + + /** + * Create new associations and system data transfer request consents using backed up payload. + */ + void applyRestoredPayload(byte[] payload, int userId) { + ByteBuffer buffer = ByteBuffer.wrap(payload); + + // Make sure that payload version matches current version to ensure proper deserialization + int version = buffer.getInt(); + if (version != BACKUP_AND_RESTORE_VERSION) { + Slog.e(TAG, "Unsupported backup payload version"); + return; + } + + // Read the bytes containing backed-up associations + byte[] associationsPayload = new byte[buffer.getInt()]; + buffer.get(associationsPayload); + final Set<AssociationInfo> restoredAssociations = new HashSet<>(); + mPersistentStore.readStateFromPayload(associationsPayload, userId, + restoredAssociations, new HashMap<>()); + + // Read the bytes containing backed-up system data transfer requests user consent + byte[] requestsPayload = new byte[buffer.getInt()]; + buffer.get(requestsPayload); + List<SystemDataTransferRequest> restoredRequestsForUser = + mSystemDataTransferRequestStore.readRequestsFromPayload(requestsPayload); + + // Get a list of installed packages ahead of time. + List<ApplicationInfo> installedApps = mPackageManager.getInstalledApplications( + 0, userId, getCallingUserId()); + + // Restored device may have a different user ID than the backed-up user's user-ID. Since + // association ID is dependent on the user ID, restored associations must account for + // this potential difference on their association IDs. + for (AssociationInfo restored : restoredAssociations) { + // Don't restore a revoked association. Since they weren't added to the device being + // restored in the first place, there is no need to worry about revoking a role that + // was never granted either. + if (restored.isRevoked()) { + continue; + } + + // Filter restored requests for those that belong to the restored association. + List<SystemDataTransferRequest> restoredRequests = CollectionUtils.filter( + restoredRequestsForUser, it -> it.getAssociationId() == restored.getId()); + + // Handle collision: If a local association belonging to the same package already exists + // and their tags match, then keep the local one in favor of creating a new association. + if (handleCollision(userId, restored, restoredRequests)) { + continue; + } + + // Create a new association reassigned to this user and a valid association ID + final String packageName = restored.getPackageName(); + final int newId = mService.getNewAssociationIdForPackage(userId, packageName); + AssociationInfo newAssociation = + new AssociationInfo.Builder(newId, userId, packageName, restored) + .build(); + + // Check if the companion app for this association is already installed, then do one + // of the following: + // (1) If the app is already installed, then go ahead and add this association and grant + // the role attached to this association to the app. + // (2) If the app isn't yet installed, then add this association to the list of pending + // associations to be added when the package is installed in the future. + boolean isPackageInstalled = installedApps.stream() + .anyMatch(app -> packageName.equals(app.packageName)); + if (isPackageInstalled) { + mAssociationRequestsProcessor.maybeGrantRoleAndStoreAssociation(newAssociation, + null, null); + } else { + // TODO(b/314992577): Check if package is installed before granting + } + + // Re-map restored system data transfer requests to newly created associations + for (SystemDataTransferRequest restoredRequest : restoredRequests) { + SystemDataTransferRequest newRequest = restoredRequest.copyWithNewId(newId); + newRequest.setUserId(userId); + mSystemDataTransferRequestStore.writeRequest(userId, newRequest); + } + } + + // Persist restored state. + mService.persistStateForUser(userId); + } + + /** + * Detects and handles collision between restored association and local association. Returns + * true if there has been a collision and false otherwise. + */ + private boolean handleCollision(@UserIdInt int userId, + AssociationInfo restored, + List<SystemDataTransferRequest> restoredRequests) { + List<AssociationInfo> localAssociations = mAssociationStore.getAssociationsForPackage( + restored.getUserId(), restored.getPackageName()); + Predicate<AssociationInfo> isSameDevice = associationInfo -> { + boolean matchesMacAddress = Objects.equals( + associationInfo.getDeviceMacAddress(), + restored.getDeviceMacAddress()); + boolean matchesTag = !Flags.associationTag() + || Objects.equals(associationInfo.getTag(), restored.getTag()); + return matchesMacAddress && matchesTag; + }; + AssociationInfo local = CollectionUtils.find(localAssociations, isSameDevice); + + // No collision detected + if (local == null) { + return false; + } + + Log.d(TAG, "Conflict detected with association id=" + local.getId() + + " while restoring CDM backup. Keeping local association."); + + List<SystemDataTransferRequest> localRequests = mSystemDataTransferRequestStore + .readRequestsByAssociationId(userId, local.getId()); + + // If local association doesn't have any existing system data transfer request of same type + // attached, then restore corresponding request onto the local association. Otherwise, keep + // the locally stored request. + for (SystemDataTransferRequest restoredRequest : restoredRequests) { + boolean requestTypeExists = CollectionUtils.any(localRequests, request -> + request.getDataType() == restoredRequest.getDataType()); + + // This type of request consent already exists for the association. + if (requestTypeExists) { + continue; + } + + Log.d(TAG, "Restoring " + restoredRequest.getClass().getSimpleName() + + " to an existing association id=" + local.getId() + "."); + + SystemDataTransferRequest newRequest = + restoredRequest.copyWithNewId(local.getId()); + newRequest.setUserId(userId); + mSystemDataTransferRequestStore.writeRequest(userId, newRequest); + } + + return true; + } +} diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 487b66cb4e32..858887ae20c6 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -164,6 +164,7 @@ public class CompanionDeviceManagerService extends SystemService { private final SystemDataTransferRequestStore mSystemDataTransferRequestStore; private AssociationRequestsProcessor mAssociationRequestsProcessor; private SystemDataTransferProcessor mSystemDataTransferProcessor; + private BackupRestoreProcessor mBackupRestoreProcessor; private CompanionDevicePresenceMonitor mDevicePresenceMonitor; private CompanionApplicationController mCompanionAppController; private CompanionTransportManager mTransportManager; @@ -256,6 +257,9 @@ public class CompanionDeviceManagerService extends SystemService { mSystemDataTransferProcessor = new SystemDataTransferProcessor(this, mPackageManagerInternal, mAssociationStore, mSystemDataTransferRequestStore, mTransportManager); + mBackupRestoreProcessor = new BackupRestoreProcessor( + /* cdmService */ this, mAssociationStore, mPersistentStore, + mSystemDataTransferRequestStore, mAssociationRequestsProcessor); // TODO(b/279663946): move context sync to a dedicated system service mCrossDeviceSyncController = new CrossDeviceSyncController(getContext(), mTransportManager); @@ -501,7 +505,7 @@ public class CompanionDeviceManagerService extends SystemService { updateAtm(userId, updatedAssociations); } - private void persistStateForUser(@UserIdInt int userId) { + void persistStateForUser(@UserIdInt int userId) { // We want to store both active associations and the revoked (removed) association that we // are keeping around for the final clean-up (delayed role holder removal). final List<AssociationInfo> allAssociations; @@ -577,6 +581,11 @@ public class CompanionDeviceManagerService extends SystemService { mCompanionAppController.onPackagesChanged(userId); } + private void onPackageAddedInternal(@UserIdInt int userId, @NonNull String packageName) { + if (DEBUG) Log.i(TAG, "onPackageAddedInternal() u" + userId + "/" + packageName); + // TODO(b/314992577): Retroactively grant roles for restored associations + } + // Revoke associations if the selfManaged companion device does not connect for 3 months. void removeInactiveSelfManagedAssociations() { final long currentTime = System.currentTimeMillis(); @@ -1052,13 +1061,14 @@ public class CompanionDeviceManagerService extends SystemService { @Override public byte[] getBackupPayload(int userId) { - // TODO(b/286124853): back up CDM data - return new byte[0]; + Log.i(TAG, "getBackupPayload() userId=" + userId); + return mBackupRestoreProcessor.getBackupPayload(userId); } @Override public void applyRestoredPayload(byte[] payload, int userId) { - // TODO(b/286124853): restore CDM data + Log.i(TAG, "applyRestoredPayload() userId=" + userId); + mBackupRestoreProcessor.applyRestoredPayload(payload, userId); } @Override @@ -1067,7 +1077,8 @@ public class CompanionDeviceManagerService extends SystemService { @NonNull String[] args) { return new CompanionDeviceShellCommand(CompanionDeviceManagerService.this, mAssociationStore, mDevicePresenceMonitor, mTransportManager, - mSystemDataTransferProcessor, mAssociationRequestsProcessor) + mSystemDataTransferProcessor, mAssociationRequestsProcessor, + mBackupRestoreProcessor) .exec(this, in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(), args); } @@ -1499,6 +1510,11 @@ public class CompanionDeviceManagerService extends SystemService { public void onPackageModified(String packageName) { onPackageModifiedInternal(getChangingUserId(), packageName); } + + @Override + public void onPackageAdded(String packageName, int uid) { + onPackageAddedInternal(getChangingUserId(), packageName); + } }; static int getFirstAssociationIdForUser(@UserIdInt int userId) { diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java index 9fdf5c2d0fc1..53c0184c6e29 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java @@ -18,6 +18,8 @@ package com.android.server.companion; import static android.companion.CompanionDeviceManager.MESSAGE_REQUEST_CONTEXT_SYNC; +import static com.android.server.companion.PermissionsUtils.sanitizeWithCallerChecks; + import android.companion.AssociationInfo; import android.companion.ContextSyncMessage; import android.companion.Flags; @@ -26,6 +28,7 @@ import android.companion.datatransfer.PermissionSyncRequest; import android.net.MacAddress; import android.os.Binder; import android.os.ShellCommand; +import android.util.Base64; import android.util.proto.ProtoOutputStream; import com.android.server.companion.datatransfer.SystemDataTransferProcessor; @@ -47,19 +50,22 @@ class CompanionDeviceShellCommand extends ShellCommand { private final SystemDataTransferProcessor mSystemDataTransferProcessor; private final AssociationRequestsProcessor mAssociationRequestsProcessor; + private final BackupRestoreProcessor mBackupRestoreProcessor; CompanionDeviceShellCommand(CompanionDeviceManagerService service, AssociationStoreImpl associationStore, CompanionDevicePresenceMonitor devicePresenceMonitor, CompanionTransportManager transportManager, SystemDataTransferProcessor systemDataTransferProcessor, - AssociationRequestsProcessor associationRequestsProcessor) { + AssociationRequestsProcessor associationRequestsProcessor, + BackupRestoreProcessor backupRestoreProcessor) { mService = service; mAssociationStore = associationStore; mDevicePresenceMonitor = devicePresenceMonitor; mTransportManager = transportManager; mSystemDataTransferProcessor = systemDataTransferProcessor; mAssociationRequestsProcessor = associationRequestsProcessor; + mBackupRestoreProcessor = backupRestoreProcessor; } @Override @@ -111,6 +117,19 @@ class CompanionDeviceShellCommand extends ShellCommand { } break; + case "disassociate-all": { + final int userId = getNextIntArgRequired(); + final String packageName = getNextArgRequired(); + final List<AssociationInfo> userAssociations = + mAssociationStore.getAssociationsForPackage(userId, packageName); + for (AssociationInfo association : userAssociations) { + if (sanitizeWithCallerChecks(mService.getContext(), association) != null) { + mService.disassociateInternal(association.getId()); + } + } + } + break; + case "clear-association-memory-cache": mService.persistState(); mService.loadAssociationsFromDisk(); @@ -126,6 +145,20 @@ class CompanionDeviceShellCommand extends ShellCommand { mDevicePresenceMonitor.simulateDeviceEvent(associationId, /* event */ 1); break; + case "get-backup-payload": { + final int userId = getNextIntArgRequired(); + byte[] payload = mBackupRestoreProcessor.getBackupPayload(userId); + out.println(Base64.encodeToString(payload, Base64.NO_WRAP)); + } + break; + + case "apply-restored-payload": { + final int userId = getNextIntArgRequired(); + byte[] payload = Base64.decode(getNextArgRequired(), Base64.NO_WRAP); + mBackupRestoreProcessor.applyRestoredPayload(payload, userId); + } + break; + case "remove-inactive-associations": { // This command should trigger the same "clean-up" job as performed by the // InactiveAssociationsRemovalService JobService. However, since the @@ -355,6 +388,8 @@ class CompanionDeviceShellCommand extends ShellCommand { pw.println(" Create a new Association."); pw.println(" disassociate USER_ID PACKAGE MAC_ADDRESS"); pw.println(" Remove an existing Association."); + pw.println(" disassociate-all USER_ID"); + pw.println(" Remove all Associations for a user."); pw.println(" clear-association-memory-cache"); pw.println(" Clear the in-memory association cache and reload all association "); pw.println(" information from persistent storage. USE FOR DEBUGGING PURPOSES ONLY."); @@ -378,6 +413,14 @@ class CompanionDeviceShellCommand extends ShellCommand { pw.println(" invoked for the same device (same ASSOCIATION_ID) no longer than"); pw.println(" 60 seconds ago."); + pw.println(" get-backup-payload USER_ID"); + pw.println(" Generate backup payload for the given user and print its content"); + pw.println(" encoded to a Base64 string."); + pw.println(" USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY."); + pw.println(" apply-restored-payload USER_ID PAYLOAD"); + pw.println(" Apply restored backup payload for the given user."); + pw.println(" USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY."); + if (Flags.devicePresence()) { pw.println(" simulate-device-event ASSOCIATION_ID EVENT"); pw.println(" Simulate the companion device event changes:"); diff --git a/services/companion/java/com/android/server/companion/DataStoreUtils.java b/services/companion/java/com/android/server/companion/DataStoreUtils.java index c1825296ca69..04ce1f673124 100644 --- a/services/companion/java/com/android/server/companion/DataStoreUtils.java +++ b/services/companion/java/com/android/server/companion/DataStoreUtils.java @@ -30,8 +30,11 @@ import com.android.internal.util.FunctionalUtils.ThrowingConsumer; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.IOException; /** * Util class for CDM data stores @@ -88,6 +91,29 @@ public final class DataStoreUtils { } } + /** + * Read a file and return the byte array containing the bytes of the file. + */ + @NonNull + public static byte[] fileToByteArray(@NonNull AtomicFile file) { + if (!file.getBaseFile().exists()) { + Slog.d(TAG, "File does not exist"); + return new byte[0]; + } + try (FileInputStream in = file.openRead()) { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int read; + while ((read = in.read(buffer)) != -1) { + bytes.write(buffer, 0, read); + } + return bytes.toByteArray(); + } catch (IOException e) { + Slog.e(TAG, "Error while reading requests file", e); + } + return new byte[0]; + } + private DataStoreUtils() { } } diff --git a/services/companion/java/com/android/server/companion/PersistentDataStore.java b/services/companion/java/com/android/server/companion/PersistentDataStore.java index b4b93793d549..dbaf7e85b7fa 100644 --- a/services/companion/java/com/android/server/companion/PersistentDataStore.java +++ b/services/companion/java/com/android/server/companion/PersistentDataStore.java @@ -28,6 +28,7 @@ import static com.android.internal.util.XmlUtils.writeStringAttribute; import static com.android.server.companion.CompanionDeviceManagerService.getFirstAssociationIdForUser; import static com.android.server.companion.CompanionDeviceManagerService.getLastAssociationIdForUser; import static com.android.server.companion.DataStoreUtils.createStorageFileForUser; +import static com.android.server.companion.DataStoreUtils.fileToByteArray; import static com.android.server.companion.DataStoreUtils.isEndOfTag; import static com.android.server.companion.DataStoreUtils.isStartOfTag; import static com.android.server.companion.DataStoreUtils.writeToFileSafely; @@ -55,9 +56,11 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -328,34 +331,42 @@ final class PersistentDataStore { @NonNull String rootTag, @Nullable Collection<AssociationInfo> associationsOut, @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut) { try (FileInputStream in = file.openRead()) { - final TypedXmlPullParser parser = Xml.resolvePullParser(in); - - XmlUtils.beginDocument(parser, rootTag); - final int version = readIntAttribute(parser, XML_ATTR_PERSISTENCE_VERSION, 0); - switch (version) { - case 0: - readAssociationsV0(parser, userId, associationsOut); - break; - case 1: - while (true) { - parser.nextTag(); - if (isStartOfTag(parser, XML_TAG_ASSOCIATIONS)) { - readAssociationsV1(parser, userId, associationsOut); - } else if (isStartOfTag(parser, XML_TAG_PREVIOUSLY_USED_IDS)) { - readPreviouslyUsedIdsV1(parser, previouslyUsedIdsPerPackageOut); - } else if (isEndOfTag(parser, rootTag)) { - break; - } - } - break; - } - return version; + return readStateFromInputStream(userId, in, rootTag, associationsOut, + previouslyUsedIdsPerPackageOut); } catch (XmlPullParserException | IOException e) { Slog.e(TAG, "Error while reading associations file", e); return -1; } } + private int readStateFromInputStream(@UserIdInt int userId, @NonNull InputStream in, + @NonNull String rootTag, @Nullable Collection<AssociationInfo> associationsOut, + @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut) + throws XmlPullParserException, IOException { + final TypedXmlPullParser parser = Xml.resolvePullParser(in); + + XmlUtils.beginDocument(parser, rootTag); + final int version = readIntAttribute(parser, XML_ATTR_PERSISTENCE_VERSION, 0); + switch (version) { + case 0: + readAssociationsV0(parser, userId, associationsOut); + break; + case 1: + while (true) { + parser.nextTag(); + if (isStartOfTag(parser, XML_TAG_ASSOCIATIONS)) { + readAssociationsV1(parser, userId, associationsOut); + } else if (isStartOfTag(parser, XML_TAG_PREVIOUSLY_USED_IDS)) { + readPreviouslyUsedIdsV1(parser, previouslyUsedIdsPerPackageOut); + } else if (isEndOfTag(parser, rootTag)) { + break; + } + } + break; + } + return version; + } + private void persistStateToFileLocked(@NonNull AtomicFile file, @Nullable Collection<AssociationInfo> associations, @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackage) { @@ -391,6 +402,26 @@ final class PersistentDataStore { u -> createStorageFileForUser(userId, FILE_NAME)); } + byte[] getBackupPayload(@UserIdInt int userId) { + Slog.i(TAG, "Fetching stored state data for user " + userId + " from disk"); + final AtomicFile file = getStorageFileForUser(userId); + + synchronized (file) { + return fileToByteArray(file); + } + } + + void readStateFromPayload(byte[] payload, @UserIdInt int userId, + @NonNull Set<AssociationInfo> associationsOut, + @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut) { + try (ByteArrayInputStream in = new ByteArrayInputStream(payload)) { + readStateFromInputStream(userId, in, XML_TAG_STATE, associationsOut, + previouslyUsedIdsPerPackageOut); + } catch (XmlPullParserException | IOException e) { + Slog.e(TAG, "Error while reading associations file", e); + } + } + private static @NonNull File getBaseLegacyStorageFileForUser(@UserIdInt int userId) { return new File(Environment.getUserSystemDirectory(userId), FILE_NAME_LEGACY); } diff --git a/services/companion/java/com/android/server/companion/RolesUtils.java b/services/companion/java/com/android/server/companion/RolesUtils.java index 163f614fb65d..af9d2d783100 100644 --- a/services/companion/java/com/android/server/companion/RolesUtils.java +++ b/services/companion/java/com/android/server/companion/RolesUtils.java @@ -47,6 +47,17 @@ final class RolesUtils { return roleHolders.contains(packageName); } + /** + * Attempt to add the association's companion app as the role holder for the device profile + * specified in the association. If the association does not have any device profile specified, + * then the operation will always be successful as a no-op. + * + * @param context + * @param associationInfo the association for which the role should be granted to the app + * @param roleGrantResult the result callback for adding role holder. True if successful, and + * false if failed. If the association does not have any device profile + * specified, then the operation will always be successful as a no-op. + */ static void addRoleHolderForAssociation( @NonNull Context context, @NonNull AssociationInfo associationInfo, @NonNull Consumer<Boolean> roleGrantResult) { @@ -55,7 +66,11 @@ final class RolesUtils { } final String deviceProfile = associationInfo.getDeviceProfile(); - if (deviceProfile == null) return; + if (deviceProfile == null) { + // If no device profile is specified, then no-op and resolve callback with success. + roleGrantResult.accept(true); + return; + } final RoleManager roleManager = context.getSystemService(RoleManager.class); diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java index 9f489e8d613a..8fe04547a9ec 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java +++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java @@ -23,6 +23,7 @@ import static com.android.internal.util.XmlUtils.readIntAttribute; import static com.android.internal.util.XmlUtils.writeBooleanAttribute; import static com.android.internal.util.XmlUtils.writeIntAttribute; import static com.android.server.companion.DataStoreUtils.createStorageFileForUser; +import static com.android.server.companion.DataStoreUtils.fileToByteArray; import static com.android.server.companion.DataStoreUtils.isEndOfTag; import static com.android.server.companion.DataStoreUtils.isStartOfTag; import static com.android.server.companion.DataStoreUtils.writeToFileSafely; @@ -44,6 +45,7 @@ import com.android.modules.utils.TypedXmlSerializer; import org.xmlpull.v1.XmlPullParserException; +import java.io.ByteArrayInputStream; import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; @@ -152,6 +154,33 @@ public class SystemDataTransferRequestStore { mExecutor.execute(() -> writeRequestsToStore(userId, cachedRequests)); } + /** + * Return the byte contents of the XML file storing current system data transfer requests. + */ + public byte[] getBackupPayload(@UserIdInt int userId) { + final AtomicFile file = getStorageFileForUser(userId); + + synchronized (file) { + return fileToByteArray(file); + } + } + + /** + * Parse the byte array containing XML information of system data transfer requests into + * an array list of requests. + */ + public List<SystemDataTransferRequest> readRequestsFromPayload(byte[] payload) { + try (ByteArrayInputStream in = new ByteArrayInputStream(payload)) { + final TypedXmlPullParser parser = Xml.resolvePullParser(in); + XmlUtils.beginDocument(parser, XML_TAG_REQUESTS); + + return readRequestsFromXml(parser); + } catch (XmlPullParserException | IOException e) { + Slog.e(LOG_TAG, "Error while reading requests file", e); + return new ArrayList<>(); + } + } + @GuardedBy("mLock") private ArrayList<SystemDataTransferRequest> readRequestsFromCache(@UserIdInt int userId) { ArrayList<SystemDataTransferRequest> cachedRequests = mCachedPerUser.get(userId); diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index 39b8643e6d38..19a9239752cd 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -3236,7 +3236,7 @@ class StorageManagerService extends IStorageManager.Stub super.createUserStorageKeys_enforcePermission(); try { - mVold.createUserStorageKeys(userId, serialNumber, ephemeral); + mVold.createUserStorageKeys(userId, ephemeral); // Since the user's CE key was just created, the user's CE storage is now unlocked. synchronized (mLock) { mCeUnlockedUsers.append(userId); @@ -3281,7 +3281,7 @@ class StorageManagerService extends IStorageManager.Stub super.unlockCeStorage_enforcePermission(); if (StorageManager.isFileEncrypted()) { - mVold.unlockCeStorage(userId, serialNumber, HexDump.toHexString(secret)); + mVold.unlockCeStorage(userId, HexDump.toHexString(secret)); } synchronized (mLock) { mCeUnlockedUsers.append(userId); @@ -3368,7 +3368,7 @@ class StorageManagerService extends IStorageManager.Stub private void prepareUserStorageInternal(String volumeUuid, int userId, int serialNumber, int flags) throws Exception { try { - mVold.prepareUserStorage(volumeUuid, userId, serialNumber, flags); + mVold.prepareUserStorage(volumeUuid, userId, flags); // After preparing user storage, we should check if we should mount data mirror again, // and we do it for user 0 only as we only need to do once for all users. if (volumeUuid != null) { diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 0185c22c5c7c..e7e721b3ec55 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -162,8 +162,7 @@ public class SettingsToPropertiesMapper { "pdf_viewer", "pixel_audio_android", "pixel_bluetooth", - "pixel_system_sw_touch", - "pixel_system_sw_usb", + "pixel_system_sw_video", "pixel_watch", "platform_security", "power", @@ -175,6 +174,7 @@ public class SettingsToPropertiesMapper { "safety_center", "sensors", "system_performance", + "system_sw_touch", "system_sw_usb", "test_suites", "text", diff --git a/services/core/java/com/android/server/am/TEST_MAPPING b/services/core/java/com/android/server/am/TEST_MAPPING index 575db01931e6..e90910a13b3b 100644 --- a/services/core/java/com/android/server/am/TEST_MAPPING +++ b/services/core/java/com/android/server/am/TEST_MAPPING @@ -146,6 +146,15 @@ { "include-filter": "android.app.cts.ServiceTest" }, { "include-filter": "android.app.cts.ActivityManagerFgsBgStartTest" } ] + }, + { + "name": "CtsStatsdAtomHostTestCases", + "options": [ + { "include-filter": "android.cts.statsdatom.appexit.AppExitHostTest" }, + { "exclude-annotation": "androidx.test.filters.LargeTest" }, + { "exclude-annotation": "androidx.test.filters.FlakyTest" }, + { "exclude-annotation": "org.junit.Ignore" } + ] } ] } diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 203ac2cba3ca..df8d9e1a406c 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -2906,7 +2906,13 @@ public class AppOpsService extends IAppOpsService.Stub { // TODO(b/302609140): Remove extra logging after this issue is diagnosed. if (code == OP_BLUETOOTH_CONNECT) { Slog.e(TAG, "noting OP_BLUETOOTH_CONNECT returned MODE_ERRORED as" - + " #getOpsLocked returned null"); + + " #getOpsLocked returned null for" + + " uid: " + uid + + " packageName: " + packageName + + " attributionTag: " + attributionTag + + " pvr.isAttributionTagValid: " + pvr.isAttributionTagValid + + " pvr.bypass: " + pvr.bypass); + Slog.e(TAG, "mUidStates.get(" + uid + "): " + mUidStates.get(uid)); } return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag, packageName); diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 7ac4dd30fe71..a1b6f297f287 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -7879,7 +7879,6 @@ public class AudioService extends IAudioService.Stub DEVICE_MEDIA_UNMUTED_ON_PLUG_SET.addAll(AudioSystem.DEVICE_OUT_ALL_A2DP_SET); DEVICE_MEDIA_UNMUTED_ON_PLUG_SET.addAll(AudioSystem.DEVICE_OUT_ALL_BLE_SET); DEVICE_MEDIA_UNMUTED_ON_PLUG_SET.addAll(AudioSystem.DEVICE_OUT_ALL_USB_SET); - DEVICE_MEDIA_UNMUTED_ON_PLUG_SET.add(AudioSystem.DEVICE_OUT_HDMI); } /** only public for mocking/spying, do not call outside of AudioService */ diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java index 0bb6141583d5..90da74ccaa1c 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java @@ -147,7 +147,7 @@ public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRe gestureAvailabilityDispatcher, () -> mCurrentUserId, getUserSwitchCallback())); mLockoutTracker = new LockoutFrameworkImpl(getContext(), userId -> mLockoutResetDispatcher.notifyLockoutResetCallbacks( - getSensorProperties().sensorId)); + getSensorProperties().sensorId), getHandler()); } @Override diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java index 2f77275890dd..0e05a7923db4 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java @@ -19,6 +19,7 @@ package com.android.server.biometrics.sensors.fingerprint.hidl; import static android.Manifest.permission.RESET_FINGERPRINT_LOCKOUT; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; @@ -81,19 +82,30 @@ public class LockoutFrameworkImpl implements LockoutTracker { @NonNull LockoutResetCallback lockoutResetCallback) { this(context, lockoutResetCallback, (userId) -> PendingIntent.getBroadcast(context, userId, new Intent(ACTION_LOCKOUT_RESET).putExtra(KEY_LOCKOUT_RESET_USER, userId), - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)); + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE), + null /* handler */); + } + + public LockoutFrameworkImpl(@NonNull Context context, + @NonNull LockoutResetCallback lockoutResetCallback, + @NonNull Handler handler) { + this(context, lockoutResetCallback, (userId) -> PendingIntent.getBroadcast(context, userId, + new Intent(ACTION_LOCKOUT_RESET).putExtra(KEY_LOCKOUT_RESET_USER, userId), + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE), + handler); } @VisibleForTesting LockoutFrameworkImpl(@NonNull Context context, @NonNull LockoutResetCallback lockoutResetCallback, - @NonNull Function<Integer, PendingIntent> lockoutResetIntent) { + @NonNull Function<Integer, PendingIntent> lockoutResetIntent, + @Nullable Handler handler) { mLockoutResetCallback = lockoutResetCallback; mTimedLockoutCleared = new SparseBooleanArray(); mFailedAttempts = new SparseIntArray(); mAlarmManager = context.getSystemService(AlarmManager.class); mLockoutReceiver = new LockoutReceiver(); - mHandler = new Handler(Looper.getMainLooper()); + mHandler = handler == null ? new Handler(Looper.getMainLooper()) : handler; mLockoutResetIntent = lockoutResetIntent; context.registerReceiver(mLockoutReceiver, new IntentFilter(ACTION_LOCKOUT_RESET), diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java index 56a94ec06ad4..49f607095b90 100644 --- a/services/core/java/com/android/server/clipboard/ClipboardService.java +++ b/services/core/java/com/android/server/clipboard/ClipboardService.java @@ -1424,7 +1424,11 @@ public class ClipboardService extends SystemService { String defaultIme = Settings.Secure.getStringForUser(getContext().getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD, userId); if (!TextUtils.isEmpty(defaultIme)) { - final String imePkg = ComponentName.unflattenFromString(defaultIme).getPackageName(); + final ComponentName imeComponent = ComponentName.unflattenFromString(defaultIme); + if (imeComponent == null) { + return false; + } + final String imePkg = imeComponent.getPackageName(); return imePkg.equals(packageName); } return false; diff --git a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java index 1ac3a12fad21..7cea9c422b4d 100644 --- a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java +++ b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java @@ -63,12 +63,14 @@ class DeviceStateToLayoutMap { private static final String CONFIG_FILE_PATH = "etc/displayconfig/display_layout_configuration.xml"; + private static final String DATA_CONFIG_FILE_PATH = + "system/displayconfig/display_layout_configuration.xml"; + private final SparseArray<Layout> mLayoutMap = new SparseArray<>(); private final DisplayIdProducer mIdProducer; DeviceStateToLayoutMap(DisplayIdProducer idProducer) { - this(idProducer, Environment.buildPath( - Environment.getVendorDirectory(), CONFIG_FILE_PATH)); + this(idProducer, getConfigFile()); } DeviceStateToLayoutMap(DisplayIdProducer idProducer, File configFile) { @@ -77,6 +79,16 @@ class DeviceStateToLayoutMap { createLayout(STATE_DEFAULT); } + static private File getConfigFile() { + final File configFileFromDataDir = Environment.buildPath(Environment.getDataDirectory(), + DATA_CONFIG_FILE_PATH); + if (configFileFromDataDir.exists()) { + return configFileFromDataDir; + } else { + return Environment.buildPath(Environment.getVendorDirectory(), CONFIG_FILE_PATH); + } + } + public void dumpLocked(IndentingPrintWriter ipw) { ipw.println("DeviceStateToLayoutMap:"); ipw.increaseIndent(); diff --git a/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java b/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java index 6978686faa12..544f490913e2 100644 --- a/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java +++ b/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java @@ -118,8 +118,13 @@ public class DisplayBrightnessMappingConfig { "The first lux value in the display brightness mapping must be 0"); } - String key = (mapping.getMode() == null ? "default" : mapping.getMode()) + "_" - + (mapping.getSetting() == null ? "normal" : mapping.getSetting()); + String key = (mapping.getMode() == null + ? AutoBrightnessModeName._default.getRawName() + : mapping.getMode().getRawName()) + + "_" + + (mapping.getSetting() == null + ? AutoBrightnessSettingName.normal.getRawName() + : mapping.getSetting().getRawName()); if (mBrightnessLevelsMap.containsKey(key) || mBrightnessLevelsLuxMap.containsKey(key)) { throw new IllegalArgumentException( diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index aa7f07d24dbf..35991b356bba 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -109,6 +109,11 @@ public class DisplayManagerFlags { Flags.FLAG_FAST_HDR_TRANSITIONS, Flags::fastHdrTransitions); + private final FlagState mRefreshRateVotingTelemetry = new FlagState( + Flags.FLAG_REFRESH_RATE_VOTING_TELEMETRY, + Flags::refreshRateVotingTelemetry + ); + /** Returns whether connected display management is enabled or not. */ public boolean isConnectedDisplayManagementEnabled() { return mConnectedDisplayManagementFlagState.isEnabled(); @@ -220,6 +225,10 @@ public class DisplayManagerFlags { return mFastHdrTransitions.isEnabled(); } + public boolean isRefreshRateVotingTelemetryEnabled() { + return mRefreshRateVotingTelemetry.isEnabled(); + } + /** * dumps all flagstates * @param pw printWriter @@ -242,6 +251,7 @@ public class DisplayManagerFlags { pw.println(" " + mBrightnessWearBedtimeModeClamperFlagState); pw.println(" " + mAutoBrightnessModesFlagState); pw.println(" " + mFastHdrTransitions); + pw.println(" " + mRefreshRateVotingTelemetry); } private static class FlagState { diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index 04ecbb9e08ad..e735282a496e 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -161,3 +161,10 @@ flag { is_fixed_read_only: true } +flag { + name: "refresh_rate_voting_telemetry" + namespace: "display_manager" + description: "Feature flag for enabling telemetry for refresh rate voting in DisplayManager" + bug: "310029108" + is_fixed_read_only: true +} diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java index 6e503cbd68f3..50e953323443 100644 --- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java @@ -154,6 +154,9 @@ public class DisplayModeDirector { private final VotesStorage mVotesStorage; + @Nullable + private final VotesStatsReporter mVotesStatsReporter; + /** * The allowed refresh rate switching type. This is used by SurfaceFlinger. */ @@ -204,6 +207,8 @@ public class DisplayModeDirector { mContext = context; mHandler = new DisplayModeDirectorHandler(handler.getLooper()); mInjector = injector; + mVotesStatsReporter = injector.getVotesStatsReporter( + displayManagerFlags.isRefreshRateVotingTelemetryEnabled()); mSupportedModesByDisplay = new SparseArray<>(); mDefaultModeByDisplay = new SparseArray<>(); mAppRequestObserver = new AppRequestObserver(); @@ -214,7 +219,8 @@ public class DisplayModeDirector { mBrightnessObserver = new BrightnessObserver(context, handler, injector); mDefaultDisplayDeviceConfig = null; mUdfpsObserver = new UdfpsObserver(); - mVotesStorage = new VotesStorage(this::notifyDesiredDisplayModeSpecsChangedLocked); + mVotesStorage = new VotesStorage(this::notifyDesiredDisplayModeSpecsChangedLocked, + mVotesStatsReporter); mDisplayObserver = new DisplayObserver(context, handler, mVotesStorage); mSensorObserver = new SensorObserver(context, mVotesStorage, injector); mSkinThermalStatusObserver = new SkinThermalStatusObserver(injector, mVotesStorage); @@ -341,6 +347,11 @@ public class DisplayModeDirector { appRequestSummary.limitRefreshRanges(primarySummary); Display.Mode baseMode = primarySummary.selectBaseMode(availableModes, defaultMode); + if (mVotesStatsReporter != null) { + mVotesStatsReporter.reportVotesActivated(displayId, lowestConsideredPriority, + baseMode, votes); + } + if (baseMode == null) { Slog.w(TAG, "Can't find a set of allowed modes which satisfies the votes. Falling" + " back to the default mode. Display = " + displayId + ", votes = " + votes @@ -970,9 +981,14 @@ public class DisplayModeDirector { if (!mIsBackUpSmoothDisplayAndForcePeakRefreshRateEnabled) { // The flag had been turned off, we need to restore the original value - Settings.System.putFloatForUser(cr, - Settings.System.MIN_REFRESH_RATE, minRefreshRate, cr.getUserId()); + Settings.System.putFloatForUser(cr, Settings.System.MIN_REFRESH_RATE, + highestRefreshRate, cr.getUserId()); } + } else if (mIsBackUpSmoothDisplayAndForcePeakRefreshRateEnabled + && Math.round(minRefreshRate) == Math.round(highestRefreshRate)) { + // The flag has been turned on, we need to upgrade the setting + Settings.System.putFloatForUser(cr, Settings.System.MIN_REFRESH_RATE, + Float.POSITIVE_INFINITY, cr.getUserId()); } float peakRefreshRate = Settings.System.getFloatForUser(cr, @@ -983,9 +999,14 @@ public class DisplayModeDirector { if (!mIsBackUpSmoothDisplayAndForcePeakRefreshRateEnabled) { // The flag had been turned off, we need to restore the original value - Settings.System.putFloatForUser(cr, - Settings.System.PEAK_REFRESH_RATE, peakRefreshRate, cr.getUserId()); + Settings.System.putFloatForUser(cr, Settings.System.PEAK_REFRESH_RATE, + highestRefreshRate, cr.getUserId()); } + } else if (mIsBackUpSmoothDisplayAndForcePeakRefreshRateEnabled + && Math.round(peakRefreshRate) == Math.round(highestRefreshRate)) { + // The flag has been turned on, we need to upgrade the setting + Settings.System.putFloatForUser(cr, Settings.System.PEAK_REFRESH_RATE, + Float.POSITIVE_INFINITY, cr.getUserId()); } updateRefreshRateSettingLocked(minRefreshRate, peakRefreshRate, mDefaultRefreshRate); @@ -2811,6 +2832,9 @@ public class DisplayModeDirector { StatusBarManagerInternal getStatusBarManagerInternal(); SensorManagerInternal getSensorManagerInternal(); + + @Nullable + VotesStatsReporter getVotesStatsReporter(boolean refreshRateVotingTelemetryEnabled); } @VisibleForTesting @@ -2943,6 +2967,13 @@ public class DisplayModeDirector { return LocalServices.getService(SensorManagerInternal.class); } + @Override + public VotesStatsReporter getVotesStatsReporter(boolean refreshRateVotingTelemetryEnabled) { + // if frame rate override supported, renderRates will be ignored in mode selection + return new VotesStatsReporter(supportsFrameRateOverride(), + refreshRateVotingTelemetryEnabled); + } + private DisplayManager getDisplayManager() { if (mDisplayManager == null) { mDisplayManager = mContext.getSystemService(DisplayManager.class); diff --git a/services/core/java/com/android/server/display/mode/VotesStatsReporter.java b/services/core/java/com/android/server/display/mode/VotesStatsReporter.java new file mode 100644 index 000000000000..a30c4d2b5b0b --- /dev/null +++ b/services/core/java/com/android/server/display/mode/VotesStatsReporter.java @@ -0,0 +1,93 @@ +/* + * 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.display.mode; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Trace; +import android.util.SparseArray; +import android.view.Display; + +/** + * The VotesStatsReporter is responsible for collecting and sending Vote related statistics + */ +class VotesStatsReporter { + private static final String TAG = "VotesStatsReporter"; + private static final int REFRESH_RATE_NOT_LIMITED = 1000; + private final boolean mIgnoredRenderRate; + private final boolean mFrameworkStatsLogReportingEnabled; + + public VotesStatsReporter(boolean ignoreRenderRate, boolean refreshRateVotingTelemetryEnabled) { + mIgnoredRenderRate = ignoreRenderRate; + mFrameworkStatsLogReportingEnabled = refreshRateVotingTelemetryEnabled; + } + + void reportVoteAdded(int displayId, int priority, @NonNull Vote vote) { + int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate); + Trace.traceCounter(Trace.TRACE_TAG_POWER, + TAG + "." + displayId + ":" + Vote.priorityToString(priority), maxRefreshRate); + // if ( mFrameworkStatsLogReportingEnabled) { + // FrameworkStatsLog.write(VOTE_CHANGED, displayID, priority, ADDED, maxRefreshRate, -1); + // } + } + + void reportVoteRemoved(int displayId, int priority) { + Trace.traceCounter(Trace.TRACE_TAG_POWER, + TAG + "." + displayId + ":" + Vote.priorityToString(priority), -1); + // if ( mFrameworkStatsLogReportingEnabled) { + // FrameworkStatsLog.write(VOTE_CHANGED, displayID, priority, REMOVED, -1, -1); + // } + } + + void reportVotesActivated(int displayId, int minPriority, @Nullable Display.Mode baseMode, + SparseArray<Vote> votes) { +// if (!mFrameworkStatsLogReportingEnabled) { +// return; +// } +// int selectedRefreshRate = baseMode != null ? (int) baseMode.getRefreshRate() : -1; +// for (int priority = minPriority; priority <= Vote.MAX_PRIORITY; priority ++) { +// Vote vote = votes.get(priority); +// if (vote != null) { +// int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate); +// FrameworkStatsLog.write(VOTE_CHANGED, displayId, priority, +// ACTIVE, maxRefreshRate, selectedRefreshRate); +// } +// } + } + + private static int getMaxRefreshRate(@NonNull Vote vote, boolean ignoreRenderRate) { + int maxRefreshRate = REFRESH_RATE_NOT_LIMITED; + if (vote instanceof RefreshRateVote.PhysicalVote physicalVote) { + maxRefreshRate = (int) physicalVote.mMaxRefreshRate; + } else if (!ignoreRenderRate && (vote instanceof RefreshRateVote.RenderVote renderVote)) { + maxRefreshRate = (int) renderVote.mMaxRefreshRate; + } else if (vote instanceof SupportedModesVote supportedModesVote) { + // SupportedModesVote limits mode by specific refreshRates, so highest rr is allowed + maxRefreshRate = 0; + for (SupportedModesVote.SupportedMode mode : supportedModesVote.mSupportedModes) { + maxRefreshRate = Math.max(maxRefreshRate, (int) mode.mPeakRefreshRate); + } + } else if (vote instanceof CombinedVote combinedVote) { + for (Vote subVote: combinedVote.mVotes) { + // CombinedVote should not have CombinedVote in mVotes, so recursion depth will be 1 + maxRefreshRate = Math.min(maxRefreshRate, + getMaxRefreshRate(subVote, ignoreRenderRate)); + } + } + return maxRefreshRate; + } +} diff --git a/services/core/java/com/android/server/display/mode/VotesStorage.java b/services/core/java/com/android/server/display/mode/VotesStorage.java index 95fb8fc0947a..7a1f7e9d857c 100644 --- a/services/core/java/com/android/server/display/mode/VotesStorage.java +++ b/services/core/java/com/android/server/display/mode/VotesStorage.java @@ -18,7 +18,6 @@ package com.android.server.display.mode; import android.annotation.NonNull; import android.annotation.Nullable; -import android.os.Trace; import android.util.Slog; import android.util.SparseArray; @@ -38,6 +37,9 @@ class VotesStorage { private final Listener mListener; + @Nullable + private final VotesStatsReporter mVotesStatsReporter; + private final Object mStorageLock = new Object(); // A map from the display ID to the collection of votes and their priority. The latter takes // the form of another map from the priority to the vote itself so that each priority is @@ -45,8 +47,9 @@ class VotesStorage { @GuardedBy("mStorageLock") private final SparseArray<SparseArray<Vote>> mVotesByDisplay = new SparseArray<>(); - VotesStorage(@NonNull Listener listener) { + VotesStorage(@NonNull Listener listener, @Nullable VotesStatsReporter votesStatsReporter) { mListener = listener; + mVotesStatsReporter = votesStatsReporter; } /** sets logging enabled/disabled for this class */ void setLoggingEnabled(boolean loggingEnabled) { @@ -110,17 +113,26 @@ class VotesStorage { changed = true; } } - Trace.traceCounter(Trace.TRACE_TAG_POWER, - TAG + "." + displayId + ":" + Vote.priorityToString(priority), - getMaxPhysicalRefreshRate(vote)); if (mLoggingEnabled) { Slog.i(TAG, "Updated votes for display=" + displayId + " votes=" + votes); } if (changed) { + reportVoteStats(displayId, priority, vote); mListener.onChanged(); } } + private void reportVoteStats(int displayId, int priority, @Nullable Vote vote) { + if (mVotesStatsReporter == null) { + return; + } + if (vote == null) { + mVotesStatsReporter.reportVoteRemoved(displayId, priority); + } else { + mVotesStatsReporter.reportVoteAdded(displayId, priority, vote); + } + } + /** dump class values, for debugging */ void dump(@NonNull PrintWriter pw) { SparseArray<SparseArray<Vote>> votesByDisplayLocal = new SparseArray<>(); @@ -157,21 +169,6 @@ class VotesStorage { } } - private static int getMaxPhysicalRefreshRate(@Nullable Vote vote) { - if (vote == null) { - return -1; - } else if (vote instanceof RefreshRateVote.PhysicalVote physicalVote) { - return (int) physicalVote.mMaxRefreshRate; - } else if (vote instanceof CombinedVote combinedVote) { - return combinedVote.mVotes.stream() - .filter(v -> v instanceof RefreshRateVote.PhysicalVote) - .map(pv -> (int) (((RefreshRateVote.PhysicalVote) pv).mMaxRefreshRate)) - .min(Integer::compare) - .orElse(1000); // for visualisation - } - return 1000; // for visualisation, otherwise e.g. -1 -> 60 will be unnoticeable - } - interface Listener { void onChanged(); } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 087c52573118..36dac8316e54 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -48,6 +48,7 @@ import android.hardware.input.IInputDevicesChangedListener; import android.hardware.input.IInputManager; import android.hardware.input.IInputSensorEventListener; import android.hardware.input.IKeyboardBacklightListener; +import android.hardware.input.IStickyModifierStateListener; import android.hardware.input.ITabletModeChangedListener; import android.hardware.input.InputDeviceIdentifier; import android.hardware.input.InputManager; @@ -319,6 +320,9 @@ public class InputManagerService extends IInputManager.Stub // Manages Keyboard backlight private final KeyboardBacklightControllerInterface mKeyboardBacklightController; + // Manages Sticky modifier state + private final StickyModifierStateController mStickyModifierStateController; + // Manages Keyboard modifier keys remapping private final KeyRemapper mKeyRemapper; @@ -464,6 +468,7 @@ public class InputManagerService extends IInputManager.Stub ? new KeyboardBacklightController(mContext, mNative, mDataStore, injector.getLooper(), injector.getUEventManager()) : new KeyboardBacklightControllerInterface() {}; + mStickyModifierStateController = new StickyModifierStateController(); mKeyRemapper = new KeyRemapper(mContext, mNative, mDataStore, injector.getLooper()); mUseDevInputEventForAudioJack = @@ -2827,6 +2832,33 @@ public class InputManagerService extends IInputManager.Stub yPosition)).sendToTarget(); } + @Override + @EnforcePermission(Manifest.permission.MONITOR_STICKY_MODIFIER_STATE) + public void registerStickyModifierStateListener( + @NonNull IStickyModifierStateListener listener) { + super.registerStickyModifierStateListener_enforcePermission(); + Objects.requireNonNull(listener); + mStickyModifierStateController.registerStickyModifierStateListener(listener, + Binder.getCallingPid()); + } + + @Override + @EnforcePermission(Manifest.permission.MONITOR_STICKY_MODIFIER_STATE) + public void unregisterStickyModifierStateListener( + @NonNull IStickyModifierStateListener listener) { + super.unregisterStickyModifierStateListener_enforcePermission(); + Objects.requireNonNull(listener); + mStickyModifierStateController.unregisterStickyModifierStateListener(listener, + Binder.getCallingPid()); + } + + // Native callback + @SuppressWarnings("unused") + void notifyStickyModifierStateChanged(int modifierState, int lockedModifierState) { + mStickyModifierStateController.notifyStickyModifierStateChanged(modifierState, + lockedModifierState); + } + // Native callback. @SuppressWarnings("unused") boolean isInputMethodConnectionActive() { diff --git a/services/core/java/com/android/server/input/StickyModifierStateController.java b/services/core/java/com/android/server/input/StickyModifierStateController.java new file mode 100644 index 000000000000..5a22c107db56 --- /dev/null +++ b/services/core/java/com/android/server/input/StickyModifierStateController.java @@ -0,0 +1,133 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.input; + +import android.annotation.BinderThread; +import android.hardware.input.IStickyModifierStateListener; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; + +/** + * A thread-safe component of {@link InputManagerService} responsible for managing the sticky + * modifier state for A11y Sticky keys feature. + */ +final class StickyModifierStateController { + + private static final String TAG = "ModifierStateController"; + + // To enable these logs, run: + // 'adb shell setprop log.tag.ModifierStateController DEBUG' (requires restart) + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + // List of currently registered sticky modifier state listeners + @GuardedBy("mStickyModifierStateListenerRecords") + private final SparseArray<StickyModifierStateListenerRecord> + mStickyModifierStateListenerRecords = new SparseArray<>(); + + public void notifyStickyModifierStateChanged(int modifierState, int lockedModifierState) { + if (DEBUG) { + Slog.d(TAG, "Sticky modifier state changed, modifierState = " + modifierState + + ", lockedModifierState = " + lockedModifierState); + } + + synchronized (mStickyModifierStateListenerRecords) { + for (int i = 0; i < mStickyModifierStateListenerRecords.size(); i++) { + mStickyModifierStateListenerRecords.valueAt(i).notifyStickyModifierStateChanged( + modifierState, lockedModifierState); + } + } + } + + /** Register the sticky modifier state listener for a process. */ + @BinderThread + public void registerStickyModifierStateListener(IStickyModifierStateListener listener, + int pid) { + synchronized (mStickyModifierStateListenerRecords) { + if (mStickyModifierStateListenerRecords.get(pid) != null) { + throw new IllegalStateException("The calling process has already registered " + + "a StickyModifierStateListener."); + } + StickyModifierStateListenerRecord record = new StickyModifierStateListenerRecord(pid, + listener); + try { + listener.asBinder().linkToDeath(record, 0); + } catch (RemoteException ex) { + throw new RuntimeException(ex); + } + mStickyModifierStateListenerRecords.put(pid, record); + } + } + + /** Unregister the sticky modifier state listener for a process. */ + @BinderThread + public void unregisterStickyModifierStateListener(IStickyModifierStateListener listener, + int pid) { + synchronized (mStickyModifierStateListenerRecords) { + StickyModifierStateListenerRecord record = mStickyModifierStateListenerRecords.get(pid); + if (record == null) { + throw new IllegalStateException("The calling process has no registered " + + "StickyModifierStateListener."); + } + if (record.mListener.asBinder() != listener.asBinder()) { + throw new IllegalStateException("The calling process has a different registered " + + "StickyModifierStateListener."); + } + record.mListener.asBinder().unlinkToDeath(record, 0); + mStickyModifierStateListenerRecords.remove(pid); + } + } + + private void onStickyModifierStateListenerDied(int pid) { + synchronized (mStickyModifierStateListenerRecords) { + mStickyModifierStateListenerRecords.remove(pid); + } + } + + // A record of a registered sticky modifier state listener from one process. + private class StickyModifierStateListenerRecord implements IBinder.DeathRecipient { + public final int mPid; + public final IStickyModifierStateListener mListener; + + StickyModifierStateListenerRecord(int pid, IStickyModifierStateListener listener) { + mPid = pid; + mListener = listener; + } + + @Override + public void binderDied() { + if (DEBUG) { + Slog.d(TAG, "Sticky modifier state listener for pid " + mPid + " died."); + } + onStickyModifierStateListenerDied(mPid); + } + + public void notifyStickyModifierStateChanged(int modifierState, int lockedModifierState) { + try { + mListener.onStickyModifierStateChanged(modifierState, lockedModifierState); + } catch (RemoteException ex) { + Slog.w(TAG, "Failed to notify process " + mPid + + " that sticky modifier state changed, assuming it died.", ex); + binderDied(); + } + } + } +} diff --git a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java index 66807aeb6629..f96bb8fb6c6f 100644 --- a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java +++ b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java @@ -52,6 +52,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.OptionalInt; +import java.util.function.IntConsumer; // TODO(b/210039666): See if we can make this class thread-safe. final class HandwritingModeController { @@ -84,14 +85,14 @@ final class HandwritingModeController { private boolean mDelegatorFromDefaultHomePackage; private Runnable mDelegationIdleTimeoutRunnable; private Handler mDelegationIdleTimeoutHandler; - + private IntConsumer mPointerToolTypeConsumer; private HandwritingEventReceiverSurface mHandwritingSurface; private int mCurrentRequestId; @AnyThread HandwritingModeController(Context context, Looper uiThreadLooper, - Runnable inkWindowInitRunnable) { + Runnable inkWindowInitRunnable, IntConsumer toolTypeConsumer) { mContext = context; mLooper = uiThreadLooper; mCurrentDisplayId = Display.INVALID_DISPLAY; @@ -100,6 +101,7 @@ final class HandwritingModeController { mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); mCurrentRequestId = 0; mInkWindowInitRunnable = inkWindowInitRunnable; + mPointerToolTypeConsumer = toolTypeConsumer; } /** @@ -355,6 +357,11 @@ final class HandwritingModeController { return false; } final MotionEvent event = (MotionEvent) ev; + if (mPointerToolTypeConsumer != null && event.getAction() == MotionEvent.ACTION_DOWN) { + int toolType = event.getToolType(event.getActionIndex()); + // notify IME of change in tool type. + mPointerToolTypeConsumer.accept(toolType); + } if (!event.isStylusPointer()) { return false; } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 94811585dc56..ac1c78f226a2 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -16,6 +16,7 @@ package com.android.server.inputmethod; +import static android.content.Context.DEVICE_ID_DEFAULT; import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL; import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_NORMAL; import static android.os.IServiceManager.DUMP_FLAG_PROTO; @@ -124,6 +125,7 @@ import android.view.WindowManager.DisplayImePolicy; import android.view.WindowManager.LayoutParams; import android.view.WindowManager.LayoutParams.SoftInputModeFlags; import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.Flags; import android.view.inputmethod.ImeTracker; import android.view.inputmethod.InputBinding; import android.view.inputmethod.InputConnection; @@ -206,6 +208,7 @@ import java.util.WeakHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.IntConsumer; /** * This class provides a system service that manages input methods. @@ -313,7 +316,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // All known input methods. final ArrayList<InputMethodInfo> mMethodList = new ArrayList<>(); private final ArrayMap<String, InputMethodInfo> mMethodMap = new ArrayMap<>(); + // Mapping from deviceId to the device-specific imeId for that device. + @GuardedBy("ImfLock.class") private final SparseArray<String> mVirtualDeviceMethodMap = new SparseArray<>(); // TODO: Instantiate mSwitchingController for each user. @@ -340,6 +345,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private int mDisplayIdToShowIme = INVALID_DISPLAY; + @GuardedBy("ImfLock.class") + private int mDeviceIdToShowIme = DEVICE_ID_DEFAULT; + @Nullable private StatusBarManagerInternal mStatusBarManagerInternal; private boolean mShowOngoingImeSwitcherForPhones; @GuardedBy("ImfLock.class") @@ -1718,8 +1726,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub com.android.internal.R.bool.config_preventImeStartupUnlessTextEditor); mNonPreemptibleInputMethods = mRes.getStringArray( com.android.internal.R.array.config_nonPreemptibleInputMethods); + IntConsumer toolTypeConsumer = + Flags.useHandwritingListenerForTooltype() + ? toolType -> onUpdateEditorToolType(toolType) : null; mHwController = new HandwritingModeController(mContext, thread.getLooper(), - new InkWindowInitializer()); + new InkWindowInitializer(), toolTypeConsumer); registerDeviceListenerAndCheckStylusSupport(); } @@ -1740,6 +1751,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } } + private void onUpdateEditorToolType(int toolType) { + synchronized (ImfLock.class) { + IInputMethodInvoker curMethod = getCurMethodLocked(); + if (curMethod != null) { + curMethod.updateEditorToolType(toolType); + } + } + } + @GuardedBy("ImfLock.class") private void resetDefaultImeLocked(Context context) { // Do not reset the default (current) IME when it is a 3rd-party IME @@ -2455,11 +2475,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @StartInputReason int startInputReason, int unverifiedTargetSdkVersion, @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { - // If no method is currently selected, do nothing. - final String selectedMethodId = getSelectedMethodIdLocked(); - if (selectedMethodId == null) { - return InputBindResult.NO_IME; - } + String selectedMethodId = getSelectedMethodIdLocked(); if (!mSystemReady) { // If the system is not yet ready, we shouldn't be running third @@ -2484,8 +2500,21 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return InputBindResult.NOT_IME_TARGET_WINDOW; } final int csDisplayId = cs.mSelfReportedDisplayId; + final int oldDisplayIdToShowIme = mDisplayIdToShowIme; mDisplayIdToShowIme = mVisibilityStateComputer.computeImeDisplayId(winState, csDisplayId); + // Potentially override the selected input method if the new display belongs to a virtual + // device with a custom IME. + if (oldDisplayIdToShowIme != mDisplayIdToShowIme) { + final String deviceMethodId = computeCurrentDeviceMethodIdLocked(selectedMethodId); + if (deviceMethodId == null) { + mVisibilityStateComputer.getImePolicy().setImeHiddenByDisplayPolicy(true); + } else if (!Objects.equals(deviceMethodId, selectedMethodId)) { + setInputMethodLocked(deviceMethodId, NOT_A_SUBTYPE_ID, mDeviceIdToShowIme); + selectedMethodId = deviceMethodId; + } + } + if (mVisibilityStateComputer.getImePolicy().isImeHiddenByDisplayPolicy()) { hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */, null /* resultReceiver */, @@ -2493,6 +2522,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return InputBindResult.NO_IME; } + // If no method is currently selected, do nothing. + if (selectedMethodId == null) { + return InputBindResult.NO_IME; + } + if (mCurClient != cs) { prepareClientSwitchLocked(cs); } @@ -2559,6 +2593,62 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return mBindingController.bindCurrentMethod(); } + /** + * Update the current deviceId and return the relevant imeId for this device. + * 1. If the device changes to virtual and its custom IME is not available, then disable IME. + * 2. If the device changes to virtual with valid custom IME, then return the custom IME. If + * the old device was default, then store the current imeId so it can be restored. + * 3. If the device changes to default, restore the default device IME. + * 4. Otherwise keep the current imeId. + */ + @GuardedBy("ImfLock.class") + private String computeCurrentDeviceMethodIdLocked(String currentMethodId) { + if (mVdmInternal == null) { + mVdmInternal = LocalServices.getService(VirtualDeviceManagerInternal.class); + } + if (mVdmInternal == null || !android.companion.virtual.flags.Flags.vdmCustomIme()) { + return currentMethodId; + } + + final int oldDeviceId = mDeviceIdToShowIme; + mDeviceIdToShowIme = mVdmInternal.getDeviceIdForDisplayId(mDisplayIdToShowIme); + if (mDeviceIdToShowIme == oldDeviceId) { + return currentMethodId; + } + if (mDeviceIdToShowIme == DEVICE_ID_DEFAULT) { + final String defaultDeviceMethodId = mSettings.getSelectedDefaultDeviceInputMethod(); + if (DEBUG) { + Slog.v(TAG, "Restoring default device input method: " + defaultDeviceMethodId); + } + return defaultDeviceMethodId; + } + + final String deviceMethodId = + mVirtualDeviceMethodMap.get(mDeviceIdToShowIme, currentMethodId); + if (Objects.equals(deviceMethodId, currentMethodId)) { + return currentMethodId; + } else if (!mMethodMap.containsKey(deviceMethodId)) { + if (DEBUG) { + Slog.v(TAG, "Disabling IME on virtual device with id " + mDeviceIdToShowIme + + " because its custom input method is not available: " + deviceMethodId); + } + return null; + } + + if (oldDeviceId == DEVICE_ID_DEFAULT) { + if (DEBUG) { + Slog.v(TAG, "Storing default device input method " + currentMethodId); + } + mSettings.putSelectedDefaultDeviceInputMethod(currentMethodId); + } + if (DEBUG) { + Slog.v(TAG, "Switching current input method from " + currentMethodId + + " to device-specific one " + deviceMethodId + " because the current display " + + mDisplayIdToShowIme + " belongs to device with id " + mDeviceIdToShowIme); + } + return deviceMethodId; + } + @GuardedBy("ImfLock.class") void invalidateAutofillSessionLocked() { mAutofillController.invalidateAutofillSession(); @@ -3241,6 +3331,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") void setInputMethodLocked(String id, int subtypeId) { + setInputMethodLocked(id, subtypeId, DEVICE_ID_DEFAULT); + } + + @GuardedBy("ImfLock.class") + void setInputMethodLocked(String id, int subtypeId, int deviceId) { InputMethodInfo info = mMethodMap.get(id); if (info == null) { throw getExceptionForUnknownImeId(id); @@ -3284,6 +3379,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } // Changing to a different IME. + if (mDeviceIdToShowIme != DEVICE_ID_DEFAULT && deviceId == DEVICE_ID_DEFAULT) { + // This change should only be applicable to the default device but the current input + // method is a custom one specific to a virtual device. So only update the settings + // entry used to restore the default device input method once we want to show the IME + // back on the default device. + mSettings.putSelectedDefaultDeviceInputMethod(id); + return; + } IInputMethodInvoker curMethod = getCurMethodLocked(); if (curMethod != null) { curMethod.removeStylusHandwritingWindow(); @@ -3538,7 +3641,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_HAS_IME); mCurStatsToken = null; - if (lastClickToolType != MotionEvent.TOOL_TYPE_UNKNOWN) { + if (!Flags.useHandwritingListenerForTooltype() + && lastClickToolType != MotionEvent.TOOL_TYPE_UNKNOWN) { curMethod.updateEditorToolType(lastClickToolType); } mVisibilityApplier.performShowIme(windowToken, statsToken, @@ -5318,11 +5422,21 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub StringBuilder builder = new StringBuilder(); if (mSettings.buildAndPutEnabledInputMethodsStrRemovingIdLocked( builder, enabledInputMethodsList, id)) { - // Disabled input method is currently selected, switch to another one. - final String selId = mSettings.getSelectedInputMethod(); - if (id.equals(selId) && !chooseNewDefaultIMELocked()) { - Slog.i(TAG, "Can't find new IME, unsetting the current input method."); - resetSelectedInputMethodAndSubtypeLocked(""); + if (mDeviceIdToShowIme == DEVICE_ID_DEFAULT) { + // Disabled input method is currently selected, switch to another one. + final String selId = mSettings.getSelectedInputMethod(); + if (id.equals(selId) && !chooseNewDefaultIMELocked()) { + Slog.i(TAG, "Can't find new IME, unsetting the current input method."); + resetSelectedInputMethodAndSubtypeLocked(""); + } + } else if (id.equals(mSettings.getSelectedDefaultDeviceInputMethod())) { + // Disabled default device IME while using a virtual device one, choose a + // new default one but only update the settings. + InputMethodInfo newDefaultIme = + InputMethodInfoUtils.getMostApplicableDefaultIME( + mSettings.getEnabledInputMethodListLocked()); + mSettings.putSelectedDefaultDeviceInputMethod( + newDefaultIme == null ? "" : newDefaultIme.getId()); } // Previous state was enabled. return true; @@ -5662,9 +5776,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @Override public void setVirtualDeviceInputMethodForAllUsers(int deviceId, @Nullable String imeId) { - // TODO(b/287269288): validate that id belongs to a valid virtual device instead. - Preconditions.checkArgument(deviceId != Context.DEVICE_ID_DEFAULT, - "DeviceId " + deviceId + " does not belong to a virtual device."); + Preconditions.checkArgument(deviceId != DEVICE_ID_DEFAULT, + TextUtils.formatSimple("DeviceId %d is not a virtual device id.", deviceId)); synchronized (ImfLock.class) { if (imeId == null) { mVirtualDeviceMethodMap.remove(deviceId); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java index a0b55edddec7..f9b06deb3142 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java @@ -51,6 +51,7 @@ import com.android.server.textservices.TextServicesManagerInternal; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.StringJoiner; import java.util.function.Consumer; import java.util.function.Predicate; @@ -241,6 +242,12 @@ final class InputMethodUtils { Slog.d(TAG, "--- Switch the current user from " + mCurrentUserId + " to " + userId); } mCurrentUserId = userId; + String ime = getSelectedInputMethod(); + String defaultDeviceIme = getSelectedDefaultDeviceInputMethod(); + if (defaultDeviceIme != null && !Objects.equals(ime, defaultDeviceIme)) { + putSelectedInputMethod(defaultDeviceIme); + putSelectedDefaultDeviceInputMethod(null); + } } private void putString(@NonNull String key, @Nullable String str) { @@ -636,6 +643,24 @@ final class InputMethodUtils { return imi; } + @Nullable + String getSelectedDefaultDeviceInputMethod() { + final String imi = getString(Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, null); + if (DEBUG) { + Slog.d(TAG, "getSelectedDefaultDeviceInputMethodStr: " + imi + ", " + + mCurrentUserId); + } + return imi; + } + + void putSelectedDefaultDeviceInputMethod(String imeId) { + if (DEBUG) { + Slog.d(TAG, "putSelectedDefaultDeviceInputMethodStr: " + imeId + ", " + + mCurrentUserId); + } + putString(Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, imeId); + } + void putDefaultVoiceInputMethod(String imeId) { if (DEBUG) { Slog.d(TAG, "putDefaultVoiceInputMethodStr: " + imeId + ", " + mCurrentUserId); diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java index ae889d8255c6..21e7befc1b89 100644 --- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java +++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java @@ -231,18 +231,21 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider } private boolean shouldBind() { - if (mRunning) { - boolean shouldBind = - mLastDiscoveryPreference != null - && !mLastDiscoveryPreference.getPreferredFeatures().isEmpty(); - if (mIsSelfScanOnlyProvider) { - shouldBind &= mLastDiscoveryPreferenceIncludesThisPackage; - } - shouldBind |= mIsManagerScanning; - shouldBind |= !getSessionInfos().isEmpty(); - return shouldBind; + if (!mRunning) { + return false; } - return false; + if (!getSessionInfos().isEmpty() || mIsManagerScanning) { + // We bind if any manager is scanning (regardless of whether an app is scanning) to give + // the opportunity for providers to publish routing sessions that were established + // directly between the app and the provider (typically via AndroidX MediaRouter). See + // b/176774510#comment20 for more information. + return true; + } + boolean anAppIsScanning = + mLastDiscoveryPreference != null + && !mLastDiscoveryPreference.getPreferredFeatures().isEmpty(); + return anAppIsScanning + && (mLastDiscoveryPreferenceIncludesThisPackage || !mIsSelfScanOnlyProvider); } private void bind() { diff --git a/services/core/java/com/android/server/notification/ZenAdapters.java b/services/core/java/com/android/server/notification/ZenAdapters.java index 2a65aff7f28d..91df04c4f2cb 100644 --- a/services/core/java/com/android/server/notification/ZenAdapters.java +++ b/services/core/java/com/android/server/notification/ZenAdapters.java @@ -16,6 +16,7 @@ package com.android.server.notification; +import android.app.Flags; import android.app.NotificationManager.Policy; import android.service.notification.ZenModeConfig; import android.service.notification.ZenPolicy; @@ -57,6 +58,12 @@ class ZenAdapters { .showStatusBarIcons(policy.showStatusBarIcons()); } + if (Flags.modesApi()) { + zenPolicyBuilder.allowChannels( + policy.allowPriorityChannels() + ? ZenPolicy.CHANNEL_TYPE_PRIORITY : ZenPolicy.CHANNEL_TYPE_NONE); + } + return zenPolicyBuilder.build(); } diff --git a/services/core/java/com/android/server/pdb/TEST_MAPPING b/services/core/java/com/android/server/pdb/TEST_MAPPING index 1aa8601bdcf9..9e9802354a4d 100644 --- a/services/core/java/com/android/server/pdb/TEST_MAPPING +++ b/services/core/java/com/android/server/pdb/TEST_MAPPING @@ -1,5 +1,5 @@ { - "postsubmit": [ + "presubmit": [ { "name": "FrameworksServicesTests", "options": [ diff --git a/services/core/java/com/android/server/pm/BroadcastHelper.java b/services/core/java/com/android/server/pm/BroadcastHelper.java index e830d288e204..e984e9c255da 100644 --- a/services/core/java/com/android/server/pm/BroadcastHelper.java +++ b/services/core/java/com/android/server/pm/BroadcastHelper.java @@ -262,6 +262,7 @@ public final class BroadcastHelper { // Deliver LOCKED_BOOT_COMPLETED first Intent lockedBcIntent = new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED) .setPackage(packageName); + lockedBcIntent.putExtra(Intent.EXTRA_USER_HANDLE, userId); if (includeStopped) { lockedBcIntent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); } @@ -275,6 +276,7 @@ public final class BroadcastHelper { // Deliver BOOT_COMPLETED only if user is unlocked if (mUmInternal.isUserUnlockingOrUnlocked(userId)) { Intent bcIntent = new Intent(Intent.ACTION_BOOT_COMPLETED).setPackage(packageName); + bcIntent.putExtra(Intent.EXTRA_USER_HANDLE, userId); if (includeStopped) { bcIntent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); } diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 3cb2420cd223..0555d90779e9 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -78,6 +78,7 @@ import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.AuxiliaryResolveInfo; import android.content.pm.ComponentInfo; +import android.content.pm.Flags; import android.content.pm.InstallSourceInfo; import android.content.pm.InstantAppRequest; import android.content.pm.InstantAppResolveInfo; @@ -1511,6 +1512,13 @@ public class ComputerEngine implements Computer { packageInfo.packageName = packageInfo.applicationInfo.packageName = resolveExternalPackageName(p); + if (Flags.provideInfoOfApkInApex()) { + final String apexModuleName = ps.getApexModuleName(); + if (apexModuleName != null) { + packageInfo.setApexPackageName( + mApexManager.getActivePackageNameForApexModuleName(apexModuleName)); + } + } return packageInfo; } else if ((flags & (MATCH_UNINSTALLED_PACKAGES | MATCH_ARCHIVED_PACKAGES)) != 0 && PackageUserStateUtils.isAvailable(state, flags)) { diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java index c920ca89b75e..588c6291f2f1 100644 --- a/services/core/java/com/android/server/pm/DeletePackageHelper.java +++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java @@ -487,7 +487,7 @@ final class DeletePackageHelper { // Do not uninstall the APK if an app should be cached boolean keepUninstalledPackage = mPm.shouldKeepUninstalledPackageLPr(packageName); - if (ps.isInstalledOrHasDataOnAnyOtherUser( + if (ps.isInstalledOnAnyOtherUser( mUserManagerInternal.getUserIds(), userId) || keepUninstalledPackage) { // Other users still have this package installed, so all // we need to do is clear this user's data and save that @@ -533,7 +533,7 @@ final class DeletePackageHelper { // artifacts are not stored in the same directory as the APKs deleteArtDexoptArtifacts(packageName); } - deleteInstalledPackageLIF(ps, deleteCodeAndResources, flags, allUserHandles, + deleteInstalledPackageLIF(ps, userId, deleteCodeAndResources, flags, allUserHandles, outInfo, writeSettings); } @@ -554,7 +554,7 @@ final class DeletePackageHelper { } @GuardedBy("mPm.mInstallLock") - private void deleteInstalledPackageLIF(PackageSetting ps, + private void deleteInstalledPackageLIF(PackageSetting ps, int userId, boolean deleteCodeAndResources, int flags, @NonNull int[] allUserHandles, @NonNull PackageRemovedInfo outInfo, boolean writeSettings) { synchronized (mPm.mLock) { @@ -567,7 +567,7 @@ final class DeletePackageHelper { // Delete package data from internal structures and also remove data if flag is set mRemovePackageHelper.removePackageDataLIF( - ps, allUserHandles, outInfo, flags, writeSettings); + ps, userId, allUserHandles, outInfo, flags, writeSettings); // Delete application code and resources only for parent packages if (deleteCodeAndResources) { @@ -677,8 +677,8 @@ final class DeletePackageHelper { flags |= PackageManager.DELETE_KEEP_DATA; } synchronized (mPm.mInstallLock) { - deleteInstalledPackageLIF(deletedPs, true, flags, allUserHandles, outInfo, - writeSettings); + deleteInstalledPackageLIF(deletedPs, UserHandle.USER_ALL, true, flags, allUserHandles, + outInfo, writeSettings); } } diff --git a/services/core/java/com/android/server/pm/InitAppsHelper.java b/services/core/java/com/android/server/pm/InitAppsHelper.java index 41d01762505d..22951d522b53 100644 --- a/services/core/java/com/android/server/pm/InitAppsHelper.java +++ b/services/core/java/com/android/server/pm/InitAppsHelper.java @@ -180,7 +180,9 @@ final class InitAppsHelper { // priority of system overlays. final ArrayMap<String, File> apkInApexPreInstalledPaths = new ArrayMap<>(); for (ApexManager.ActiveApexInfo apexInfo : mApexManager.getActiveApexInfos()) { - for (String packageName : mApexManager.getApksInApex(apexInfo.apexModuleName)) { + final String apexPackageName = mApexManager.getActivePackageNameForApexModuleName( + apexInfo.apexModuleName); + for (String packageName : mApexManager.getApksInApex(apexPackageName)) { apkInApexPreInstalledPaths.put(packageName, apexInfo.preInstalledApexPath); } } diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index b638d306544c..992d8eb8b1bb 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -3264,9 +3264,9 @@ final class InstallPackageHelper { Slog.e(TAG, "updateAllSharedLibrariesLPw failed: " + e.getMessage()); } } - mAppDataHelper.prepareAppDataAfterInstallLIF(pkg); - setPackageInstalledForSystemPackage(pkg, allUserHandles, origUserHandles, writeSettings); + + mAppDataHelper.prepareAppDataAfterInstallLIF(pkg); } private void setPackageInstalledForSystemPackage(@NonNull AndroidPackage pkg, @@ -4590,7 +4590,9 @@ final class InstallPackageHelper { private void assertPackageWithSharedUserIdIsPrivileged(AndroidPackage pkg) throws PackageManagerException { - if (!AndroidPackageLegacyUtils.isPrivileged(pkg) && (pkg.getSharedUserId() != null)) { + if (!AndroidPackageLegacyUtils.isPrivileged(pkg) + && (pkg.getSharedUserId() != null) + && !pkg.isLeavingSharedUser()) { SharedUserSetting sharedUserSetting = null; try { synchronized (mPm.mLock) { @@ -4630,7 +4632,8 @@ final class InstallPackageHelper { if (((scanFlags & SCAN_AS_PRIVILEGED) == 0) && !AndroidPackageLegacyUtils.isPrivileged(pkg) && (pkg.getSharedUserId() != null) - && !skipVendorPrivilegeScan) { + && !skipVendorPrivilegeScan + && !pkg.isLeavingSharedUser()) { SharedUserSetting sharedUserSetting = null; synchronized (mPm.mLock) { try { diff --git a/services/core/java/com/android/server/pm/ModuleInfoProvider.java b/services/core/java/com/android/server/pm/ModuleInfoProvider.java index 230f5558b37d..6561d462e716 100644 --- a/services/core/java/com/android/server/pm/ModuleInfoProvider.java +++ b/services/core/java/com/android/server/pm/ModuleInfoProvider.java @@ -18,6 +18,7 @@ package com.android.server.pm; import android.annotation.NonNull; import android.content.Context; +import android.content.pm.Flags; import android.content.pm.IPackageManager; import android.content.pm.ModuleInfo; import android.content.pm.PackageInfo; @@ -165,6 +166,10 @@ public class ModuleInfoProvider { mi.setApexModuleName( mApexManager.getApexModuleNameForPackageName(modulePackageName)); + if (Flags.provideInfoOfApkInApex()) { + mi.setApkInApexPackageNames(mApexManager.getApksInApex(modulePackageName)); + } + mModuleInfo.put(modulePackageName, mi); } } catch (XmlPullParserException | IOException e) { diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java index dcfc855da855..376b06105b8c 100644 --- a/services/core/java/com/android/server/pm/PackageArchiver.java +++ b/services/core/java/com/android/server/pm/PackageArchiver.java @@ -25,6 +25,7 @@ import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED import static android.content.pm.ArchivedActivityInfo.bytesFromBitmap; import static android.content.pm.ArchivedActivityInfo.drawableToBitmap; import static android.content.pm.PackageInstaller.EXTRA_UNARCHIVE_STATUS; +import static android.content.pm.PackageInstaller.STATUS_PENDING_USER_ACTION; import static android.content.pm.PackageInstaller.UNARCHIVAL_OK; import static android.content.pm.PackageManager.DELETE_ARCHIVE; import static android.content.pm.PackageManager.DELETE_KEEP_DATA; @@ -72,10 +73,12 @@ import android.os.Environment; import android.os.IBinder; import android.os.ParcelableException; import android.os.Process; +import android.os.RemoteException; import android.os.SELinux; import android.os.UserHandle; import android.text.TextUtils; import android.util.ExceptionUtils; +import android.util.Pair; import android.util.Slog; import com.android.internal.R; @@ -93,7 +96,9 @@ import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.concurrent.CompletableFuture; @@ -140,15 +145,23 @@ public class PackageArchiver { private final Context mContext; private final PackageManagerService mPm; + private final AppStateHelper mAppStateHelper; + @Nullable private LauncherApps mLauncherApps; @Nullable private AppOpsManager mAppOpsManager; + /* IntentSender store that maps key: {userId, appPackageName} to respective existing attached + unarchival intent sender. */ + private final Map<Pair<Integer, String>, IntentSender> mLauncherIntentSenders; + PackageArchiver(Context context, PackageManagerService mPm) { this.mContext = context; this.mPm = mPm; + this.mAppStateHelper = new AppStateHelper(mContext); + this.mLauncherIntentSenders = new HashMap<>(); } /** Returns whether a package is archived for a user. */ @@ -235,37 +248,32 @@ public class PackageArchiver { // Return early as the calling UID does not match caller package's UID. return START_CLASS_NOT_FOUND; } + String currentLauncherPackageName = getCurrentLauncherPackageName(userId); if ((currentLauncherPackageName == null || !callerPackageName.equals( currentLauncherPackageName)) && callingUid != Process.SHELL_UID) { // TODO(b/311619990): Remove dependency on SHELL_UID for testing Slog.e(TAG, TextUtils.formatSimple( - "callerPackageName: %s does not qualify for archival of package: " + "%s!", + "callerPackageName: %s does not qualify for unarchival of package: " + "%s!", callerPackageName, packageName)); return START_PERMISSION_DENIED; } - // TODO(b/302114464): Handle edge cases & also divert to a dialog based on - // permissions + compat options + Slog.i(TAG, TextUtils.formatSimple("Unarchival is starting for: %s", packageName)); - try { - final IIntentSender.Stub mLocalSender = new IIntentSender.Stub() { - @Override - public void send(int code, Intent intent, String resolvedType, - IBinder allowlistToken, - IIntentReceiver finishedReceiver, String requiredPermission, - Bundle options) { - // TODO(b/302114464): Handle intent sender status codes - } - }; + try { + // TODO(b/311709794) Make showUnarchivalConfirmation dependent on the compat options. requestUnarchive(packageName, callerPackageName, - new IntentSender((IIntentSender) mLocalSender), UserHandle.of(userId)); + getOrCreateUnarchiveIntentSender(userId, packageName), + UserHandle.of(userId), + false /* showUnarchivalConfirmation= */); } catch (Throwable t) { Slog.e(TAG, TextUtils.formatSimple( "Unexpected error occurred while unarchiving package %s: %s.", packageName, t.getLocalizedMessage())); return START_ABORTED; } + return START_SUCCESS; } @@ -321,6 +329,20 @@ public class PackageArchiver { return true; } + private IntentSender getOrCreateUnarchiveIntentSender(int userId, String packageName) { + Pair<Integer, String> key = Pair.create(userId, packageName); + synchronized (mLauncherIntentSenders) { + IntentSender intentSender = mLauncherIntentSenders.get(key); + if (intentSender != null) { + return intentSender; + } + IntentSender unarchiveIntentSender = new IntentSender( + (IIntentSender) new UnarchiveIntentSender()); + mLauncherIntentSenders.put(key, unarchiveIntentSender); + return unarchiveIntentSender; + } + } + /** Creates archived state for the package and user. */ private CompletableFuture<ArchiveState> createArchiveState(String packageName, int userId) throws PackageManager.NameNotFoundException { @@ -553,6 +575,15 @@ public class PackageArchiver { @NonNull String callerPackageName, @NonNull IntentSender statusReceiver, @NonNull UserHandle userHandle) { + requestUnarchive(packageName, callerPackageName, statusReceiver, userHandle, + false /* showUnarchivalConfirmation= */); + } + + private void requestUnarchive( + @NonNull String packageName, + @NonNull String callerPackageName, + @NonNull IntentSender statusReceiver, + @NonNull UserHandle userHandle, boolean showUnarchivalConfirmation) { Objects.requireNonNull(packageName); Objects.requireNonNull(callerPackageName); Objects.requireNonNull(statusReceiver); @@ -597,8 +628,8 @@ public class PackageArchiver { + "an unarchival."); } - if (!hasInstallPackages) { - requestUnarchiveConfirmation(packageName, statusReceiver); + if (!hasInstallPackages || showUnarchivalConfirmation) { + requestUnarchiveConfirmation(packageName, statusReceiver, userHandle); return; } @@ -622,7 +653,8 @@ public class PackageArchiver { () -> unarchiveInternal(packageName, userHandle, installerPackage, draftSessionId)); } - private void requestUnarchiveConfirmation(String packageName, IntentSender statusReceiver) { + private void requestUnarchiveConfirmation(String packageName, IntentSender statusReceiver, + UserHandle user) { final Intent dialogIntent = new Intent(ACTION_UNARCHIVE_DIALOG); dialogIntent.putExtra(EXTRA_UNARCHIVE_INTENT_SENDER, statusReceiver); dialogIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName); @@ -632,6 +664,7 @@ public class PackageArchiver { broadcastIntent.putExtra(EXTRA_UNARCHIVE_STATUS, PackageInstaller.STATUS_PENDING_USER_ACTION); broadcastIntent.putExtra(Intent.EXTRA_INTENT, dialogIntent); + broadcastIntent.putExtra(Intent.EXTRA_USER, user); sendIntent(statusReceiver, packageName, /* message= */ "", broadcastIntent); } @@ -656,6 +689,7 @@ public class PackageArchiver { int installerUid = mPm.snapshotComputer().getPackageUid(installerPackage, 0, userId); // Handles case of repeated unarchival calls for the same package. + // TODO(b/316881759) Allow attaching multiple intentSenders to one session. int existingSessionId = mPm.mInstallerService.getExistingDraftSessionId(installerUid, sessionParams, userId); @@ -849,7 +883,13 @@ public class PackageArchiver { void notifyUnarchivalListener(int status, String installerPackageName, String appPackageName, long requiredStorageBytes, @Nullable PendingIntent userActionIntent, - IntentSender unarchiveIntentSender, int userId) { + @Nullable IntentSender unarchiveIntentSender, int userId) { + if (unarchiveIntentSender == null) { + // Maybe this can happen if the installer calls reportUnarchivalStatus twice in quick + // succession. + return; + } + final Intent broadcastIntent = new Intent(); broadcastIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, appPackageName); broadcastIntent.putExtra(EXTRA_UNARCHIVE_STATUS, status); @@ -863,6 +903,7 @@ public class PackageArchiver { return; } broadcastIntent.putExtra(Intent.EXTRA_INTENT, dialogIntent); + broadcastIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(userId)); } final BroadcastOptions options = BroadcastOptions.makeBasic(); @@ -874,6 +915,10 @@ public class PackageArchiver { options.toBundle()); } catch (IntentSender.SendIntentException e) { Slog.e(TAG, TextUtils.formatSimple("Failed to send unarchive intent"), e); + } finally { + synchronized (mLauncherIntentSenders) { + mLauncherIntentSenders.remove(Pair.create(userId, appPackageName)); + } } } @@ -883,6 +928,7 @@ public class PackageArchiver { long requiredStorageBytes, PendingIntent userActionIntent, int userId) { final Intent dialogIntent = new Intent(ACTION_UNARCHIVE_ERROR_DIALOG); dialogIntent.putExtra(EXTRA_UNARCHIVE_STATUS, status); + dialogIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(userId)); if (requiredStorageBytes > 0) { dialogIntent.putExtra(EXTRA_REQUIRED_BYTES, requiredStorageBytes); } @@ -1118,4 +1164,25 @@ public class PackageArchiver { return activities.toArray(new ArchivedActivityParcel[activities.size()]); } + + private class UnarchiveIntentSender extends IIntentSender.Stub { + @Override + public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken, + IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) + throws RemoteException { + int status = intent.getExtras().getInt(PackageInstaller.EXTRA_UNARCHIVE_STATUS, + STATUS_PENDING_USER_ACTION); + if (status == UNARCHIVAL_OK) { + return; + } + Intent extraIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent.class); + UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER, UserHandle.class); + if (extraIntent != null && user != null + && mAppStateHelper.isAppTopVisible( + getCurrentLauncherPackageName(user.getIdentifier()))) { + extraIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivityAsUser(extraIntent, user); + } + } + } } diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java index 02ce15922272..45fc49a2e855 100644 --- a/services/core/java/com/android/server/pm/PackageSetting.java +++ b/services/core/java/com/android/server/pm/PackageSetting.java @@ -825,7 +825,7 @@ public class PackageSetting extends SettingBase implements PackageStateInternal return changed; } - boolean isInstalledOrHasDataOnAnyOtherUser(int[] allUsers, int currentUser) { + boolean isInstalledOnAnyOtherUser(int[] allUsers, int currentUser) { for (int user: allUsers) { if (user == currentUser) { continue; @@ -834,6 +834,16 @@ public class PackageSetting extends SettingBase implements PackageStateInternal if (userState.isInstalled()) { return true; } + } + return false; + } + + boolean hasDataOnAnyOtherUser(int[] allUsers, int currentUser) { + for (int user: allUsers) { + if (user == currentUser) { + continue; + } + final PackageUserStateInternal userState = readUserState(user); if (userState.dataExists()) { return true; } diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java index 285445361a29..7bd6a43969ba 100644 --- a/services/core/java/com/android/server/pm/RemovePackageHelper.java +++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java @@ -355,16 +355,22 @@ final class RemovePackageHelper { // Called to clean up disabled system packages public void removePackageData(final PackageSetting deletedPs, @NonNull int[] allUserHandles) { synchronized (mPm.mInstallLock) { - removePackageDataLIF(deletedPs, allUserHandles, new PackageRemovedInfo(), - /* flags= */ 0, /* writeSettings= */ false); + removePackageDataLIF(deletedPs, UserHandle.USER_ALL, allUserHandles, + new PackageRemovedInfo(), /* flags= */ 0, /* writeSettings= */ false); } } - /* + /** * This method deletes the package from internal data structures such as mPackages / mSettings. + * + * @param targetUserId indicates the target user of the deletion. It equals to + * {@link UserHandle.USER_ALL} if the deletion was initiated for all users, + * otherwise it equals to the specific user id that the deletion was meant + * for. */ @GuardedBy("mPm.mInstallLock") - public void removePackageDataLIF(final PackageSetting deletedPs, @NonNull int[] allUserHandles, + public void removePackageDataLIF(final PackageSetting deletedPs, int targetUserId, + @NonNull int[] allUserHandles, @NonNull PackageRemovedInfo outInfo, int flags, boolean writeSettings) { String packageName = deletedPs.getPackageName(); if (DEBUG_REMOVE) Slog.d(TAG, "removePackageDataLI: " + deletedPs); @@ -372,7 +378,7 @@ final class RemovePackageHelper { final AndroidPackage deletedPkg = deletedPs.getPkg(); // Delete all the data and states related to this package. - clearPackageStateForUserLIF(deletedPs, UserHandle.USER_ALL, flags); + clearPackageStateForUserLIF(deletedPs, targetUserId, flags); // Delete from mPackages removePackageLI(packageName, (flags & PackageManager.DELETE_CHATTY) != 0); @@ -384,7 +390,7 @@ final class RemovePackageHelper { deletedPs.setPkg(null); } - if ((flags & PackageManager.DELETE_KEEP_DATA) == 0) { + if (shouldDeletePackageSetting(deletedPs, targetUserId, allUserHandles, flags)) { // Delete from mSettings final SparseBooleanArray changedUsers = new SparseBooleanArray(); synchronized (mPm.mLock) { @@ -457,11 +463,32 @@ final class RemovePackageHelper { } } + private static boolean shouldDeletePackageSetting(PackageSetting deletedPs, int userId, + int[] allUserHandles, int flags) { + if ((flags & PackageManager.DELETE_KEEP_DATA) != 0) { + return false; + } + if (userId == UserHandle.USER_ALL) { + // Deleting for ALL. Let's wipe the PackageSetting. + return true; + } + if (deletedPs.hasDataOnAnyOtherUser(allUserHandles, userId)) { + // We arrived here because we are uninstalling the package for a specified user, and the + // package isn't installed on any other user. Before we proceed to completely delete the + // PackageSetting from mSettings, let's first check if data exists on any other user. + // If so, do not wipe the PackageSetting. + return false; + } + return true; + } + void cleanUpResources(@Nullable String packageName, @Nullable File codeFile, @Nullable String[] instructionSets) { synchronized (mPm.mInstallLock) { cleanUpResourcesLI(codeFile, instructionSets); } + // TODO: open logging to help debug, will delete or add debug flag + Slog.d(TAG, "cleanUpResources for " + codeFile); if (packageName == null) { return; } diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index d683855cc5d9..5d710d272fc9 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -1405,7 +1405,9 @@ public class PermissionManagerService extends IPermissionManager.Stub { case AppOpsManager.MODE_ERRORED: { if (permission.equals(Manifest.permission.BLUETOOTH_CONNECT)) { Slog.e(LOG_TAG, "BLUETOOTH_CONNECT permission hard denied as op" - + " mode is MODE_ERRORED for " + attributionSource); + + " mode is MODE_ERRORED. Permission check was requested for: " + + attributionSource + " and op transaction was invoked for " + + current); } return PermissionChecker.PERMISSION_HARD_DENIED; } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index edce3ec4b37c..f651dbf591d1 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -4211,6 +4211,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { return redoLayout; } + /** + * Shows the keyguard without immediately locking the device. + */ + @Override + public void showDismissibleKeyguard() { + mKeyguardDelegate.showDismissibleKeyguard(); + } + // There are several different flavors of "assistant" that can be launched from // various parts of the UI. diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java index 3016b39a7e6b..2174fd62ea00 100644 --- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java +++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java @@ -178,6 +178,12 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants { int applyKeyguardOcclusionChange(); /** + * Shows the keyguard immediately if not already shown. + * Does NOT immediately request the device to lock. + */ + void showDismissibleKeyguard(); + + /** * Interface to the Window Manager state associated with a particular * window. You can hold on to an instance of this interface from the call * to prepareAddWindow() until removeWindow(). diff --git a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java index 495e239d4cd7..d0b70c391579 100644 --- a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java +++ b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java @@ -410,6 +410,15 @@ public class KeyguardServiceDelegate { } } + /** + * Request to show the keyguard immediately without immediately locking the device. + */ + public void showDismissibleKeyguard() { + if (mKeyguardService != null) { + mKeyguardService.showDismissibleKeyguard(); + } + } + public void setCurrentUser(int newUserId) { if (mKeyguardService != null) { mKeyguardService.setCurrentUser(newUserId); diff --git a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java index 774e26178db3..cd789eaed1b3 100644 --- a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java +++ b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java @@ -209,6 +209,16 @@ public class KeyguardServiceWrapper implements IKeyguardService { } } + // Binder interface + @Override + public void showDismissibleKeyguard() { + try { + mService.showDismissibleKeyguard(); + } catch (RemoteException e) { + Slog.w(TAG , "Remote Exception", e); + } + } + @Override // Binder interface public void setSwitchingUser(boolean switching) { try { diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java index 3ecc9853be91..652cf18ed257 100644 --- a/services/core/java/com/android/server/power/Notifier.java +++ b/services/core/java/com/android/server/power/Notifier.java @@ -411,6 +411,11 @@ public class Notifier { mWakeLockLog.onWakeLockReleased(tag, ownerUid); } + /** Shows the keyguard without requesting the device to immediately lock. */ + public void showDismissibleKeyguard() { + mPolicy.showDismissibleKeyguard(); + } + private int getBatteryStatsWakeLockMonitorType(int flags) { switch (flags & PowerManager.WAKE_LOCK_LEVEL_MASK) { case PowerManager.PARTIAL_WAKE_LOCK: @@ -958,7 +963,8 @@ public class Notifier { final boolean vibrate = Settings.Secure.getIntForUser(mContext.getContentResolver(), Settings.Secure.CHARGING_VIBRATION_ENABLED, 1, userId) != 0; if (vibrate) { - mVibrator.vibrate(CHARGING_VIBRATION_EFFECT, + mVibrator.vibrate(Process.SYSTEM_UID, mContext.getOpPackageName(), + CHARGING_VIBRATION_EFFECT, /* reason= */ "Charging started", HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES); } diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index ec5172fca5fa..2128c991c273 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -115,6 +115,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.IBatteryStats; import com.android.internal.display.BrightnessSynchronizer; +import com.android.internal.foldables.FoldGracePeriodProvider; import com.android.internal.os.BackgroundThread; import com.android.internal.util.DumpUtils; import com.android.internal.util.FrameworkStatsLog; @@ -308,6 +309,7 @@ public final class PowerManagerService extends SystemService private final Context mContext; private final ServiceThread mHandlerThread; private final Handler mHandler; + private final FoldGracePeriodProvider mFoldGracePeriodProvider; private final AmbientDisplayConfiguration mAmbientDisplayConfiguration; @Nullable private final BatterySaverStateMachine mBatterySaverStateMachine; @@ -1007,6 +1009,10 @@ public final class PowerManagerService extends SystemService return new InattentiveSleepWarningController(); } + FoldGracePeriodProvider createFoldGracePeriodProvider() { + return new FoldGracePeriodProvider(); + } + public SystemPropertiesWrapper createSystemPropertiesWrapper() { return new SystemPropertiesWrapper() { @Override @@ -1147,6 +1153,7 @@ public final class PowerManagerService extends SystemService mHandler = injector.createHandler(mHandlerThread.getLooper(), new PowerManagerHandlerCallback()); mConstants = new Constants(mHandler); + mFoldGracePeriodProvider = injector.createFoldGracePeriodProvider(); mAmbientDisplayConfiguration = mInjector.createAmbientDisplayConfiguration(context); mAmbientDisplaySuppressionController = mInjector.createAmbientDisplaySuppressionController( @@ -6933,8 +6940,15 @@ public final class PowerManagerService extends SystemService + ") doesn't exist"); } if ((flags & PowerManager.GO_TO_SLEEP_FLAG_SOFT_SLEEP) != 0) { - if (powerGroup.hasWakeLockKeepingScreenOnLocked()) { - continue; + if (mFoldGracePeriodProvider.isEnabled()) { + if (!powerGroup.hasWakeLockKeepingScreenOnLocked()) { + mNotifier.showDismissibleKeyguard(); + } + continue; // never actually goes to sleep for SOFT_SLEEP + } else { + if (powerGroup.hasWakeLockKeepingScreenOnLocked()) { + continue; + } } } if (isNoDoze) { diff --git a/services/core/java/com/android/server/trust/TEST_MAPPING b/services/core/java/com/android/server/trust/TEST_MAPPING index fa46acd9c39b..0de7c28c209b 100644 --- a/services/core/java/com/android/server/trust/TEST_MAPPING +++ b/services/core/java/com/android/server/trust/TEST_MAPPING @@ -12,6 +12,19 @@ ] } ], + "postsubmit": [ + { + "name": "FrameworksMockingServicesTests", + "options": [ + { + "include-filter": "com.android.server.trust" + }, + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + } + ], "trust-tablet": [ { "name": "TrustTests", diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java index 9a85c42e1a10..5c603c2238bd 100644 --- a/services/core/java/com/android/server/trust/TrustManagerService.java +++ b/services/core/java/com/android/server/trust/TrustManagerService.java @@ -74,6 +74,7 @@ import android.view.IWindowManager; import android.view.WindowManagerGlobal; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.PackageMonitor; import com.android.internal.infra.AndroidFuture; import com.android.internal.util.DumpUtils; @@ -1758,6 +1759,11 @@ public class TrustManagerService extends SystemService { } }; + @VisibleForTesting + void waitForIdle() { + mHandler.runWithScissors(() -> {}, 0); + } + private boolean isTrustUsuallyManagedInternal(int userId) { synchronized (mTrustUsuallyManagedForUser) { int i = mTrustUsuallyManagedForUser.indexOfKey(userId); diff --git a/services/core/java/com/android/server/utils/AnrTimer.java b/services/core/java/com/android/server/utils/AnrTimer.java index 7b5192c4bd6b..e3aba0f6bc6f 100644 --- a/services/core/java/com/android/server/utils/AnrTimer.java +++ b/services/core/java/com/android/server/utils/AnrTimer.java @@ -16,21 +16,30 @@ package com.android.server.utils; +import static android.text.TextUtils.formatSimple; + import android.annotation.NonNull; import android.os.Handler; import android.os.Message; import android.os.SystemClock; import android.os.Trace; +import android.text.TextUtils; import android.text.format.TimeMigrationUtils; +import android.util.ArrayMap; import android.util.IndentingPrintWriter; import android.util.Log; +import android.util.LongSparseArray; +import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.Keep; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.RingBuffer; +import java.lang.ref.WeakReference; import java.io.PrintWriter; import java.util.Arrays; +import java.util.ArrayList; import java.util.Objects; /** @@ -60,9 +69,14 @@ import java.util.Objects; * is restarted with the extension timeout. If extensions are disabled or if the extension is zero, * the client process is notified of the expiration. * + * <p>Instances use native resources but not system resources when the feature is enabled. + * Instances should be explicitly closed unless they are being closed as part of process + * exit. (So, instances in system server generally need not be explicitly closed since they are + * created during process start and will last until process exit.) + * * @hide */ -public class AnrTimer<V> { +public class AnrTimer<V> implements AutoCloseable { /** * The log tag. @@ -87,6 +101,12 @@ public class AnrTimer<V> { 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. */ @@ -103,6 +123,9 @@ public class AnrTimer<V> { } } + /** The default injector. */ + private static final Injector sDefaultInjector = new Injector(); + /** * An error is defined by its issue, the operation that detected the error, the tag of the * affected service, a short stack of the bad call, and the stringified arg associated with @@ -160,41 +183,46 @@ public class AnrTimer<V> { /** A lock for the AnrTimer instance. */ private final Object mLock = new Object(); - /** - * The total number of timers started. - */ + /** The map from client argument to the associated timer ID. */ + @GuardedBy("mLock") + private final ArrayMap<V, Integer> mTimerIdMap = new ArrayMap<>(); + + /** Reverse map from timer ID to client argument. */ + @GuardedBy("mLock") + private final SparseArray<V> mTimerArgMap = new SparseArray<>(); + + /** The highwater mark of started, but not closed, timers. */ + @GuardedBy("mLock") + private int mMaxStarted = 0; + + /** The total number of timers started. */ @GuardedBy("mLock") private int mTotalStarted = 0; - /** - * The total number of errors detected. - */ + /** The total number of errors detected. */ @GuardedBy("mLock") private int mTotalErrors = 0; - /** - * The handler for messages sent from this instance. - */ + /** The total number of timers that have expired. */ + @GuardedBy("mLock") + private int mTotalExpired = 0; + + /** The handler for messages sent from this instance. */ private final Handler mHandler; - /** - * The message type for messages sent from this interface. - */ + /** The message type for messages sent from this interface. */ private final int mWhat; - /** - * A label that identifies the AnrTimer associated with a Timer in log messages. - */ + /** A label that identifies the AnrTimer associated with a Timer in log messages. */ private final String mLabel; - /** - * Whether this timer instance supports extending timeouts. - */ + /** Whether this timer instance supports extending timeouts. */ private final boolean mExtend; - /** - * The top-level switch for the feature enabled or disabled. - */ + /** The injector used to create this instance. This is only used for testing. */ + private final Injector mInjector; + + /** The top-level switch for the feature enabled or disabled. */ private final FeatureSwitch mFeature; /** @@ -223,7 +251,27 @@ public class AnrTimer<V> { mWhat = what; mLabel = label; mExtend = extend; - mFeature = new FeatureDisabled(); + mInjector = injector; + boolean enabled = mInjector.anrTimerServiceEnabled() && nativeTimersSupported(); + mFeature = createFeatureSwitch(enabled); + } + + // Return the correct feature. FeatureEnabled is returned if and only if the feature is + // flag-enabled and if the native shadow was successfully created. Otherwise, FeatureDisabled + // is returned. + private FeatureSwitch createFeatureSwitch(boolean enabled) { + if (!enabled) { + return new FeatureDisabled(); + } else { + try { + return new FeatureEnabled(); + } catch (RuntimeException e) { + // Something went wrong in the native layer. Log the error and fall back on the + // feature-disabled logic. + Log.e(TAG, e.toString()); + return new FeatureDisabled(); + } + } } /** @@ -245,7 +293,7 @@ public class AnrTimer<V> { * @param extend A flag to indicate if expired timers can be granted extensions. */ public AnrTimer(@NonNull Handler handler, int what, @NonNull String label, boolean extend) { - this(handler, what, label, extend, new Injector()); + this(handler, what, label, extend, sDefaultInjector); } /** @@ -272,19 +320,44 @@ public class AnrTimer<V> { } /** + * Start a trace on the timer. The trace is laid down in the AnrTimerTrack. + */ + 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); + } + } + + /** + * End a trace on the timer. + */ + private void traceEnd(int timerId) { + if (ENABLE_TRACING) { + final int cookie = timerId; + Trace.asyncTraceForTrackEnd(TRACE_TAG, TRACK, cookie); + } + } + + /** * The FeatureSwitch class provides a quick switch between feature-enabled behavior and * feature-disabled behavior. */ private abstract class FeatureSwitch { abstract void start(@NonNull V arg, int pid, int uid, long timeoutMs); - abstract void cancel(@NonNull V arg); + abstract boolean cancel(@NonNull V arg); - abstract void accept(@NonNull V arg); + abstract boolean accept(@NonNull V arg); - abstract void discard(@NonNull V arg); + abstract boolean discard(@NonNull V arg); abstract boolean enabled(); + + abstract void dump(PrintWriter pw, boolean verbose); + + abstract void close(); } /** @@ -301,18 +374,21 @@ public class AnrTimer<V> { /** Cancel a timer by removing the message from the client's handler. */ @Override - void cancel(@NonNull V arg) { + boolean cancel(@NonNull V arg) { mHandler.removeMessages(mWhat, arg); + return true; } /** accept() is a no-op when the feature is disabled. */ @Override - void accept(@NonNull V arg) { + boolean accept(@NonNull V arg) { + return true; } /** discard() is a no-op when the feature is disabled. */ @Override - void discard(@NonNull V arg) { + boolean discard(@NonNull V arg) { + return true; } /** The feature is not enabled. */ @@ -320,12 +396,179 @@ public class AnrTimer<V> { boolean enabled() { return false; } + + /** dump() is a no-op when the feature is disabled. */ + @Override + void dump(PrintWriter pw, boolean verbose) { + } + + /** close() is a no-op when the feature is disabled. */ + @Override + void close() { + } + } + + /** + * A static list of AnrTimer instances. The list is traversed by dumpsys. Only instances + * using native resources are included. + */ + @GuardedBy("sAnrTimerList") + private static final LongSparseArray<WeakReference<AnrTimer>> sAnrTimerList = + new LongSparseArray<>(); + + /** + * The FeatureEnabled class enables the AnrTimer logic. It is used when the AnrTimer service + * is enabled via Flags.anrTimerServiceEnabled. + */ + private class FeatureEnabled extends FeatureSwitch { + + /** + * The native timer that supports this instance. The value is set to non-zero when the + * native timer is created and it is set back to zero when the native timer is freed. + */ + private long mNative = 0; + + /** Fetch the native tag (an integer) for the given label. */ + FeatureEnabled() { + mNative = nativeAnrTimerCreate(mLabel); + if (mNative == 0) throw new IllegalArgumentException("unable to create native timer"); + synchronized (sAnrTimerList) { + sAnrTimerList.put(mNative, new WeakReference(AnrTimer.this)); + } + } + + /** + * Start a timer. + */ + @Override + void start(@NonNull V arg, int pid, int uid, long timeoutMs) { + synchronized (mLock) { + if (mTimerIdMap.containsKey(arg)) { + // There is an existing timer. Cancel it. + cancel(arg); + } + int timerId = nativeAnrTimerStart(mNative, pid, uid, timeoutMs, mExtend); + if (timerId > 0) { + mTimerIdMap.put(arg, timerId); + mTimerArgMap.put(timerId, arg); + mTotalStarted++; + mMaxStarted = Math.max(mMaxStarted, mTimerIdMap.size()); + } else { + throw new RuntimeException("unable to start timer"); + } + } + } + + /** + * Cancel a timer. No error is reported if the timer is not found because some clients + * cancel timers from common code that runs even if a timer was never started. + */ + @Override + boolean cancel(@NonNull V arg) { + synchronized (mLock) { + Integer timer = removeLocked(arg); + if (timer == null) { + return false; + } + if (!nativeAnrTimerCancel(mNative, timer)) { + // There may be an expiration message in flight. Cancel it. + mHandler.removeMessages(mWhat, arg); + return false; + } + return true; + } + } + + /** + * Accept a timer in the framework-level handler. The timeout has been accepted and the + * timeout handler is executing. + */ + @Override + boolean accept(@NonNull V arg) { + synchronized (mLock) { + Integer timer = removeLocked(arg); + if (timer == null) { + notFoundLocked("accept", arg); + return false; + } + nativeAnrTimerAccept(mNative, timer); + traceEnd(timer); + return true; + } + } + + /** + * Discard a timer in the framework-level handler. For whatever reason, the timer is no + * longer interesting. No statistics are collected. Return false if the time was not + * found. + */ + @Override + boolean discard(@NonNull V arg) { + synchronized (mLock) { + Integer timer = removeLocked(arg); + if (timer == null) { + notFoundLocked("discard", arg); + return false; + } + nativeAnrTimerDiscard(mNative, timer); + traceEnd(timer); + return true; + } + } + + /** The feature is enabled. */ + @Override + boolean enabled() { + return true; + } + + /** Dump statistics from the native layer. */ + @Override + void dump(PrintWriter pw, boolean verbose) { + synchronized (mLock) { + if (mNative != 0) { + nativeAnrTimerDump(mNative, verbose); + } else { + pw.println("closed"); + } + } + } + + /** Free native resources. */ + @Override + void close() { + // Remove self from the list of active timers. + synchronized (sAnrTimerList) { + sAnrTimerList.remove(mNative); + } + synchronized (mLock) { + if (mNative != 0) nativeAnrTimerClose(mNative); + mNative = 0; + } + } + + /** + * Delete the entries associated with arg from the maps and return the ID of the timer, if + * any. + */ + @GuardedBy("mLock") + private Integer removeLocked(V arg) { + Integer r = mTimerIdMap.remove(arg); + if (r != null) { + synchronized (mTimerArgMap) { + mTimerArgMap.remove(r); + } + } + return r; + } } /** * Start a timer associated with arg. The same object must be used to cancel, accept, or * discard a timer later. If a timer already exists with the same arg, then the existing timer - * is canceled and a new timer is created. + * is canceled and a new timer is created. The timeout is signed but negative delays are + * nonsensical. Rather than throw an exception, timeouts less than 0ms are forced to 0ms. This + * allows a client to deliver an immediate timeout via the AnrTimer. * * @param arg The key by which the timer is known. This is never examined or modified. * @param pid The Linux process ID of the target being timed. @@ -333,25 +576,39 @@ public class AnrTimer<V> { * @param timeoutMs The timer timeout, in milliseconds. */ public void start(@NonNull V arg, int pid, int uid, long timeoutMs) { + if (timeoutMs < 0) timeoutMs = 0; mFeature.start(arg, pid, uid, timeoutMs); } /** * Cancel the running timer associated with arg. The timer is forgotten. If the timer has - * expired, the call is treated as a discard. No errors are reported if the timer does not - * exist or if the timer has expired. + * expired, the call is treated as a discard. The function returns true if a running timer was + * found, and false if an expired timer was found or if no timer was found. After this call, + * the timer does not exist. + * + * Note: the return value is always true if the feature is not enabled. + * + * @param arg The key by which the timer is known. This is never examined or modified. + * @return True if a running timer was canceled. */ - public void cancel(@NonNull V arg) { - mFeature.cancel(arg); + public boolean cancel(@NonNull V arg) { + return mFeature.cancel(arg); } /** * Accept the expired timer associated with arg. This indicates that the caller considers the - * timer expiration to be a true ANR. (See {@link #discard} for an alternate response.) It is - * an error to accept a running timer, however the running timer will be canceled. + * timer expiration to be a true ANR. (See {@link #discard} for an alternate response.) The + * function returns true if an expired timer was found and false if a running timer was found or + * if no timer was found. After this call, the timer does not exist. It is an error to accept + * a running timer, however, the running timer will be canceled. + * + * Note: the return value is always true if the feature is not enabled. + * + * @param arg The key by which the timer is known. This is never examined or modified. + * @return True if an expired timer was accepted. */ - public void accept(@NonNull V arg) { - mFeature.accept(arg); + public boolean accept(@NonNull V arg) { + return mFeature.accept(arg); } /** @@ -359,11 +616,57 @@ public class AnrTimer<V> { * timer expiration to be a false ANR. ((See {@link #accept} for an alternate response.) One * reason to discard an expired timer is if the process being timed was also being debugged: * such a process could be stopped at a breakpoint and its failure to respond would not be an - * error. It is an error to discard a running timer, however the running timer will be - * canceled. + * error. After this call thie timer does not exist. It is an error to discard a running timer, + * however the running timer will be canceled. + * + * Note: the return value is always true if the feature is not enabled. + * + * @param arg The key by which the timer is known. This is never examined or modified. + * @return True if an expired timer was discarded. + */ + public boolean discard(@NonNull V arg) { + return mFeature.discard(arg); + } + + /** + * 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). + */ + @Keep + private boolean expire(int timerId, int pid, int uid) { + traceBegin(timerId, pid, uid, "expired"); + V arg = null; + synchronized (mLock) { + arg = mTimerArgMap.get(timerId); + if (arg == null) { + Log.e(TAG, formatSimple("failed to expire timer %s:%d : arg not found", + mLabel, timerId)); + mTotalErrors++; + return false; + } + mTotalExpired++; + } + mHandler.sendMessage(Message.obtain(mHandler, mWhat, arg)); + return true; + } + + /** + * Close the object and free any native resources. */ - public void discard(@NonNull V arg) { - mFeature.discard(arg); + public void close() { + mFeature.close(); + } + + /** + * Ensure any native resources are freed when the object is GC'ed. Best practice is to close + * the object explicitly, but overriding finalize() avoids accidental leaks. + */ + @SuppressWarnings("Finalize") + @Override + protected void finalize() throws Throwable { + close(); + super.finalize(); } /** @@ -373,8 +676,11 @@ public class AnrTimer<V> { synchronized (mLock) { pw.format("timer: %s\n", mLabel); pw.increaseIndent(); - pw.format("started=%d errors=%d\n", mTotalStarted, mTotalErrors); + pw.format("started=%d maxStarted=%d running=%d expired=%d errors=%d\n", + mTotalStarted, mMaxStarted, mTimerIdMap.size(), + mTotalExpired, mTotalErrors); pw.decreaseIndent(); + mFeature.dump(pw, false); } } @@ -386,6 +692,13 @@ public class AnrTimer<V> { } /** + * The current time in milliseconds. + */ + private static long now() { + return SystemClock.uptimeMillis(); + } + + /** * Dump all errors to the output stream. */ private static void dumpErrors(IndentingPrintWriter ipw) { @@ -422,23 +735,89 @@ public class AnrTimer<V> { mTotalErrors++; } - /** - * Log an error about a timer not found. - */ + /** Record an error about a timer not found. */ @GuardedBy("mLock") private void notFoundLocked(String operation, Object arg) { recordErrorLocked(operation, "notFound", arg); } - /** - * Dumpsys output. - */ - public static void dump(@NonNull PrintWriter pw, boolean verbose) { + /** Dumpsys output, allowing for overrides. */ + @VisibleForTesting + static void dump(@NonNull PrintWriter pw, boolean verbose, @NonNull Injector injector) { + if (!injector.anrTimerServiceEnabled()) return; + final IndentingPrintWriter ipw = new IndentingPrintWriter(pw); ipw.println("AnrTimer statistics"); ipw.increaseIndent(); + synchronized (sAnrTimerList) { + final int size = sAnrTimerList.size(); + ipw.println("reporting " + size + " timers"); + for (int i = 0; i < size; i++) { + AnrTimer a = sAnrTimerList.valueAt(i).get(); + if (a != null) a.dump(ipw); + } + } if (verbose) dumpErrors(ipw); ipw.format("AnrTimerEnd\n"); ipw.decreaseIndent(); } + + /** Dumpsys output. There is no output if the feature is not enabled. */ + public static void dump(@NonNull PrintWriter pw, boolean verbose) { + dump(pw, verbose, sDefaultInjector); + } + + /** + * Return true if the native timers are supported. Native timers are supported if the method + * nativeAnrTimerSupported() can be executed and it returns true. + */ + private static boolean nativeTimersSupported() { + try { + return nativeAnrTimerSupported(); + } catch (java.lang.UnsatisfiedLinkError e) { + return false; + } + } + + /** + * Native methods + */ + + /** Return true if the native AnrTimer code is operational. */ + private static native boolean nativeAnrTimerSupported(); + + /** + * Create a new native timer with the given key and name. The key is not used by the native + * code but it is returned to the Java layer in the expiration handler. The name is only for + * logging. Unlike the other methods, this is an instance method: the "this" parameter is + * passed into the native layer. + */ + private native long nativeAnrTimerCreate(String name); + + /** Release the native resources. No further operations are premitted. */ + private static native int nativeAnrTimerClose(long service); + + /** Start a timer and return its ID. Zero is returned on error. */ + private static native int nativeAnrTimerStart(long service, int pid, int uid, long timeoutMs, + boolean extend); + + /** + * Cancel a timer by ID. Return true if the timer was running and canceled. Return false if + * the timer was not found or if the timer had already expired. + */ + private static native boolean nativeAnrTimerCancel(long service, int timerId); + + /** Accept an expired timer by ID. Return true if the timer was found. */ + private static native boolean nativeAnrTimerAccept(long service, int timerId); + + /** Discard an expired timer by ID. Return true if the timer was found. */ + private static native boolean nativeAnrTimerDiscard(long service, int timerId); + + /** 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/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index 1577cef9de00..2d584c428d4c 100644 --- a/services/core/java/com/android/server/wm/AccessibilityController.java +++ b/services/core/java/com/android/server/wm/AccessibilityController.java @@ -98,6 +98,8 @@ import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import com.android.internal.R; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.SomeArgs; import com.android.internal.util.TraceBuffer; import com.android.internal.util.function.pooled.PooledLambda; @@ -297,6 +299,18 @@ final class AccessibilityController { } } + /** It is only used by unit test. */ + @VisibleForTesting + Surface forceShowMagnifierSurface(int displayId) { + final DisplayMagnifier displayMagnifier = mDisplayMagnifiers.get(displayId); + if (displayMagnifier != null) { + displayMagnifier.mMagnifedViewport.mWindow.setAlpha(DisplayMagnifier.MagnifiedViewport + .ViewportWindow.AnimationController.MAX_ALPHA); + return displayMagnifier.mMagnifedViewport.mWindow.mSurface; + } + return null; + } + void onWindowLayersChanged(int displayId) { if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK | FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK)) { @@ -448,6 +462,7 @@ final class AccessibilityController { } } + // TODO(b/318327737): Remove parameter 't' when removing flag DRAW_IN_WM_LOCK. void drawMagnifiedRegionBorderIfNeeded(int displayId, SurfaceControl.Transaction t) { if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) { mAccessibilityTracing.logTrace( @@ -1106,11 +1121,19 @@ final class AccessibilityController { } void setMagnifiedRegionBorderShown(boolean shown, boolean animate) { - if (shown) { + if (ViewportWindow.DRAW_IN_WM_LOCK) { + if (shown) { + mFullRedrawNeeded = true; + mOldMagnificationRegion.set(0, 0, 0, 0); + } + mWindow.setShown(shown, animate); + return; + } + if (mWindow.setShown(shown, animate)) { mFullRedrawNeeded = true; + // Clear the old region, so recomputeBounds will refresh the current region. mOldMagnificationRegion.set(0, 0, 0, 0); } - mWindow.setShown(shown, animate); } void getMagnifiedFrameInContentCoords(Rect rect) { @@ -1130,7 +1153,11 @@ final class AccessibilityController { void drawWindowIfNeeded(SurfaceControl.Transaction t) { recomputeBounds(); - mWindow.drawIfNeeded(t); + if (ViewportWindow.DRAW_IN_WM_LOCK) { + mWindow.drawOrRemoveIfNeeded(t); + return; + } + mWindow.postDrawIfNeeded(); } void destroyWindow() { @@ -1158,23 +1185,28 @@ final class AccessibilityController { mWindow.dump(pw, prefix); } - private final class ViewportWindow { + private final class ViewportWindow implements Runnable { private static final String SURFACE_TITLE = "Magnification Overlay"; + // TODO(b/318327737): Remove if it is stable. + static final boolean DRAW_IN_WM_LOCK = !Flags.drawMagnifierBorderOutsideWmlock(); private final Region mBounds = new Region(); private final Rect mDirtyRect = new Rect(); private final Paint mPaint = new Paint(); private final SurfaceControl mSurfaceControl; + /** After initialization, it should only be accessed from animation thread. */ + private final SurfaceControl.Transaction mTransaction; private final BLASTBufferQueue mBlastBufferQueue; private final Surface mSurface; private final AnimationController mAnimationController; private boolean mShown; + private boolean mLastSurfaceShown; private int mAlpha; - private boolean mInvalidated; + private volatile boolean mInvalidated; ViewportWindow(Context context) { SurfaceControl surfaceControl = null; @@ -1202,6 +1234,7 @@ final class AccessibilityController { InputMonitor.setTrustedOverlayInputInfo(mSurfaceControl, t, mDisplayContent.getDisplayId(), "Magnification Overlay"); t.apply(); + mTransaction = t; mSurface = mBlastBufferQueue.createSurface(); mAnimationController = new AnimationController(context, @@ -1219,10 +1252,11 @@ final class AccessibilityController { mInvalidated = true; } - void setShown(boolean shown, boolean animate) { + /** Returns {@code true} if the state is changed to shown. */ + boolean setShown(boolean shown, boolean animate) { synchronized (mService.mGlobalLock) { if (mShown == shown) { - return; + return false; } mShown = shown; mAnimationController.onFrameShownStateChanged(shown, animate); @@ -1230,6 +1264,7 @@ final class AccessibilityController { Slog.i(LOG_TAG, "ViewportWindow shown: " + mShown); } } + return shown; } @SuppressWarnings("unused") @@ -1285,7 +1320,22 @@ final class AccessibilityController { mService.scheduleAnimationLocked(); } - void drawIfNeeded(SurfaceControl.Transaction t) { + void postDrawIfNeeded() { + if (mInvalidated) { + mService.mAnimationHandler.post(this); + } + } + + @Override + public void run() { + drawOrRemoveIfNeeded(mTransaction); + } + + /** + * This method must only be called by animation handler directly to make sure + * thread safe and there is no lock held outside. + */ + private void drawOrRemoveIfNeeded(SurfaceControl.Transaction t) { // Drawing variables (alpha, dirty rect, and bounds) access is synchronized // using WindowManagerGlobalLock. Grab copies of these values before // drawing on the canvas so that drawing can be performed outside of the lock. @@ -1293,6 +1343,14 @@ final class AccessibilityController { Rect drawingRect = null; Region drawingBounds = null; synchronized (mService.mGlobalLock) { + if (!DRAW_IN_WM_LOCK && mBlastBufferQueue.mNativeObject == 0) { + // Complete removal since releaseSurface has been called. + if (mSurface.isValid()) { + mTransaction.remove(mSurfaceControl).apply(); + mSurface.release(); + } + return; + } if (!mInvalidated) { return; } @@ -1314,6 +1372,7 @@ final class AccessibilityController { } } + final boolean showSurface; // Draw without holding WindowManagerGlobalLock. if (alpha > 0) { Canvas canvas = null; @@ -1329,18 +1388,38 @@ final class AccessibilityController { mPaint.setAlpha(alpha); canvas.drawPath(drawingBounds.getBoundaryPath(), mPaint); mSurface.unlockCanvasAndPost(canvas); - t.show(mSurfaceControl); + if (DRAW_IN_WM_LOCK) { + t.show(mSurfaceControl); + return; + } + showSurface = true; } else { - t.hide(mSurfaceControl); + if (DRAW_IN_WM_LOCK) { + t.hide(mSurfaceControl); + return; + } + showSurface = false; + } + + if (showSurface && !mLastSurfaceShown) { + mTransaction.show(mSurfaceControl).apply(); + mLastSurfaceShown = true; + } else if (!showSurface && mLastSurfaceShown) { + mTransaction.hide(mSurfaceControl).apply(); + mLastSurfaceShown = false; } } + @GuardedBy("mService.mGlobalLock") void releaseSurface() { - if (mBlastBufferQueue != null) { - mBlastBufferQueue.destroy(); + mBlastBufferQueue.destroy(); + if (DRAW_IN_WM_LOCK) { + mService.mTransactionFactory.get().remove(mSurfaceControl).apply(); + mSurface.release(); + return; } - mService.mTransactionFactory.get().remove(mSurfaceControl).apply(); - mSurface.release(); + // Post to perform cleanup on the thread which handles mSurface. + mService.mAnimationHandler.post(this); } void dump(PrintWriter pw, String prefix) { diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index a43e7d533240..febcc052064e 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -183,6 +183,8 @@ import static com.android.server.wm.ActivityRecordProto.PROVIDES_MAX_BOUNDS; import static com.android.server.wm.ActivityRecordProto.REPORTED_DRAWN; import static com.android.server.wm.ActivityRecordProto.REPORTED_VISIBLE; import static com.android.server.wm.ActivityRecordProto.SHOULD_FORCE_ROTATE_FOR_CAMERA_COMPAT; +import static com.android.server.wm.ActivityRecordProto.SHOULD_IGNORE_ORIENTATION_REQUEST_LOOP; +import static com.android.server.wm.ActivityRecordProto.SHOULD_OVERRIDE_FORCE_RESIZE_APP; import static com.android.server.wm.ActivityRecordProto.SHOULD_OVERRIDE_MIN_ASPECT_RATIO; import static com.android.server.wm.ActivityRecordProto.SHOULD_REFRESH_ACTIVITY_FOR_CAMERA_COMPAT; import static com.android.server.wm.ActivityRecordProto.SHOULD_REFRESH_ACTIVITY_VIA_PAUSE_FOR_CAMERA_COMPAT; @@ -4312,7 +4314,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mTaskSupervisor.getActivityMetricsLogger().notifyActivityRemoved(this); mTaskSupervisor.mStoppingActivities.remove(this); mLetterboxUiController.destroy(); - waitingToShow = false; // Defer removal of this activity when either a child is animating, or app transition is on // going. App transition animation might be applied on the parent task not on the activity, @@ -5386,7 +5387,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A final DisplayContent displayContent = getDisplayContent(); displayContent.mOpeningApps.remove(this); displayContent.mClosingApps.remove(this); - waitingToShow = false; setVisibleRequested(visible); mLastDeferHidingClient = deferHidingClient; @@ -5411,25 +5411,16 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // stopped, then we need to set up to wait for its windows to be ready. if (!isVisible() || mAppStopped) { clearAllDrawn(); - - // If the app was already visible, don't reset the waitingToShow state. - if (!isVisible()) { - waitingToShow = true; - - // If the client isn't hidden, we don't need to reset the drawing state. - if (!isClientVisible()) { - // Let's reset the draw state in order to prevent the starting window to be - // immediately dismissed when the app still has the surface. - forAllWindows(w -> { - if (w.mWinAnimator.mDrawState == HAS_DRAWN) { - w.mWinAnimator.resetDrawState(); - - // Force add to mResizingWindows, so that we are guaranteed to get - // another reportDrawn callback. - w.forceReportingResized(); - } - }, true /* traverseTopToBottom */); - } + // Reset the draw state in order to prevent the starting window to be immediately + // dismissed when the app still has the surface. + if (!isVisible() && !isClientVisible()) { + forAllWindows(w -> { + if (w.mWinAnimator.mDrawState == HAS_DRAWN) { + w.mWinAnimator.resetDrawState(); + // Force add to mResizingWindows, so the window will report drawn. + w.forceReportingResized(); + } + }, true /* traverseTopToBottom */); } } @@ -10341,6 +10332,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mLetterboxUiController.shouldRefreshActivityViaPauseForCameraCompat()); proto.write(SHOULD_OVERRIDE_MIN_ASPECT_RATIO, mLetterboxUiController.shouldOverrideMinAspectRatio()); + proto.write(SHOULD_IGNORE_ORIENTATION_REQUEST_LOOP, + mLetterboxUiController.shouldIgnoreOrientationRequestLoop()); + proto.write(SHOULD_OVERRIDE_FORCE_RESIZE_APP, + mLetterboxUiController.shouldOverrideForceResizeApp()); } @Override diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index d90d017a5570..13f71521c240 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -993,17 +993,6 @@ class ActivityStarter { } } - if (Flags.archiving()) { - PackageArchiver packageArchiver = mService - .getPackageManagerInternalLocked() - .getPackageArchiver(); - if (packageArchiver.isIntentResolvedToArchivedApp(intent, mRequest.userId)) { - return packageArchiver - .requestUnarchiveOnActivityStart( - intent, callingPackage, mRequest.userId, realCallingUid); - } - } - final int launchFlags = intent.getFlags(); if ((launchFlags & Intent.FLAG_ACTIVITY_FORWARD_RESULT) != 0 && sourceRecord != null) { // Transfer the result target from the source activity to the new one being started, @@ -1045,6 +1034,17 @@ class ActivityStarter { } if (err == ActivityManager.START_SUCCESS && aInfo == null) { + if (Flags.archiving()) { + PackageArchiver packageArchiver = mService + .getPackageManagerInternalLocked() + .getPackageArchiver(); + if (packageArchiver.isIntentResolvedToArchivedApp(intent, mRequest.userId)) { + return packageArchiver + .requestUnarchiveOnActivityStart( + intent, callingPackage, mRequest.userId, realCallingUid); + } + } + // We couldn't find the specific class specified in the Intent. // Also the end of the line. err = ActivityManager.START_CLASS_NOT_FOUND; diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index f43c1b01e87c..3959a5e54cbf 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -3691,19 +3691,13 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { return false; } - // If the app is using legacy-entry (not auto-enter), then we will get a client-request - // that was actually a server-request (via pause(userLeaving=true)). This happens when - // the app is PAUSING, so detect that case here. - boolean originallyFromClient = fromClient - && (!r.isState(PAUSING) || params.isAutoEnterEnabled()); - - // If PiP2 flag is on and client-request to enter PiP came via onUserLeaveHint(), - // we request a direct transition from Shell to TRANSIT_PIP_LEGACY to get the startWct - // with the right entry bounds. - if (isPip2ExperimentEnabled() && !originallyFromClient && !params.isAutoEnterEnabled()) { + // If PiP2 flag is on and client-request to enter PiP comes in, + // we request a direct transition from Shell to TRANSIT_PIP to get the startWct + // with the right entry bounds. So PiP activity isn't moved to a pinned task until after + // Shell calls back into Core with the entry bounds passed through. + if (isPip2ExperimentEnabled()) { final Transition legacyEnterPipTransition = new Transition(TRANSIT_PIP, - 0 /* flags */, getTransitionController(), - mWindowManager.mSyncEngine); + 0 /* flags */, getTransitionController(), mWindowManager.mSyncEngine); legacyEnterPipTransition.setPipActivity(r); getTransitionController().startCollectOrQueue(legacyEnterPipTransition, (deferred) -> { getTransitionController().requestStartTransition(legacyEnterPipTransition, @@ -3712,6 +3706,12 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { return true; } + // If the app is using legacy-entry (not auto-enter), then we will get a client-request + // that was actually a server-request (via pause(userLeaving=true)). This happens when + // the app is PAUSING, so detect that case here. + boolean originallyFromClient = fromClient + && (!r.isState(PAUSING) || params.isAutoEnterEnabled()); + // Create a transition only for this pip entry if it is coming from the app without the // system requesting that the app enter-pip. If the system requested it, that means it // should be part of that transition if possible. diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java index 05087f8a6edf..939babc4df41 100644 --- a/services/core/java/com/android/server/wm/AppTransitionController.java +++ b/services/core/java/com/android/server/wm/AppTransitionController.java @@ -1176,7 +1176,6 @@ public class AppTransitionController { mDisplayContent.mNoAnimationNotifyOnTransitionFinished.add(app.token); } app.updateReportedVisibilityLocked(); - app.waitingToShow = false; app.showAllWindowsLocked(); if (mDisplayContent.mAppTransition.isNextAppTransitionThumbnailUp()) { diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index eed46fee1ae1..fc3a33883de6 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -61,6 +61,7 @@ import android.widget.Toast; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.FrameworkStatsLog; +import com.android.internal.util.Preconditions; import com.android.server.UiThread; import com.android.server.am.PendingIntentRecord; import com.android.window.flags.Flags; @@ -219,6 +220,9 @@ public class BackgroundActivityStartController { private final WindowProcessController mCallerApp; private final WindowProcessController mRealCallerApp; private final boolean mIsCallForResult; + private final ActivityOptions mCheckedOptions; + private BalVerdict mResultForCaller; + private BalVerdict mResultForRealCaller; private BalState(int callingUid, int callingPid, final String callingPackage, int realCallingUid, int realCallingPid, @@ -239,6 +243,7 @@ public class BackgroundActivityStartController { mIntent = intent; mRealCallingPackage = mService.getPackageNameIfUnique(realCallingUid, realCallingPid); mIsCallForResult = resultRecord != null; + mCheckedOptions = checkedOptions; if (balRequireOptInByPendingIntentCreator() // auto-opt in introduced with this feature && (originatingPendingIntent == null // not a PendingIntent || mIsCallForResult) // sent for result @@ -369,8 +374,19 @@ public class BackgroundActivityStartController { return mCallingUid == mRealCallingUid; } - private String dump(BalVerdict resultIfPiCreatorAllowsBal, - BalVerdict resultIfPiSenderAllowsBal) { + public void setResultForCaller(BalVerdict resultForCaller) { + Preconditions.checkState(mResultForCaller == null, + "mResultForCaller can only be set once"); + this.mResultForCaller = resultForCaller; + } + + public void setResultForRealCaller(BalVerdict resultForRealCaller) { + Preconditions.checkState(mResultForRealCaller == null, + "mResultForRealCaller can only be set once"); + this.mResultForRealCaller = resultForRealCaller; + } + + private String dump() { StringBuilder sb = new StringBuilder(2048); sb.append("[callingPackage: ") .append(getDebugPackageName(mCallingPackage, mCallingUid)); @@ -392,7 +408,7 @@ public class BackgroundActivityStartController { sb.append("; balAllowedByPiCreator: ").append(mBalAllowedByPiCreator); sb.append("; balAllowedByPiCreatorWithHardening: ") .append(mBalAllowedByPiCreatorWithHardening); - sb.append("; resultIfPiCreatorAllowsBal: ").append(resultIfPiCreatorAllowsBal); + sb.append("; resultIfPiCreatorAllowsBal: ").append(mResultForCaller); sb.append("; hasRealCaller: ").append(hasRealCaller()); sb.append("; isCallForResult: ").append(mIsCallForResult); sb.append("; isPendingIntent: ").append(isPendingIntent()); @@ -416,7 +432,7 @@ public class BackgroundActivityStartController { .append(mRealCallerApp.hasActivityInVisibleTask()); } sb.append("; balAllowedByPiSender: ").append(mBalAllowedByPiSender); - sb.append("; resultIfPiSenderAllowsBal: ").append(resultIfPiSenderAllowsBal); + sb.append("; resultIfPiSenderAllowsBal: ").append(mResultForRealCaller); } sb.append("]"); return sb.toString(); @@ -559,23 +575,25 @@ public class BackgroundActivityStartController { // realCallingSdkSandboxUidToAppUid should probably just be used instead (or in addition // to realCallingUid when calculating resultForRealCaller below. if (mService.hasActiveVisibleWindow(realCallingSdkSandboxUidToAppUid)) { - BalVerdict balVerdict = new BalVerdict(BAL_ALLOW_SDK_SANDBOX, /*background*/ false, - "uid in SDK sandbox has visible (non-toast) window"); - return statsLog(balVerdict, state); + state.setResultForRealCaller(new BalVerdict(BAL_ALLOW_SDK_SANDBOX, + /*background*/ false, + "uid in SDK sandbox has visible (non-toast) window")); + return allowBasedOnRealCaller(state); } } BalVerdict resultForCaller = checkBackgroundActivityStartAllowedByCaller(state); + state.setResultForCaller(resultForCaller); if (!state.hasRealCaller()) { if (resultForCaller.allows()) { if (DEBUG_ACTIVITY_STARTS) { Slog.d(TAG, "Background activity start allowed. " - + state.dump(resultForCaller, resultForCaller)); + + state.dump()); } - return statsLog(resultForCaller, state); + return allowBasedOnCaller(state); } - return abortLaunch(state, resultForCaller, resultForCaller); + return abortLaunch(state); } // The realCaller result is only calculated for PendingIntents (indicated by a valid @@ -589,6 +607,8 @@ public class BackgroundActivityStartController { ? resultForCaller : checkBackgroundActivityStartAllowedBySender(state, checkedOptions) .setBasedOnRealCaller(); + state.setResultForRealCaller(resultForRealCaller); + if (state.isPendingIntent()) { resultForCaller.setOnlyCreatorAllows( resultForCaller.allows() && resultForRealCaller.blocks()); @@ -600,18 +620,18 @@ public class BackgroundActivityStartController { == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) { if (DEBUG_ACTIVITY_STARTS) { Slog.d(TAG, "Activity start explicitly allowed by caller. " - + state.dump(resultForCaller, resultForRealCaller)); + + state.dump()); } - return statsLog(resultForCaller, state); + return allowBasedOnCaller(state); } if (resultForRealCaller.allows() && checkedOptions.getPendingIntentBackgroundActivityStartMode() == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) { if (DEBUG_ACTIVITY_STARTS) { Slog.d(TAG, "Activity start explicitly allowed by real caller. " - + state.dump(resultForCaller, resultForRealCaller)); + + state.dump()); } - return statsLog(resultForRealCaller, state); + return allowBasedOnRealCaller(state); } // Handle PendingIntent cases with default behavior next boolean callerCanAllow = resultForCaller.allows() @@ -626,26 +646,24 @@ public class BackgroundActivityStartController { // Will be allowed even with BAL hardening. if (DEBUG_ACTIVITY_STARTS) { Slog.d(TAG, "Activity start allowed by caller. " - + state.dump(resultForCaller, resultForRealCaller)); + + state.dump()); } - // return the realCaller result for backwards compatibility - return statsLog(resultForRealCaller, state); + return allowBasedOnCaller(state); } if (state.mBalAllowedByPiCreator.allowsBackgroundActivityStarts()) { Slog.wtf(TAG, "With Android 15 BAL hardening this activity start may be blocked" + " if the PI creator upgrades target_sdk to 35+" + " AND the PI sender upgrades target_sdk to 34+! " - + state.dump(resultForCaller, resultForRealCaller)); + + state.dump()); showBalRiskToast(); - // return the realCaller result for backwards compatibility - return statsLog(resultForRealCaller, state); + return allowBasedOnCaller(state); } Slog.wtf(TAG, "Without Android 15 BAL hardening this activity start would be allowed" + " (missing opt in by PI creator or sender)! " - + state.dump(resultForCaller, resultForRealCaller)); - return abortLaunch(state, resultForCaller, resultForRealCaller); + + state.dump()); + return abortLaunch(state); } if (callerCanAllow) { // Allowed before V by creator @@ -653,24 +671,24 @@ public class BackgroundActivityStartController { // Will be allowed even with BAL hardening. if (DEBUG_ACTIVITY_STARTS) { Slog.d(TAG, "Activity start allowed by caller. " - + state.dump(resultForCaller, resultForRealCaller)); + + state.dump()); } - return statsLog(resultForCaller, state); + return allowBasedOnCaller(state); } if (state.mBalAllowedByPiCreator.allowsBackgroundActivityStarts()) { Slog.wtf(TAG, "With Android 15 BAL hardening this activity start may be blocked" + " if the PI creator upgrades target_sdk to 35+! " + " (missing opt in by PI creator)! " - + state.dump(resultForCaller, resultForRealCaller)); + + state.dump()); showBalRiskToast(); - return statsLog(resultForCaller, state); + return allowBasedOnCaller(state); } Slog.wtf(TAG, "Without Android 15 BAL hardening this activity start would be allowed" + " (missing opt in by PI creator)! " - + state.dump(resultForCaller, resultForRealCaller)); - return abortLaunch(state, resultForCaller, resultForRealCaller); + + state.dump()); + return abortLaunch(state); } if (realCallerCanAllow) { // Allowed before U by sender @@ -679,23 +697,38 @@ public class BackgroundActivityStartController { "With Android 14 BAL hardening this activity start will be blocked" + " if the PI sender upgrades target_sdk to 34+! " + " (missing opt in by PI sender)! " - + state.dump(resultForCaller, resultForRealCaller)); + + state.dump()); showBalRiskToast(); - return statsLog(resultForRealCaller, state); + return allowBasedOnRealCaller(state); } Slog.wtf(TAG, "Without Android 14 BAL hardening this activity start would be allowed" + " (missing opt in by PI sender)! " - + state.dump(resultForCaller, resultForRealCaller)); - return abortLaunch(state, resultForCaller, resultForRealCaller); + + state.dump()); + return abortLaunch(state); } // neither the caller not the realCaller can allow or have explicitly opted out - return abortLaunch(state, resultForCaller, resultForRealCaller); + return abortLaunch(state); + } + + private BalVerdict allowBasedOnCaller(BalState state) { + if (DEBUG_ACTIVITY_STARTS) { + Slog.d(TAG, "Background activity launch allowed based on caller. " + + state.dump()); + } + return statsLog(state.mResultForCaller, state); } - private BalVerdict abortLaunch(BalState state, BalVerdict resultForCaller, - BalVerdict resultForRealCaller) { + private BalVerdict allowBasedOnRealCaller(BalState state) { + if (DEBUG_ACTIVITY_STARTS) { + Slog.d(TAG, "Background activity launch allowed based on real caller. " + + state.dump()); + } + return statsLog(state.mResultForRealCaller, state); + } + + private BalVerdict abortLaunch(BalState state) { Slog.w(TAG, "Background activity launch blocked! " - + state.dump(resultForCaller, resultForRealCaller)); + + state.dump()); showBalBlockedToast(); return statsLog(BalVerdict.BLOCK, state); } @@ -1471,24 +1504,36 @@ public class BackgroundActivityStartController { && (callingUid == Process.SYSTEM_UID || realCallingUid == Process.SYSTEM_UID)) { String activityName = intent != null ? requireNonNull(intent.getComponent()).flattenToShortString() : ""; - FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED, - activityName, - BAL_ALLOW_PENDING_INTENT, - callingUid, - realCallingUid); + writeBalAllowedLog(activityName, BAL_ALLOW_PENDING_INTENT, + state); } if (code == BAL_ALLOW_PERMISSION || code == BAL_ALLOW_FOREGROUND - || code == BAL_ALLOW_SAW_PERMISSION) { + || code == BAL_ALLOW_SAW_PERMISSION) { // We don't need to know which activity in this case. - FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED, - /*activityName*/ "", - code, - callingUid, - realCallingUid); + writeBalAllowedLog("", code, state); + } return finalVerdict; } + private static void writeBalAllowedLog(String activityName, int code, BalState state) { + FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED, + activityName, + code, + state.mCallingUid, + state.mRealCallingUid, + state.mResultForCaller == null ? BAL_BLOCK : state.mResultForCaller.getRawCode(), + state.mBalAllowedByPiCreator.allowsBackgroundActivityStarts(), + state.mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode() + != ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED, + state.mResultForRealCaller == null ? BAL_BLOCK + : state.mResultForRealCaller.getRawCode(), + state.mBalAllowedByPiSender.allowsBackgroundActivityStarts(), + state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode() + != ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED + ); + } + /** * Called whenever an activity finishes. Stores the record, so it can be used by ASM grace * period checks. diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index 68bd326448d4..47972b37d836 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -513,7 +513,6 @@ final class LetterboxUiController { * timer and activity is not letterboxed for fixed orientation * </ul> */ - @VisibleForTesting boolean shouldIgnoreOrientationRequestLoop() { if (!shouldEnableWithOptInOverrideAndOptOutProperty( /* gatingCondition */ mLetterboxConfiguration diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 4ad4b0c388ec..f51bd1be158c 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -16,6 +16,7 @@ package com.android.server.wm; +import static android.Manifest.permission.MANAGE_ACTIVITY_TASKS; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; @@ -57,6 +58,7 @@ import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_SWITC import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_TRANSITION; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; +import static com.android.server.wm.ActivityTaskManagerService.checkPermission; import static com.android.server.wm.ActivityTaskSupervisor.printThisActivity; import static com.android.server.wm.IdentifierProto.HASH_CODE; import static com.android.server.wm.IdentifierProto.TITLE; @@ -80,7 +82,9 @@ import android.app.servertransaction.ClientTransaction; import android.app.servertransaction.NewIntentItem; import android.app.servertransaction.PauseActivityItem; import android.app.servertransaction.ResumeActivityItem; +import android.content.PermissionChecker; import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; @@ -744,7 +748,17 @@ class TaskFragment extends WindowContainer<WindowContainer> { // The system is trusted to embed other apps securely and for all users. return UserHandle.getAppId(uid) == SYSTEM_UID // Activities from the same UID can be embedded freely by the host. - || a.isUid(uid); + || a.isUid(uid) + // Apps which have the signature MANAGE_ACTIVITY_TASK permission are trusted. + || hasManageTaskPermission(uid); + } + + /** + * Checks if a particular app uid has the {@link MANAGE_ACTIVITY_TASKS} permission. + */ + private static boolean hasManageTaskPermission(int uid) { + return checkPermission(MANAGE_ACTIVITY_TASKS, PermissionChecker.PID_UNKNOWN, uid) + == PackageManager.PERMISSION_GRANTED; } /** @@ -2984,7 +2998,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { @Override Dimmer getDimmer() { // If this is in an embedded TaskFragment and we want the dim applies on the TaskFragment. - if (mIsEmbedded && mEmbeddedDimArea == EMBEDDED_DIM_AREA_TASK_FRAGMENT) { + if (mIsEmbedded && !isDimmingOnParentTask()) { return mDimmer; } @@ -2993,7 +3007,9 @@ class TaskFragment extends WindowContainer<WindowContainer> { /** Bounds to be used for dimming, as well as touch related tests. */ void getDimBounds(@NonNull Rect out) { - if (mIsEmbedded && mEmbeddedDimArea == EMBEDDED_DIM_AREA_PARENT_TASK) { + if (mIsEmbedded && isDimmingOnParentTask() && getDimmer().getDimBounds() != null) { + // Return the task bounds if the dimmer is showing and should cover on the Task (not + // just on this embedded TaskFragment). out.set(getTask().getBounds()); } else { out.set(getBounds()); @@ -3004,6 +3020,11 @@ class TaskFragment extends WindowContainer<WindowContainer> { mEmbeddedDimArea = embeddedDimArea; } + @VisibleForTesting + boolean isDimmingOnParentTask() { + return mEmbeddedDimArea == EMBEDDED_DIM_AREA_PARENT_TASK; + } + @Override void prepareSurfaces() { if (asTask() != null) { diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index b12855e2bb49..56bef3335b8b 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -1906,7 +1906,6 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { for (int i = mParticipants.size() - 1; i >= 0; --i) { final WallpaperWindowToken wallpaper = mParticipants.valueAt(i).asWallpaperToken(); if (wallpaper != null) { - wallpaper.waitingToShow = false; if (!wallpaper.isVisible() && wallpaper.isVisibleRequested()) { wallpaper.commitVisibility(showWallpaper); } diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 9e4a31c3773a..59d0210251d1 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -34,6 +34,7 @@ import static android.window.TaskFragmentOperation.OP_TYPE_REQUEST_FOCUS_ON_TASK import static android.window.TaskFragmentOperation.OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS; import static android.window.TaskFragmentOperation.OP_TYPE_SET_COMPANION_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_DIM_ON_TASK; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ISOLATED_NAVIGATION; import static android.window.TaskFragmentOperation.OP_TYPE_SET_RELATIVE_BOUNDS; import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; @@ -68,6 +69,8 @@ import static com.android.server.wm.ActivityTaskManagerService.enforceTaskPermis import static com.android.server.wm.ActivityTaskSupervisor.REMOVE_FROM_RECENTS; import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_PINNED_TASK; import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_TASK_ORG; +import static com.android.server.wm.TaskFragment.EMBEDDED_DIM_AREA_PARENT_TASK; +import static com.android.server.wm.TaskFragment.EMBEDDED_DIM_AREA_TASK_FRAGMENT; import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED; import static com.android.server.wm.TaskFragment.FLAG_FORCE_HIDDEN_FOR_TASK_FRAGMENT_ORG; import static com.android.server.wm.WindowContainer.POSITION_BOTTOM; @@ -1493,6 +1496,12 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub task.removeDecorSurface(); break; } + case OP_TYPE_SET_DIM_ON_TASK: { + final boolean dimOnTask = operation.isDimOnTask(); + taskFragment.setEmbeddedDimArea(dimOnTask ? EMBEDDED_DIM_AREA_PARENT_TASK + : EMBEDDED_DIM_AREA_TASK_FRAGMENT); + break; + } } return effects; } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 315c00f7fb8c..0b43be700b0d 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -1929,9 +1929,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP * of a transition that has not yet been started. */ boolean isReadyForDisplay() { - if (mToken.waitingToShow && getDisplayContent().mAppTransition.isTransitionSet()) { - return false; - } final boolean parentAndClientVisible = !isParentWindowHidden() && mViewVisibility == View.VISIBLE && mToken.isVisible(); return mHasSurface && isVisibleByPolicy() && !mDestroying diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java index 7d21dbf85a66..5048cef3da1b 100644 --- a/services/core/java/com/android/server/wm/WindowToken.java +++ b/services/core/java/com/android/server/wm/WindowToken.java @@ -28,7 +28,6 @@ import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL; import static com.android.server.wm.WindowTokenProto.HASH_CODE; import static com.android.server.wm.WindowTokenProto.PAUSED; -import static com.android.server.wm.WindowTokenProto.WAITING_TO_SHOW; import static com.android.server.wm.WindowTokenProto.WINDOW_CONTAINER; import android.annotation.CallSuper; @@ -91,10 +90,6 @@ class WindowToken extends WindowContainer<WindowState> { // Is key dispatching paused for this token? boolean paused = false; - // Set to true when this token is in a pending transaction where it - // will be shown. - boolean waitingToShow; - /** The owner has {@link android.Manifest.permission#MANAGE_APP_TOKENS} */ final boolean mOwnerCanManageAppTokens; @@ -702,7 +697,6 @@ class WindowToken extends WindowContainer<WindowState> { final long token = proto.start(fieldId); super.dumpDebug(proto, WINDOW_CONTAINER, logLevel); proto.write(HASH_CODE, System.identityHashCode(this)); - proto.write(WAITING_TO_SHOW, waitingToShow); proto.write(PAUSED, paused); proto.end(token); } @@ -716,9 +710,6 @@ class WindowToken extends WindowContainer<WindowState> { super.dump(pw, prefix, dumpAll); pw.print(prefix); pw.print("windows="); pw.println(mChildren); pw.print(prefix); pw.print("windowType="); pw.print(windowType); - if (waitingToShow) { - pw.print(" waitingToShow=true"); - } pw.println(); if (hasFixedRotationTransform()) { pw.print(prefix); diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index b19f3d813985..dfa9dcecfbb5 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -79,6 +79,7 @@ cc_library_static { ":lib_cachedAppOptimizer_native", ":lib_gameManagerService_native", ":lib_oomConnection_native", + ":lib_anrTimer_native", ], include_dirs: [ @@ -246,3 +247,10 @@ filegroup { name: "lib_oomConnection_native", srcs: ["com_android_server_am_OomConnection.cpp"], } + +filegroup { + name: "lib_anrTimer_native", + srcs: [ + "com_android_server_utils_AnrTimer.cpp", + ], +} diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 9ba0a2aae02c..afb0b20650f8 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -114,6 +114,7 @@ static struct { jmethodID notifyFocusChanged; jmethodID notifySensorEvent; jmethodID notifySensorAccuracy; + jmethodID notifyStickyModifierStateChanged; jmethodID notifyStylusGestureStarted; jmethodID isInputMethodConnectionActive; jmethodID notifyVibratorState; @@ -270,7 +271,8 @@ static std::string getStringElementFromJavaArray(JNIEnv* env, jobjectArray array class NativeInputManager : public virtual InputReaderPolicyInterface, public virtual InputDispatcherPolicyInterface, public virtual PointerControllerPolicyInterface, - public virtual PointerChoreographerPolicyInterface { + public virtual PointerChoreographerPolicyInterface, + public virtual InputFilterPolicyInterface { protected: virtual ~NativeInputManager(); @@ -388,6 +390,10 @@ public: PointerControllerInterface::ControllerType type) override; void notifyPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position) override; + /* --- InputFilterPolicyInterface implementation --- */ + void notifyStickyModifierStateChanged(uint32_t modifierState, + uint32_t lockedModifierState) override; + private: sp<InputManagerInterface> mInputManager; @@ -477,7 +483,7 @@ NativeInputManager::NativeInputManager(jobject serviceObj, const sp<Looper>& loo mServiceObj = env->NewGlobalRef(serviceObj); - InputManager* im = new InputManager(this, *this, *this); + InputManager* im = new InputManager(this, *this, *this, *this); mInputManager = im; defaultServiceManager()->addService(String16("inputflinger"), im); } @@ -806,6 +812,14 @@ void NativeInputManager::notifyPointerDisplayIdChanged(int32_t pointerDisplayId, checkAndClearExceptionFromCallback(env, "onPointerDisplayIdChanged"); } +void NativeInputManager::notifyStickyModifierStateChanged(uint32_t modifierState, + uint32_t lockedModifierState) { + JNIEnv* env = jniEnv(); + env->CallVoidMethod(mServiceObj, gServiceClassInfo.notifyStickyModifierStateChanged, + modifierState, lockedModifierState); + checkAndClearExceptionFromCallback(env, "notifyStickyModifierStateChanged"); +} + sp<SurfaceControl> NativeInputManager::getParentSurfaceForPointers(int displayId) { JNIEnv* env = jniEnv(); jlong nativeSurfaceControlPtr = @@ -2957,6 +2971,9 @@ int register_android_server_InputManager(JNIEnv* env) { GET_METHOD_ID(gServiceClassInfo.onPointerDisplayIdChanged, clazz, "onPointerDisplayIdChanged", "(IFF)V"); + GET_METHOD_ID(gServiceClassInfo.notifyStickyModifierStateChanged, clazz, + "notifyStickyModifierStateChanged", "(II)V"); + GET_METHOD_ID(gServiceClassInfo.onPointerDownOutsideFocus, clazz, "onPointerDownOutsideFocus", "(Landroid/os/IBinder;)V"); diff --git a/services/core/jni/com_android_server_utils_AnrTimer.cpp b/services/core/jni/com_android_server_utils_AnrTimer.cpp new file mode 100644 index 000000000000..97b18fac91f4 --- /dev/null +++ b/services/core/jni/com_android_server_utils_AnrTimer.cpp @@ -0,0 +1,918 @@ +/* + * Copyright (C) 2023 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. + */ + +#include <time.h> +#include <pthread.h> +#include <sys/timerfd.h> +#include <inttypes.h> + +#include <algorithm> +#include <list> +#include <memory> +#include <set> +#include <string> +#include <vector> + +#define LOG_TAG "AnrTimerService" + +#include <jni.h> +#include <nativehelper/JNIHelp.h> +#include "android_runtime/AndroidRuntime.h" +#include "core_jni_helpers.h" + +#include <utils/Mutex.h> +#include <utils/Timers.h> + +#include <utils/Log.h> +#include <utils/Timers.h> +#include <android-base/logging.h> +#include <android-base/stringprintf.h> +#include <android-base/unique_fd.h> + +using ::android::base::StringPrintf; + + +// Native support is unavailable on WIN32 platforms. This macro preemptively disables it. +#ifdef _WIN32 +#define NATIVE_SUPPORT 0 +#else +#define NATIVE_SUPPORT 1 +#endif + +namespace android { + +// using namespace android; + +// Almost nothing in this module needs to be in the android namespace. +namespace { + +// If not on a Posix system, create stub timerfd methods. These are defined to allow +// compilation. They are not functional. Also, they do not leak outside this compilation unit. +#ifdef _WIN32 +int timer_create() { + return -1; +} +int timer_settime(int, int, void const *, void *) { + return -1; +} +#else +int timer_create() { + return timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC); +} +int timer_settime(int fd, int flags, const struct itimerspec *new_value, + struct itimerspec *_Nullable old_value) { + return timerfd_settime(fd, flags, new_value, old_value); +} +#endif + +// A local debug flag that gates a set of log messages for debug only. This is normally const +// false so the debug statements are not included in the image. The flag can be set true in a +// unit test image to debug test failures. +const bool DEBUG = false; + +// Return the current time in nanoseconds. This time is relative to system boot. +nsecs_t now() { + return systemTime(SYSTEM_TIME_MONOTONIC); +} + +/** + * This class encapsulates the anr timer service. The service manages a list of individual + * timers. A timer is either Running or Expired. Once started, a timer may be canceled or + * accepted. Both actions collect statistics about the timer and then delete it. An expired + * timer may also be discarded, which deletes the timer without collecting any statistics. + * + * All public methods in this class are thread-safe. + */ +class AnrTimerService { + private: + class ProcessStats; + class Timer; + + public: + + // The class that actually runs the clock. + class Ticker; + + // A timer is identified by a timer_id_t. Timer IDs are unique in the moment. + using timer_id_t = uint32_t; + + // A manifest constant. No timer is ever created with this ID. + 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); + + enum Status { + Invalid, + Running, + Expired, + Canceled + }; + + /** + * Create a timer service. The service is initialized with a name used for logging. The + * constructor is also given the notifier callback, and two cookies for the callback: the + * traditional void* and an int. + */ + AnrTimerService(char const* label, notifier_t notifier, void* cookie, jweak jtimer, Ticker*); + + // Delete the service and clean up memory. + ~AnrTimerService(); + + // Start a timer and return the associated timer ID. It does not matter if the same pid/uid + // are already in the running list. Once start() is called, one of cancel(), accept(), or + // discard() must be called to clean up the internal data structures. + timer_id_t start(int pid, int uid, nsecs_t timeout, bool extend); + + // Cancel a timer and remove it from all lists. This is called when the event being timed + // has occurred. If the timer was Running, the function returns true. The other + // possibilities are that the timer was Expired or non-existent; in both cases, the function + // returns false. + bool cancel(timer_id_t timerId); + + // Accept a timer and remove it from all lists. This is called when the upper layers accept + // that a timer has expired. If the timer was Expired, the function returns true. The + // other possibilities are tha the timer was Running or non-existing; in both cases, the + // function returns false. + bool accept(timer_id_t timerId); + + // Discard a timer without collecting any statistics. This is called when the upper layers + // recognize that a timer expired but decide the expiration is not significant. If the + // timer was Expired, the function returns true. The other possibilities are tha the timer + // was Running or non-existing; in both cases, the function returns false. + bool discard(timer_id_t timerId); + + // A timer has expired. + void expire(timer_id_t); + + // Dump a small amount of state to the log file. + void dump(bool verbose) const; + + // Return the Java object associated with this instance. + jweak jtimer() const { + return notifierObject_; + } + + private: + // The service cannot be copied. + AnrTimerService(AnrTimerService const &) = delete; + + // Insert a timer into the running list. The lock must be held by the caller. + void insert(const Timer&); + + // Remove a timer from the lists and return it. The lock must be held by the caller. + Timer remove(timer_id_t timerId); + + // Return a string representation of a status value. + static char const *statusString(Status); + + // The name of this service, for logging. + std::string const label_; + + // The callback that is invoked when a timer expires. + notifier_t const notifier_; + + // The two cookies passed to the notifier. + void* notifierCookie_; + jweak notifierObject_; + + // The global lock + mutable Mutex lock_; + + // The list of all timers that are still running. This is sorted by ID for fast lookup. + std::set<Timer> running_; + + // The maximum number of active timers. + size_t maxActive_; + + // Simple counters + struct Counters { + // The number of timers started, canceled, accepted, discarded, and expired. + size_t started; + size_t canceled; + size_t accepted; + size_t discarded; + size_t expired; + + // The number of times there were zero active timers. + size_t drained; + + // The number of times a protocol error was seen. + size_t error; + }; + + Counters counters_; + + // The clock used by this AnrTimerService. + Ticker *ticker_; +}; + +class AnrTimerService::ProcessStats { + public: + nsecs_t cpu_time; + nsecs_t cpu_delay; + + ProcessStats() : + cpu_time(0), + cpu_delay(0) { + } + + // Collect all statistics for a process. Return true if the fill succeeded and false if it + // did not. If there is any problem, the statistics are zeroed. + bool fill(int pid) { + cpu_time = 0; + cpu_delay = 0; + + char path[PATH_MAX]; + snprintf(path, sizeof(path), "/proc/%u/schedstat", pid); + ::android::base::unique_fd fd(open(path, O_RDONLY | O_CLOEXEC)); + if (!fd.ok()) { + return false; + } + char buffer[128]; + ssize_t len = read(fd, buffer, sizeof(buffer)); + if (len <= 0) { + return false; + } + if (len >= sizeof(buffer)) { + ALOGE("proc file too big: %s", path); + return false; + } + buffer[len] = 0; + unsigned long t1; + unsigned long t2; + if (sscanf(buffer, "%lu %lu", &t1, &t2) != 2) { + return false; + } + cpu_time = t1; + cpu_delay = t2; + return true; + } +}; + +class AnrTimerService::Timer { + public: + // A unique ID assigned when the Timer is created. + timer_id_t const id; + + // The creation parameters. The timeout is the original, relative timeout. + int const pid; + int const uid; + nsecs_t const timeout; + bool const extend; + + // The state of this timer. + Status status; + + // The scheduled timeout. This is an absolute time. It may be extended. + nsecs_t scheduled; + + // True if this timer has been extended. + bool extended; + + // Bookkeeping for extensions. The initial state of the process. This is collected only if + // the timer is extensible. + ProcessStats initial; + + // The default constructor is used to create timers that are Invalid, representing the "not + // found" condition when a collection is searched. + Timer() : + id(NOTIMER), + pid(0), + uid(0), + timeout(0), + extend(false), + status(Invalid), + scheduled(0), + extended(false) { + } + + // This constructor creates a timer with the specified id. This can be used as the argument + // to find(). + Timer(timer_id_t id) : + id(id), + pid(0), + uid(0), + timeout(0), + extend(false), + status(Invalid), + scheduled(0), + extended(false) { + } + + // Create a new timer. This starts the timer. + Timer(int pid, int uid, nsecs_t timeout, bool extend) : + id(nextId()), + pid(pid), + uid(uid), + timeout(timeout), + extend(extend), + status(Running), + scheduled(now() + timeout), + extended(false) { + if (extend && pid != 0) { + initial.fill(pid); + } + } + + // Cancel a timer. Return the headroom (which may be negative). This does not, as yet, + // account for extensions. + void cancel() { + ALOGW_IF(DEBUG && status != Running, "cancel %s", toString().c_str()); + status = Canceled; + } + + // Expire a timer. Return true if the timer is expired and false otherwise. The function + // returns false if the timer is eligible for extension. If the function returns false, the + // scheduled time is updated. + bool expire() { + ALOGI_IF(DEBUG, "expire %s", toString().c_str()); + nsecs_t extension = 0; + if (extend && !extended) { + // Only one extension is permitted. + extended = true; + ProcessStats current; + current.fill(pid); + extension = current.cpu_delay - initial.cpu_delay; + if (extension < 0) extension = 0; + if (extension > timeout) extension = timeout; + } + if (extension == 0) { + status = Expired; + } else { + scheduled += extension; + } + return status == Expired; + } + + // Accept a timeout. + void accept() { + } + + // Discard a timeout. + void discard() { + } + + // Timers are sorted by id, which is unique. This provides fast lookups. + bool operator<(Timer const &r) const { + return id < r.id; + } + + bool operator==(timer_id_t r) const { + return id == r; + } + + std::string toString() const { + return StringPrintf("timer id=%d pid=%d status=%s", id, pid, statusString(status)); + } + + std::string toString(nsecs_t now) const { + uint32_t ms = nanoseconds_to_milliseconds(now - scheduled); + return StringPrintf("timer id=%d pid=%d status=%s scheduled=%ums", + id, pid, statusString(status), -ms); + } + + static int maxId() { + return idGen; + } + + private: + // Get the next free ID. NOTIMER is never returned. + static timer_id_t nextId() { + timer_id_t id = idGen.fetch_add(1); + while (id == NOTIMER) { + id = idGen.fetch_add(1); + } + return id; + } + + // IDs start at 1. A zero ID is invalid. + static std::atomic<timer_id_t> idGen; +}; + +// IDs start at 1. +std::atomic<AnrTimerService::timer_id_t> AnrTimerService::Timer::idGen(1); + +/** + * Manage a set of timers and notify clients when there is a timeout. + */ +class AnrTimerService::Ticker { + private: + struct Entry { + const nsecs_t scheduled; + const timer_id_t id; + AnrTimerService* const service; + + Entry(nsecs_t scheduled, timer_id_t id, AnrTimerService* service) : + scheduled(scheduled), id(id), service(service) {}; + + bool operator<(const Entry &r) const { + return scheduled == r.scheduled ? id < r.id : scheduled < r.scheduled; + } + }; + + public: + + // Construct the ticker. This creates the timerfd file descriptor and starts the monitor + // thread. The monitor thread is given a unique name. + Ticker() { + timerFd_ = timer_create(); + if (timerFd_ < 0) { + ALOGE("failed to create timerFd: %s", strerror(errno)); + return; + } + + if (pthread_create(&watcher_, 0, run, this) != 0) { + ALOGE("failed to start thread: %s", strerror(errno)); + watcher_ = 0; + ::close(timerFd_); + return; + } + + // 16 is a magic number from the kernel. Thread names may not be longer than this many + // bytes, including the terminating null. The snprintf() method will truncate properly. + char name[16]; + snprintf(name, sizeof(name), "AnrTimerService"); + pthread_setname_np(watcher_, name); + + ready_ = true; + } + + ~Ticker() { + // Closing the file descriptor will close the monitor process, if any. + if (timerFd_ >= 0) ::close(timerFd_); + timerFd_ = -1; + watcher_ = 0; + } + + // Insert a timer. Unless canceled, the timer will expire at the scheduled time. If it + // expires, the service will be notified with the id. + void insert(nsecs_t scheduled, timer_id_t id, AnrTimerService *service) { + Entry e(scheduled, id, service); + AutoMutex _l(lock_); + timer_id_t front = headTimerId(); + running_.insert(e); + if (front != headTimerId()) restartLocked(); + maxRunning_ = std::max(maxRunning_, running_.size()); + } + + // Remove a timer. The timer is identified by its scheduled timeout and id. Technically, + // the id is sufficient (because timer IDs are unique) but using the timeout is more + // efficient. + void remove(nsecs_t scheduled, timer_id_t id) { + Entry key(scheduled, id, 0); + AutoMutex _l(lock_); + timer_id_t front = headTimerId(); + auto found = running_.find(key); + if (found != running_.end()) running_.erase(found); + if (front != headTimerId()) restartLocked(); + } + + // Remove every timer associated with the service. + void remove(AnrTimerService const* service) { + AutoMutex _l(lock_); + timer_id_t front = headTimerId(); + for (auto i = running_.begin(); i != running_.end(); i++) { + if (i->service == service) { + running_.erase(i); + } + } + if (front != headTimerId()) restartLocked(); + } + + // Return the number of timers still running. + size_t running() const { + AutoMutex _l(lock_); + return running_.size(); + } + + // Return the high-water mark of timers running. + size_t maxRunning() const { + AutoMutex _l(lock_); + return maxRunning_; + } + + private: + + // Return the head of the running list. The lock must be held by the caller. + timer_id_t headTimerId() { + return running_.empty() ? NOTIMER : running_.cbegin()->id; + } + + // A simple wrapper that meets the requirements of pthread_create. + static void* run(void* arg) { + reinterpret_cast<Ticker*>(arg)->monitor(); + ALOGI("monitor exited"); + return 0; + } + + // Loop (almost) forever. Whenever the timerfd expires, expire as many entries as + // possible. The loop terminates when the read fails; this generally indicates that the + // file descriptor has been closed and the thread can exit. + void monitor() { + uint64_t token = 0; + while (read(timerFd_, &token, sizeof(token)) == sizeof(token)) { + // Move expired timers into the local ready list. This is done inside + // the lock. Then, outside the lock, expire them. + nsecs_t current = now(); + std::vector<Entry> ready; + { + AutoMutex _l(lock_); + while (!running_.empty()) { + Entry timer = *(running_.begin()); + if (timer.scheduled <= current) { + ready.push_back(timer); + running_.erase(running_.cbegin()); + } else { + break; + } + } + restartLocked(); + } + // Call the notifiers outside the lock. Calling the notifiers with the lock held + // can lead to deadlock, if the Java-side handler also takes a lock. Note that the + // timerfd is already running. + for (auto i = ready.begin(); i != ready.end(); i++) { + Entry e = *i; + e.service->expire(e.id); + } + } + } + + // Restart the ticker. The caller must be holding the lock. This method updates the + // timerFd_ to expire at the time of the first Entry in the running list. This method does + // not check to see if the currently programmed expiration time is different from the + // scheduled expiration time of the first entry. + void restartLocked() { + if (!running_.empty()) { + Entry const x = *(running_.cbegin()); + nsecs_t delay = x.scheduled - now(); + // Force a minimum timeout of 10ns. + if (delay < 10) delay = 10; + time_t sec = nanoseconds_to_seconds(delay); + time_t ns = delay - seconds_to_nanoseconds(sec); + struct itimerspec setting = { + .it_interval = { 0, 0 }, + .it_value = { sec, ns }, + }; + timer_settime(timerFd_, 0, &setting, nullptr); + restarted_++; + ALOGI_IF(DEBUG, "restarted timerfd for %ld.%09ld", sec, ns); + } else { + const struct itimerspec setting = { + .it_interval = { 0, 0 }, + .it_value = { 0, 0 }, + }; + timer_settime(timerFd_, 0, &setting, nullptr); + drained_++; + ALOGI_IF(DEBUG, "drained timer list"); + } + } + + // The usual lock. + mutable Mutex lock_; + + // True if the object was initialized properly. Android does not support throwing C++ + // exceptions, so clients should check this flag after constructing the object. This is + // effectively const after the instance has been created. + bool ready_ = false; + + // The file descriptor of the timer. + int timerFd_ = -1; + + // The thread that monitors the timer. + pthread_t watcher_ = 0; + + // The number of times the timer was restarted. + size_t restarted_ = 0; + + // The number of times the timer list was exhausted. + size_t drained_ = 0; + + // The highwater mark of timers that are running. + size_t maxRunning_ = 0; + + // The list of timers that are scheduled. This set is sorted by timeout and then by timer + // ID. A set is sufficient (as opposed to a multiset) because timer IDs are unique. + std::set<Entry> running_; +}; + + +AnrTimerService::AnrTimerService(char const* label, + notifier_t notifier, void* cookie, jweak jtimer, Ticker* ticker) : + label_(label), + notifier_(notifier), + notifierCookie_(cookie), + notifierObject_(jtimer), + ticker_(ticker) { + + // Zero the statistics + maxActive_ = 0; + memset(&counters_, 0, sizeof(counters_)); + + ALOGI_IF(DEBUG, "initialized %s", label); +} + +AnrTimerService::~AnrTimerService() { + AutoMutex _l(lock_); + ticker_->remove(this); +} + +char const *AnrTimerService::statusString(Status s) { + switch (s) { + case Invalid: return "invalid"; + case Running: return "running"; + case Expired: return "expired"; + case Canceled: return "canceled"; + } + return "unknown"; +} + +AnrTimerService::timer_id_t AnrTimerService::start(int pid, int uid, + nsecs_t timeout, bool extend) { + ALOGI_IF(DEBUG, "starting"); + AutoMutex _l(lock_); + Timer t(pid, uid, timeout, extend); + insert(t); + counters_.started++; + + ALOGI_IF(DEBUG, "started timer %u timeout=%zu", t.id, static_cast<size_t>(timeout)); + return t.id; +} + +bool AnrTimerService::cancel(timer_id_t timerId) { + ALOGI_IF(DEBUG, "canceling %u", timerId); + if (timerId == NOTIMER) return false; + AutoMutex _l(lock_); + Timer timer = remove(timerId); + + bool result = timer.status == Running; + if (timer.status != Invalid) { + timer.cancel(); + } else { + counters_.error++; + } + counters_.canceled++; + ALOGI_IF(DEBUG, "canceled timer %u", timerId); + return result; +} + +bool AnrTimerService::accept(timer_id_t timerId) { + ALOGI_IF(DEBUG, "accepting %u", timerId); + if (timerId == NOTIMER) return false; + AutoMutex _l(lock_); + Timer timer = remove(timerId); + + bool result = timer.status == Expired; + if (timer.status == Expired) { + timer.accept(); + } else { + counters_.error++; + } + counters_.accepted++; + ALOGI_IF(DEBUG, "accepted timer %u", timerId); + return result; +} + +bool AnrTimerService::discard(timer_id_t timerId) { + ALOGI_IF(DEBUG, "discarding %u", timerId); + if (timerId == NOTIMER) return false; + AutoMutex _l(lock_); + Timer timer = remove(timerId); + + bool result = timer.status == Expired; + if (timer.status == Expired) { + timer.discard(); + } else { + counters_.error++; + } + counters_.discarded++; + ALOGI_IF(DEBUG, "discarded timer %u", timerId); + return result; +} + +// Hold the lock in order to manage the running list. +// the listener. +void AnrTimerService::expire(timer_id_t timerId) { + ALOGI_IF(DEBUG, "expiring %u", timerId); + // Save the timer attributes for the notification + int pid = 0; + int uid = 0; + bool expired = false; + { + AutoMutex _l(lock_); + Timer t = remove(timerId); + expired = t.expire(); + if (t.status == Invalid) { + ALOGW_IF(DEBUG, "error: expired invalid timer %u", timerId); + return; + } else { + // The timer is either Running (because it was extended) or expired (and is awaiting an + // accept or discard). + insert(t); + } + } + + // Deliver the notification outside of the lock. + if (expired) { + if (!notifier_(timerId, pid, uid, notifierCookie_, notifierObject_)) { + AutoMutex _l(lock_); + // Notification failed, which means the listener will never call accept() or + // discard(). Do not reinsert the timer. + remove(timerId); + } + } + ALOGI_IF(DEBUG, "expired timer %u", timerId); +} + +void AnrTimerService::insert(const Timer& t) { + running_.insert(t); + if (t.status == Running) { + // Only forward running timers to the ticker. Expired timers are handled separately. + ticker_->insert(t.scheduled, t.id, this); + maxActive_ = std::max(maxActive_, running_.size()); + } +} + +AnrTimerService::Timer AnrTimerService::remove(timer_id_t timerId) { + Timer key(timerId); + auto found = running_.find(key); + if (found != running_.end()) { + Timer result = *found; + running_.erase(found); + ticker_->remove(result.scheduled, result.id); + return result; + } + return Timer(); +} + +void AnrTimerService::dump(bool verbose) const { + AutoMutex _l(lock_); + ALOGI("timer %s ops started=%zu canceled=%zu accepted=%zu discarded=%zu expired=%zu", + label_.c_str(), + counters_.started, counters_.canceled, counters_.accepted, + counters_.discarded, counters_.expired); + ALOGI("timer %s stats max-active=%zu/%zu running=%zu/%zu errors=%zu", + label_.c_str(), + maxActive_, ticker_->maxRunning(), running_.size(), ticker_->running(), + counters_.error); + + if (verbose) { + nsecs_t time = now(); + for (auto i = running_.begin(); i != running_.end(); i++) { + Timer t = *i; + ALOGI(" running %s", t.toString(time).c_str()); + } + } +} + +/** + * True if the native methods are supported in this process. Native methods are supported only + * if the initialization succeeds. + */ +bool nativeSupportEnabled = false; + +/** + * Singleton/globals for the anr timer. Among other things, this includes a Ticker* and a use + * count. The JNI layer creates a single Ticker for all operational AnrTimers. The Ticker is + * created when the first AnrTimer is created, and is deleted when the last AnrTimer is closed. + */ +static Mutex gAnrLock; +struct AnrArgs { + jclass clazz = NULL; + jmethodID func = NULL; + JavaVM* vm = NULL; + AnrTimerService::Ticker* ticker = nullptr; + int tickerUseCount = 0;; +}; +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, + void* cookie, jweak jtimer) { + AutoMutex _l(gAnrLock); + AnrArgs* target = reinterpret_cast<AnrArgs* >(cookie); + JNIEnv *env; + if (target->vm->AttachCurrentThread(&env, 0) != JNI_OK) { + ALOGE("failed to attach thread to JavaVM"); + return false; + } + jboolean r = false; + jobject timer = env->NewGlobalRef(jtimer); + if (timer != nullptr) { + r = env->CallBooleanMethod(timer, target->func, timerId, pid, uid); + env->DeleteGlobalRef(timer); + } + target->vm->DetachCurrentThread(); + return r; +} + +jboolean anrTimerSupported(JNIEnv* env, jclass) { + return nativeSupportEnabled; +} + +jlong anrTimerCreate(JNIEnv* env, jobject jtimer, jstring jname) { + if (!nativeSupportEnabled) return 0; + AutoMutex _l(gAnrLock); + if (!gAnrArgs.ticker) { + gAnrArgs.ticker = new AnrTimerService::Ticker(); + } + gAnrArgs.tickerUseCount++; + + ScopedUtfChars name(env, jname); + jobject timer = env->NewWeakGlobalRef(jtimer); + AnrTimerService* service = + new AnrTimerService(name.c_str(), anrNotify, &gAnrArgs, timer, gAnrArgs.ticker); + return reinterpret_cast<jlong>(service); +} + +AnrTimerService *toService(jlong pointer) { + return reinterpret_cast<AnrTimerService*>(pointer); +} + +jint anrTimerClose(JNIEnv* env, jclass, jlong ptr) { + if (!nativeSupportEnabled) return -1; + if (ptr == 0) return -1; + AutoMutex _l(gAnrLock); + AnrTimerService *s = toService(ptr); + env->DeleteWeakGlobalRef(s->jtimer()); + delete s; + if (--gAnrArgs.tickerUseCount <= 0) { + delete gAnrArgs.ticker; + gAnrArgs.ticker = nullptr; + } + return 0; +} + +jint anrTimerStart(JNIEnv* env, jclass, jlong ptr, + jint pid, jint uid, jlong timeout, jboolean extend) { + if (!nativeSupportEnabled) return 0; + // On the Java side, timeouts are expressed in milliseconds and must be converted to + // nanoseconds before being passed to the library code. + return toService(ptr)->start(pid, uid, milliseconds_to_nanoseconds(timeout), extend); +} + +jboolean anrTimerCancel(JNIEnv* env, jclass, jlong ptr, jint timerId) { + if (!nativeSupportEnabled) return false; + return toService(ptr)->cancel(timerId); +} + +jboolean anrTimerAccept(JNIEnv* env, jclass, jlong ptr, jint timerId) { + if (!nativeSupportEnabled) return false; + return toService(ptr)->accept(timerId); +} + +jboolean anrTimerDiscard(JNIEnv* env, jclass, jlong ptr, jint timerId) { + if (!nativeSupportEnabled) return false; + return toService(ptr)->discard(timerId); +} + +jint anrTimerDump(JNIEnv *env, jclass, jlong ptr, jboolean verbose) { + if (!nativeSupportEnabled) return -1; + toService(ptr)->dump(verbose); + return 0; +} + +static const JNINativeMethod methods[] = { + {"nativeAnrTimerSupported", "()Z", (void*) anrTimerSupported}, + {"nativeAnrTimerCreate", "(Ljava/lang/String;)J", (void*) anrTimerCreate}, + {"nativeAnrTimerClose", "(J)I", (void*) anrTimerClose}, + {"nativeAnrTimerStart", "(JIIJZ)I", (void*) anrTimerStart}, + {"nativeAnrTimerCancel", "(JI)Z", (void*) anrTimerCancel}, + {"nativeAnrTimerAccept", "(JI)Z", (void*) anrTimerAccept}, + {"nativeAnrTimerDiscard", "(JI)Z", (void*) anrTimerDiscard}, + {"nativeAnrTimerDump", "(JZ)V", (void*) anrTimerDump}, +}; + +} // anonymous namespace + +int register_android_server_utils_AnrTimer(JNIEnv* env) +{ + static const char *className = "com/android/server/utils/AnrTimer"; + jniRegisterNativeMethods(env, className, methods, NELEM(methods)); + + jclass service = FindClassOrDie(env, className); + gAnrArgs.clazz = MakeGlobalRefOrDie(env, service); + gAnrArgs.func = env->GetMethodID(gAnrArgs.clazz, "expire", "(III)Z"); + env->GetJavaVM(&gAnrArgs.vm); + + nativeSupportEnabled = NATIVE_SUPPORT; + + return 0; +} + +} // namespace android diff --git a/services/core/jni/gnss/Android.bp b/services/core/jni/gnss/Android.bp index f3ba484f62a6..e72259f094bc 100644 --- a/services/core/jni/gnss/Android.bp +++ b/services/core/jni/gnss/Android.bp @@ -68,5 +68,6 @@ cc_defaults { "android.hardware.gnss@2.1", "android.hardware.gnss.measurement_corrections@1.0", "android.hardware.gnss.visibility_control@1.0", + "android_location_flags_c_lib", ], } diff --git a/services/core/jni/gnss/Utils.cpp b/services/core/jni/gnss/Utils.cpp index 571534f5d16e..274e3b740ff6 100644 --- a/services/core/jni/gnss/Utils.cpp +++ b/services/core/jni/gnss/Utils.cpp @@ -20,6 +20,7 @@ #include <android/hardware/gnss/1.0/IGnss.h> #include <android/hardware/gnss/2.0/IGnss.h> +#include <android_location_flags.h> #include <utils/SystemClock.h> /* * Save a pointer to JavaVm to attach/detach threads executing @@ -27,6 +28,8 @@ */ JavaVM* android::ScopedJniThreadAttach::sJvm; +namespace location_flags = android::location::flags; + namespace android { namespace { @@ -194,7 +197,12 @@ jobject translateGnssLocation(JNIEnv* env, const android::hardware::gnss::GnssLo flags = static_cast<uint32_t>(location.elapsedRealtime.flags); if (flags & android::hardware::gnss::ElapsedRealtime::HAS_TIMESTAMP_NS) { - SET(ElapsedRealtimeNanos, location.elapsedRealtime.timestampNs); + if (location_flags::replace_future_elapsed_realtime_jni() && + location.elapsedRealtime.timestampNs > android::elapsedRealtimeNano()) { + SET(ElapsedRealtimeNanos, android::elapsedRealtimeNano()); + } else { + SET(ElapsedRealtimeNanos, location.elapsedRealtime.timestampNs); + } } else { SET(ElapsedRealtimeNanos, android::elapsedRealtimeNano()); } diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp index 11734da5b1ac..f3158d11b9a4 100644 --- a/services/core/jni/onload.cpp +++ b/services/core/jni/onload.cpp @@ -52,6 +52,7 @@ int register_android_server_Watchdog(JNIEnv* env); int register_android_server_HardwarePropertiesManagerService(JNIEnv* env); int register_android_server_SyntheticPasswordManager(JNIEnv* env); int register_android_hardware_display_DisplayViewport(JNIEnv* env); +int register_android_server_utils_AnrTimer(JNIEnv *env); int register_android_server_am_OomConnection(JNIEnv* env); int register_android_server_am_CachedAppOptimizer(JNIEnv* env); int register_android_server_am_LowMemDetector(JNIEnv* env); @@ -113,6 +114,7 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) register_android_server_storage_AppFuse(env); register_android_server_SyntheticPasswordManager(env); register_android_hardware_display_DisplayViewport(env); + register_android_server_utils_AnrTimer(env); register_android_server_am_OomConnection(env); register_android_server_am_CachedAppOptimizer(env); register_android_server_am_LowMemDetector(env); diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java index 6899ad48b813..31409ab1de4b 100644 --- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java @@ -109,7 +109,7 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(), Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS), /*defaultProviderId=*/flattenedPrimaryProviders), - providerDataList); + providerDataList, /*isRequestForAllOptions=*/ false); mClientCallback.onPendingIntent(mPendingIntent); } catch (RemoteException e) { mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false); diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java index 3c190bf7ad11..f092dccbcfd1 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java @@ -150,9 +150,12 @@ public class CredentialManagerUi { * * @param requestInfo the information about the request * @param providerDataList the list of provider data from remote providers + * @param isRequestForAllOptions whether the bottom sheet should directly navigate to the + * all options page */ public PendingIntent createPendingIntent( - RequestInfo requestInfo, ArrayList<ProviderData> providerDataList) { + RequestInfo requestInfo, ArrayList<ProviderData> providerDataList, + boolean isRequestForAllOptions) { List<CredentialProviderInfo> allProviders = CredentialProviderInfoFactory.getCredentialProviderServices( mContext, @@ -168,7 +171,8 @@ public class CredentialManagerUi { disabledProvider.getComponentName().flattenToString())).toList(); Intent intent = IntentFactory.createCredentialSelectorIntent(requestInfo, providerDataList, - new ArrayList<>(disabledProviderDataList), mResultReceiver) + new ArrayList<>(disabledProviderDataList), mResultReceiver, + isRequestForAllOptions) .setAction(UUID.randomUUID().toString()); //TODO: Create unique pending intent using request code and cancel any pre-existing pending // intents diff --git a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java index ca5600e2049a..d1651713fe03 100644 --- a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java @@ -106,7 +106,8 @@ public class GetCandidateRequestSession extends RequestSession<GetCredentialRequ mRequestId, mClientRequest, mClientAppInfo.getPackageName(), PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(), Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS)), - providerDataList); + providerDataList, + /*isRequestForAllOptions=*/ true); List<GetCredentialProviderData> candidateProviderDataList = new ArrayList<>(); for (ProviderData providerData : providerDataList) { diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java index c9e691e199c7..3f57c804cba0 100644 --- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java @@ -99,21 +99,24 @@ public class GetRequestSession extends RequestSession<GetCredentialRequest, protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) { mRequestSessionMetric.collectUiCallStartTime(System.nanoTime()); mCredentialManagerUi.setStatus(CredentialManagerUi.UiStatus.USER_INTERACTION); - Binder.withCleanCallingIdentity(()-> { - try { + Binder.withCleanCallingIdentity(() -> { + try { cancelExistingPendingIntent(); - mPendingIntent = mCredentialManagerUi.createPendingIntent( - RequestInfo.newGetRequestInfo( - mRequestId, mClientRequest, mClientAppInfo.getPackageName(), - PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(), - Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS)), - providerDataList); - mClientCallback.onPendingIntent(mPendingIntent); - } catch (RemoteException e) { - mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false); - mCredentialManagerUi.setStatus(CredentialManagerUi.UiStatus.TERMINATED); - String exception = GetCredentialException.TYPE_UNKNOWN; - mRequestSessionMetric.collectFrameworkException(exception); + mPendingIntent = mCredentialManagerUi.createPendingIntent( + RequestInfo.newGetRequestInfo( + mRequestId, mClientRequest, mClientAppInfo.getPackageName(), + PermissionUtils.hasPermission(mContext, + mClientAppInfo.getPackageName(), + Manifest.permission + .CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS)), + providerDataList, + /*isRequestForAllOptions=*/ false); + mClientCallback.onPendingIntent(mPendingIntent); + } catch (RemoteException e) { + mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false); + mCredentialManagerUi.setStatus(CredentialManagerUi.UiStatus.TERMINATED); + String exception = GetCredentialException.TYPE_UNKNOWN; + mRequestSessionMetric.collectFrameworkException(exception); respondToClientWithErrorAndFinish(exception, "Unable to instantiate selector"); } }); diff --git a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java index f447c1fd277e..fbfc9caf0205 100644 --- a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java @@ -192,7 +192,7 @@ public class PrepareGetRequestSession extends GetRequestSession { mRequestId, mClientRequest, mClientAppInfo.getPackageName(), PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(), Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS)), - providerDataList); + providerDataList, /*isRequestForAllOptions=*/ false); } else { return null; } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java index e0232b1e1fc5..14dc0ebb8ad8 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java @@ -32,6 +32,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.AppGlobals; import android.app.BroadcastOptions; +import android.app.admin.BooleanPolicyValue; import android.app.admin.DevicePolicyIdentifiers; import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyState; @@ -142,6 +143,67 @@ final class DevicePolicyEngine { mAdminPolicySize = new SparseArray<>(); } + private void maybeForceEnforcementRefreshLocked(@NonNull PolicyDefinition<?> policyDefinition) { + try { + if (shouldForceEnforcementRefresh(policyDefinition)) { + // This is okay because it's only true for user restrictions which are all <Boolean> + forceEnforcementRefreshLocked((PolicyDefinition<Boolean>) policyDefinition); + } + } catch (Throwable e) { + // Catch any possible exceptions just to be on the safe side + Log.e(TAG, "Exception throw during maybeForceEnforcementRefreshLocked", e); + } + } + + private boolean shouldForceEnforcementRefresh(@NonNull PolicyDefinition<?> policyDefinition) { + // These are all "not nullable" but for the purposes of maximum safety for a lightly tested + // change we check here + if (policyDefinition == null) { + return false; + } + PolicyKey policyKey = policyDefinition.getPolicyKey(); + if (policyKey == null) { + return false; + } + + if (policyKey instanceof UserRestrictionPolicyKey) { + // b/307481299 We must force all user restrictions to re-sync local + // + global on each set/clear + return true; + } + + return false; + } + + private void forceEnforcementRefreshLocked(PolicyDefinition<Boolean> policyDefinition) { + Binder.withCleanCallingIdentity(() -> { + // Sync global state + PolicyValue<Boolean> globalValue = new BooleanPolicyValue(false); + try { + PolicyState<Boolean> policyState = getGlobalPolicyStateLocked(policyDefinition); + globalValue = policyState.getCurrentResolvedPolicy(); + } catch (IllegalArgumentException e) { + // Expected for local-only policies + } + + enforcePolicy(policyDefinition, globalValue, UserHandle.USER_ALL); + + // Loop through each user and sync that user's state + for (UserInfo user : mUserManager.getUsers()) { + PolicyValue<Boolean> localValue = new BooleanPolicyValue(false); + try { + PolicyState<Boolean> localPolicyState = getLocalPolicyStateLocked( + policyDefinition, user.id); + localValue = localPolicyState.getCurrentResolvedPolicy(); + } catch (IllegalArgumentException e) { + // Expected for global-only policies + } + + enforcePolicy(policyDefinition, localValue, user.id); + } + }); + } + /** * Set the policy for the provided {@code policyDefinition} (see {@link PolicyDefinition}) and * {@code enforcingAdmin} to the provided {@code value}. @@ -188,6 +250,7 @@ final class DevicePolicyEngine { // No need to notify admins as no new policy is actually enforced, we're just filling in // the data structures. if (!skipEnforcePolicy) { + maybeForceEnforcementRefreshLocked(policyDefinition); if (policyChanged) { onLocalPolicyChangedLocked(policyDefinition, enforcingAdmin, userId); } @@ -278,6 +341,7 @@ final class DevicePolicyEngine { Objects.requireNonNull(enforcingAdmin); synchronized (mLock) { + maybeForceEnforcementRefreshLocked(policyDefinition); if (!hasLocalPolicyLocked(policyDefinition, userId)) { return; } @@ -451,6 +515,7 @@ final class DevicePolicyEngine { // No need to notify admins as no new policy is actually enforced, we're just filling in // the data structures. if (!skipEnforcePolicy) { + maybeForceEnforcementRefreshLocked(policyDefinition); if (policyChanged) { onGlobalPolicyChangedLocked(policyDefinition, enforcingAdmin); } @@ -506,6 +571,7 @@ final class DevicePolicyEngine { boolean policyChanged = policyState.removePolicy(enforcingAdmin); + maybeForceEnforcementRefreshLocked(policyDefinition); if (policyChanged) { onGlobalPolicyChangedLocked(policyDefinition, enforcingAdmin); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index a490013303e9..f288103bd954 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -6243,9 +6243,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { final long id = mInjector.binderClearCallingIdentity(); try { - final KeyChainConnection keyChainConnection = - KeyChain.bindAsUser(mContext, caller.getUserHandle()); - try { + try (KeyChainConnection keyChainConnection = + KeyChain.bindAsUser(mContext, caller.getUserHandle())) { IKeyChainService keyChain = keyChainConnection.getService(); if (!keyChain.installKeyPair(privKey, cert, chain, alias, KeyStore.UID_SELF)) { logInstallKeyPairFailure(caller, isCredentialManagementApp); @@ -6263,10 +6262,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { ? CREDENTIAL_MANAGEMENT_APP : NOT_CREDENTIAL_MANAGEMENT_APP) .write(); return true; - } catch (RemoteException e) { + } catch (RemoteException | AssertionError e) { Slogf.e(LOG_TAG, "Installing certificate", e); - } finally { - keyChainConnection.close(); } } catch (InterruptedException e) { Slogf.w(LOG_TAG, "Interrupted while installing certificate", e); @@ -6313,9 +6310,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { final long id = Binder.clearCallingIdentity(); try { - final KeyChainConnection keyChainConnection = - KeyChain.bindAsUser(mContext, caller.getUserHandle()); - try { + try (KeyChainConnection keyChainConnection = + KeyChain.bindAsUser(mContext, caller.getUserHandle())) { IKeyChainService keyChain = keyChainConnection.getService(); DevicePolicyEventLogger .createEvent(DevicePolicyEnums.REMOVE_KEY_PAIR) @@ -6325,10 +6321,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { ? CREDENTIAL_MANAGEMENT_APP : NOT_CREDENTIAL_MANAGEMENT_APP) .write(); return keyChain.removeKeyPair(alias); - } catch (RemoteException e) { + } catch (RemoteException | AssertionError e) { Slogf.e(LOG_TAG, "Removing keypair", e); - } finally { - keyChainConnection.close(); } } catch (InterruptedException e) { Slogf.w(LOG_TAG, "Interrupted while removing keypair", e); @@ -6355,7 +6349,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { try (KeyChainConnection keyChainConnection = KeyChain.bindAsUser(mContext, caller.getUserHandle())) { return keyChainConnection.getService().containsKeyPair(alias); - } catch (RemoteException e) { + } catch (RemoteException | AssertionError e) { Slogf.e(LOG_TAG, "Querying keypair", e); } catch (InterruptedException e) { Slogf.w(LOG_TAG, "Interrupted while querying keypair", e); @@ -6417,7 +6411,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } return false; - } catch (RemoteException e) { + } catch (RemoteException | AssertionError e) { Slogf.e(LOG_TAG, "Querying grant to wifi auth.", e); return false; } @@ -6497,7 +6491,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } result.put(uid, new ArraySet<String>(packages)); } - } catch (RemoteException e) { + } catch (RemoteException | AssertionError e) { Slogf.e(LOG_TAG, "Querying keypair grants", e); } catch (InterruptedException e) { Slogf.w(LOG_TAG, "Interrupted while querying keypair grants", e); @@ -6667,7 +6661,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { .write(); return true; } - } catch (RemoteException e) { + } catch (RemoteException | AssertionError e) { Slogf.e(LOG_TAG, "KeyChain error while generating a keypair", e); } catch (InterruptedException e) { Slogf.w(LOG_TAG, "Interrupted while generating keypair", e); @@ -6742,7 +6736,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } catch (InterruptedException e) { Slogf.w(LOG_TAG, "Interrupted while setting keypair certificate", e); Thread.currentThread().interrupt(); - } catch (RemoteException e) { + } catch (RemoteException | AssertionError e) { Slogf.e(LOG_TAG, "Failed setting keypair certificate", e); } finally { mInjector.binderRestoreCallingIdentity(id); @@ -7227,7 +7221,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { connection.getService().getCredentialManagementAppPolicy(); return policy != null && !policy.getAppAndUriMappings().isEmpty() && containsAlias(policy, alias); - } catch (RemoteException | InterruptedException e) { + } catch (RemoteException | InterruptedException | AssertionError e) { return false; } }); diff --git a/services/print/Android.bp b/services/print/Android.bp index 5b4349a92692..0dfceaa3a9d9 100644 --- a/services/print/Android.bp +++ b/services/print/Android.bp @@ -19,4 +19,7 @@ java_library_static { defaults: ["platform_service_defaults"], srcs: [":services.print-sources"], libs: ["services.core"], + lint: { + baseline_filename: "lint-baseline.xml", + }, } diff --git a/services/robotests/Android.bp b/services/robotests/Android.bp index 52eae21f9e66..a70802ad3337 100644 --- a/services/robotests/Android.bp +++ b/services/robotests/Android.bp @@ -57,9 +57,13 @@ android_robolectric_test { ], static_libs: [ "androidx.test.ext.truth", + "Settings-robo-testutils", + "SettingsLib-robo-testutils", ], instrumentation_for: "FrameworksServicesLib", + + upstream: true, } filegroup { diff --git a/services/robotests/backup/Android.bp b/services/robotests/backup/Android.bp index 8b9efb312efe..569786b1e26f 100644 --- a/services/robotests/backup/Android.bp +++ b/services/robotests/backup/Android.bp @@ -57,6 +57,8 @@ android_robolectric_test { // Include the testing libraries libs: [ "mockito-robolectric-prebuilt", + "Settings-robo-testutils", + "SettingsLib-robo-testutils", "platform-test-annotations", "testng", "truth", @@ -64,4 +66,6 @@ android_robolectric_test { instrumentation_for: "BackupFrameworksServicesLib", + upstream: true, + } diff --git a/services/robotests/backup/config/robolectric.properties b/services/robotests/backup/config/robolectric.properties index 850557a9b693..1ebf6d423fe2 100644 --- a/services/robotests/backup/config/robolectric.properties +++ b/services/robotests/backup/config/robolectric.properties @@ -1 +1,3 @@ -sdk=NEWEST_SDK
\ No newline at end of file +sdk=NEWEST_SDK +looperMode=LEGACY +shadows=com.android.server.testing.shadows.FrameworkShadowLooper diff --git a/services/robotests/backup/src/com/android/server/backup/fullbackup/AppMetadataBackupWriterTest.java b/services/robotests/backup/src/com/android/server/backup/fullbackup/AppMetadataBackupWriterTest.java index ee5a534fe005..6839a06a0d9a 100644 --- a/services/robotests/backup/src/com/android/server/backup/fullbackup/AppMetadataBackupWriterTest.java +++ b/services/robotests/backup/src/com/android/server/backup/fullbackup/AppMetadataBackupWriterTest.java @@ -57,6 +57,7 @@ import java.nio.file.attribute.FileTime; ShadowBackupDataOutput.class, ShadowEnvironment.class, ShadowFullBackup.class, + ShadowSigningInfo.class, }) public class AppMetadataBackupWriterTest { private static final String TEST_PACKAGE = "com.test.package"; diff --git a/services/robotests/backup/src/com/android/server/backup/fullbackup/ShadowSigningInfo.java b/services/robotests/backup/src/com/android/server/backup/fullbackup/ShadowSigningInfo.java new file mode 100644 index 000000000000..53d807c2d3c9 --- /dev/null +++ b/services/robotests/backup/src/com/android/server/backup/fullbackup/ShadowSigningInfo.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.backup.fullbackup; + +import static android.os.Build.VERSION_CODES.P; + +import android.content.pm.SigningInfo; + +import org.robolectric.annotation.Implements; + +@Implements(value = SigningInfo.class, minSdk = P) +public class ShadowSigningInfo { +} diff --git a/services/robotests/src/com/android/server/location/gnss/NtpNetworkTimeHelperTest.java b/services/robotests/src/com/android/server/location/gnss/NtpNetworkTimeHelperTest.java index 4949091646dd..009276359209 100644 --- a/services/robotests/src/com/android/server/location/gnss/NtpNetworkTimeHelperTest.java +++ b/services/robotests/src/com/android/server/location/gnss/NtpNetworkTimeHelperTest.java @@ -35,6 +35,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.LooperMode; import org.robolectric.shadows.ShadowLooper; import java.util.concurrent.CountDownLatch; @@ -45,6 +46,7 @@ import java.util.concurrent.TimeUnit; */ @RunWith(RobolectricTestRunner.class) @Presubmit +@LooperMode(LooperMode.Mode.LEGACY) public class NtpNetworkTimeHelperTest { private static final long MOCK_NTP_TIME = 1519930775453L; diff --git a/services/robotests/src/com/android/server/testing/shadows/FrameworkShadowLooper.java b/services/robotests/src/com/android/server/testing/shadows/FrameworkShadowLooper.java index 16d16cda42ed..3681bd4c6588 100644 --- a/services/robotests/src/com/android/server/testing/shadows/FrameworkShadowLooper.java +++ b/services/robotests/src/com/android/server/testing/shadows/FrameworkShadowLooper.java @@ -21,12 +21,15 @@ import android.os.Looper; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.RealObject; +import org.robolectric.shadows.LooperShadowPicker; +import org.robolectric.shadows.ShadowLegacyLooper; import org.robolectric.shadows.ShadowLooper; +import org.robolectric.shadows.ShadowPausedLooper; import java.util.Optional; -@Implements(value = Looper.class) -public class FrameworkShadowLooper extends ShadowLooper { +@Implements(value = Looper.class, shadowPicker = FrameworkShadowLooper.Picker.class) +public class FrameworkShadowLooper extends ShadowLegacyLooper { @RealObject private Looper mLooper; private Optional<Boolean> mIsCurrentThread = Optional.empty(); @@ -45,4 +48,10 @@ public class FrameworkShadowLooper extends ShadowLooper { } return Thread.currentThread() == mLooper.getThread(); } + + public static class Picker extends LooperShadowPicker<ShadowLooper> { + public Picker() { + super(FrameworkShadowLooper.class, ShadowPausedLooper.class); + } + } } diff --git a/services/robotests/src/com/android/server/testing/shadows/ShadowApplicationPackageManager.java b/services/robotests/src/com/android/server/testing/shadows/ShadowApplicationPackageManager.java index 4a9948668bc4..1da67597643f 100644 --- a/services/robotests/src/com/android/server/testing/shadows/ShadowApplicationPackageManager.java +++ b/services/robotests/src/com/android/server/testing/shadows/ShadowApplicationPackageManager.java @@ -95,7 +95,6 @@ public class ShadowApplicationPackageManager sPackageAppEnabledStates.put(packageName, Integer.valueOf(newState)); // flags unused here. } - @Override protected PackageInfo getPackageInfoAsUser(String packageName, int flags, int userId) throws NameNotFoundException { if (!sPackageInfos.containsKey(packageName)) { diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java index 7a84406f1b08..e370f5501865 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java @@ -759,6 +759,15 @@ public final class DisplayDeviceConfigTest { AUTO_BRIGHTNESS_MODE_DEFAULT, Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_DIM), SMALL_DELTA); + assertArrayEquals(new float[]{0.0f, 80}, + mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux( + AUTO_BRIGHTNESS_MODE_DEFAULT, + Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_BRIGHT), ZERO_DELTA); + assertArrayEquals(new float[]{0.6f, 0.7f}, + mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels( + AUTO_BRIGHTNESS_MODE_DEFAULT, + Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_BRIGHT), SMALL_DELTA); + assertArrayEquals(new float[]{0.0f, 95}, mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux( AUTO_BRIGHTNESS_MODE_DOZE, @@ -1197,6 +1206,20 @@ public final class DisplayDeviceConfigTest { + "</map>\n" + "</luxToBrightnessMapping>\n" + "<luxToBrightnessMapping>\n" + + "<mode>default</mode>\n" + + "<setting>bright</setting>\n" + + "<map>\n" + + "<point>\n" + + "<first>0</first>\n" + + "<second>0.6</second>\n" + + "</point>\n" + + "<point>\n" + + "<first>80</first>\n" + + "<second>0.7</second>\n" + + "</point>\n" + + "</map>\n" + + "</luxToBrightnessMapping>\n" + + "<luxToBrightnessMapping>\n" + "<mode>doze</mode>\n" + "<map>\n" + "<point>\n" diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java index 60a0c039dd03..a0e5fd8e1b34 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java @@ -3434,6 +3434,11 @@ public class DisplayModeDirectorTest { return mSensorManagerInternal; } + @Override + public VotesStatsReporter getVotesStatsReporter(boolean refreshRateVotingTelemetryEnabled) { + return null; + } + protected Display createDisplay(int id) { return new Display(DisplayManagerGlobal.getInstance(), id, mDisplayInfo, ApplicationProvider.getApplicationContext().getResources()); diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/SkinThermalStatusObserverTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/SkinThermalStatusObserverTest.java index f6774017c523..2815fcb764b6 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/SkinThermalStatusObserverTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/SkinThermalStatusObserverTest.java @@ -58,7 +58,7 @@ public class SkinThermalStatusObserverTest { private RegisteringFakesInjector mInjector = new RegisteringFakesInjector(); private final TestHandler mHandler = new TestHandler(null); - private final VotesStorage mStorage = new VotesStorage(() -> {}); + private final VotesStorage mStorage = new VotesStorage(() -> {}, null); @Before public void setUp() { diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/VotesStorageTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/VotesStorageTest.java index 50e239218fa0..1f6f1a41bea7 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/VotesStorageTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/VotesStorageTest.java @@ -51,7 +51,7 @@ public class VotesStorageTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); - mVotesStorage = new VotesStorage(mVotesListener); + mVotesStorage = new VotesStorage(mVotesListener, null); } @Test diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp index 113511ef8197..321d945e9c15 100644 --- a/services/tests/mockingservicestests/Android.bp +++ b/services/tests/mockingservicestests/Android.bp @@ -73,6 +73,7 @@ android_test { // TODO: remove once Android migrates to JUnit 4.12, which provides assertThrows "testng", "compatibility-device-util-axt", + "flag-junit", ], libs: [ diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java index 57326b2e1e82..d08cdc718a82 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java @@ -343,10 +343,12 @@ public class ApexManagerTest { List<ApexManager.ScanResult> scanResults = scanApexInfos(apexInfo); mApexManager.notifyScanResult(scanResults); - assertThat(mApexManager.getApkInApexInstallError(activeApex.apexModuleName)).isNull(); + final String apexPackageName = mApexManager.getActivePackageNameForApexModuleName( + activeApex.apexModuleName); + assertThat(mApexManager.getApkInApexInstallError(apexPackageName)).isNull(); mApexManager.reportErrorWithApkInApex(activeApex.apexDirectory.getAbsolutePath(), "Some random error"); - assertThat(mApexManager.getApkInApexInstallError(activeApex.apexModuleName)) + assertThat(mApexManager.getApkInApexInstallError(apexPackageName)) .isEqualTo("Some random error"); } @@ -370,9 +372,11 @@ public class ApexManagerTest { List<ApexManager.ScanResult> scanResults = scanApexInfos(apexInfo); mApexManager.notifyScanResult(scanResults); - assertThat(mApexManager.getApksInApex(activeApex.apexModuleName)).isEmpty(); + final String apexPackageName = mApexManager.getActivePackageNameForApexModuleName( + activeApex.apexModuleName); + assertThat(mApexManager.getApksInApex(apexPackageName)).isEmpty(); mApexManager.registerApkInApex(fakeApkInApex); - assertThat(mApexManager.getApksInApex(activeApex.apexModuleName)).isEmpty(); + assertThat(mApexManager.getApksInApex(apexPackageName)).isEmpty(); } @Test diff --git a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java index 9851bc1b4be3..c42c735e3c9d 100644 --- a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java @@ -16,24 +16,23 @@ package com.android.server.trust; -import static android.content.pm.PackageManager.PERMISSION_GRANTED; - import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean; import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; import static com.android.dx.mockito.inline.extended.ExtendedMockito.argThat; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.ArgumentMatchers.anyBoolean; - import android.Manifest; import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.admin.DevicePolicyManager; import android.app.trust.ITrustListener; import android.app.trust.ITrustManager; import android.content.BroadcastReceiver; @@ -45,13 +44,15 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; +import android.content.pm.UserInfo; import android.net.Uri; import android.os.Handler; +import android.os.HandlerThread; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; -import android.os.test.TestLooper; +import android.os.UserManager; import android.provider.Settings; import android.service.trust.TrustAgentService; import android.testing.TestableContext; @@ -61,12 +62,11 @@ import android.view.WindowManagerGlobal; import androidx.test.core.app.ApplicationProvider; import com.android.internal.widget.LockPatternUtils; +import com.android.modules.utils.testing.ExtendedMockitoRule; import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.SystemServiceManager; -import com.google.android.collect.Lists; - import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -74,37 +74,60 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatcher; import org.mockito.Mock; -import org.mockito.MockitoSession; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; import java.util.ArrayList; -import java.util.Collections; -import java.util.Random; +import java.util.Collection; +import java.util.List; public class TrustManagerServiceTest { @Rule - public MockitoRule mMockitoRule = MockitoJUnit.rule(); + public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this) + .mockStatic(ServiceManager.class) + .mockStatic(WindowManagerGlobal.class) + .build(); + @Rule public final MockContext mMockContext = new MockContext( ApplicationProvider.getApplicationContext()); private static final String URI_SCHEME_PACKAGE = "package"; - private static final int TEST_USER_ID = UserHandle.USER_SYSTEM; + private static final int TEST_USER_ID = 50; - private final TestLooper mLooper = new TestLooper(); private final ArrayList<ResolveInfo> mTrustAgentResolveInfoList = new ArrayList<>(); - private final LockPatternUtils mLockPatternUtils = new LockPatternUtils(mMockContext); - private final TrustManagerService mService = new TrustManagerService(mMockContext); + private final ArrayList<ComponentName> mKnownTrustAgents = new ArrayList<>(); + private final ArrayList<ComponentName> mEnabledTrustAgents = new ArrayList<>(); + + private @Mock ActivityManager mActivityManager; + private @Mock DevicePolicyManager mDevicePolicyManager; + private @Mock LockPatternUtils mLockPatternUtils; + private @Mock PackageManager mPackageManager; + private @Mock UserManager mUserManager; + private @Mock IWindowManager mWindowManager; - @Mock - private PackageManager mPackageManagerMock; + private HandlerThread mHandlerThread; + private TrustManagerService.Injector mInjector; + private TrustManagerService mService; + private ITrustManager mTrustManager; @Before - public void setUp() { - resetTrustAgentLockSettings(); - LocalServices.addService(SystemServiceManager.class, mock(SystemServiceManager.class)); + public void setUp() throws Exception { + when(mActivityManager.isUserRunning(TEST_USER_ID)).thenReturn(true); + + when(mLockPatternUtils.getDevicePolicyManager()).thenReturn(mDevicePolicyManager); + when(mLockPatternUtils.isSecure(TEST_USER_ID)).thenReturn(true); + when(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).thenReturn(mKnownTrustAgents); + when(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).thenReturn(mEnabledTrustAgents); + doAnswer(invocation -> { + mKnownTrustAgents.clear(); + mKnownTrustAgents.addAll((Collection<ComponentName>) invocation.getArgument(0)); + return null; + }).when(mLockPatternUtils).setKnownTrustAgents(any(), eq(TEST_USER_ID)); + doAnswer(invocation -> { + mEnabledTrustAgents.clear(); + mEnabledTrustAgents.addAll((Collection<ComponentName>) invocation.getArgument(0)); + return null; + }).when(mLockPatternUtils).setEnabledTrustAgents(any(), eq(TEST_USER_ID)); ArgumentMatcher<Intent> trustAgentIntentMatcher = new ArgumentMatcher<Intent>() { @Override @@ -112,17 +135,42 @@ public class TrustManagerServiceTest { return TrustAgentService.SERVICE_INTERFACE.equals(argument.getAction()); } }; - when(mPackageManagerMock.queryIntentServicesAsUser(argThat(trustAgentIntentMatcher), + when(mPackageManager.queryIntentServicesAsUser(argThat(trustAgentIntentMatcher), anyInt(), anyInt())).thenReturn(mTrustAgentResolveInfoList); - when(mPackageManagerMock.checkPermission(any(), any())).thenReturn( + when(mPackageManager.checkPermission(any(), any())).thenReturn( PackageManager.PERMISSION_GRANTED); - mMockContext.setMockPackageManager(mPackageManagerMock); + + when(mUserManager.getAliveUsers()).thenReturn( + List.of(new UserInfo(TEST_USER_ID, "user", UserInfo.FLAG_FULL))); + + when(mWindowManager.isKeyguardLocked()).thenReturn(true); + + mMockContext.addMockSystemService(ActivityManager.class, mActivityManager); + mMockContext.setMockPackageManager(mPackageManager); + mMockContext.addMockSystemService(UserManager.class, mUserManager); + doReturn(mWindowManager).when(() -> WindowManagerGlobal.getWindowManagerService()); + LocalServices.addService(SystemServiceManager.class, mock(SystemServiceManager.class)); + + grantPermission(Manifest.permission.ACCESS_KEYGUARD_SECURE_STORAGE); + grantPermission(Manifest.permission.TRUST_LISTENER); + + mHandlerThread = new HandlerThread("handler"); + mHandlerThread.start(); + mInjector = new TrustManagerService.Injector(mLockPatternUtils, mHandlerThread.getLooper()); + mService = new TrustManagerService(mMockContext, mInjector); + + // Get the ITrustManager from the new TrustManagerService. + mService.onStart(); + ArgumentCaptor<IBinder> binderArgumentCaptor = ArgumentCaptor.forClass(IBinder.class); + verify(() -> ServiceManager.addService(eq(Context.TRUST_SERVICE), + binderArgumentCaptor.capture(), anyBoolean(), anyInt())); + mTrustManager = ITrustManager.Stub.asInterface(binderArgumentCaptor.getValue()); } @After public void tearDown() { - resetTrustAgentLockSettings(); LocalServices.removeServiceForTest(SystemServiceManager.class); + mHandlerThread.quit(); } @Test @@ -142,10 +190,9 @@ public class TrustManagerServiceTest { bootService(); - assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).containsExactly( - systemTrustAgent1, systemTrustAgent2); - assertThat(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).containsExactly( - systemTrustAgent1, systemTrustAgent2, userTrustAgent1, userTrustAgent2); + assertThat(mEnabledTrustAgents).containsExactly(systemTrustAgent1, systemTrustAgent2); + assertThat(mKnownTrustAgents).containsExactly(systemTrustAgent1, systemTrustAgent2, + userTrustAgent1, userTrustAgent2); } @Test @@ -162,10 +209,8 @@ public class TrustManagerServiceTest { bootService(); - assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).containsExactly( - defaultTrustAgent); - assertThat(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).containsExactly( - systemTrustAgent, defaultTrustAgent); + assertThat(mEnabledTrustAgents).containsExactly(defaultTrustAgent); + assertThat(mKnownTrustAgents).containsExactly(systemTrustAgent, defaultTrustAgent); } @Test @@ -174,16 +219,16 @@ public class TrustManagerServiceTest { "com.android/.SystemTrustAgent"); ComponentName trustAgent2 = ComponentName.unflattenFromString( "com.android/.AnotherSystemTrustAgent"); - initializeEnabledAgents(trustAgent1); + mEnabledTrustAgents.add(trustAgent1); + Settings.Secure.putIntForUser(mMockContext.getContentResolver(), + Settings.Secure.TRUST_AGENTS_INITIALIZED, 1, TEST_USER_ID); addTrustAgent(trustAgent1, /* isSystemApp= */ true); addTrustAgent(trustAgent2, /* isSystemApp= */ true); bootService(); - assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).containsExactly( - trustAgent1); - assertThat(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).containsExactly( - trustAgent1, trustAgent2); + assertThat(mEnabledTrustAgents).containsExactly(trustAgent1); + assertThat(mKnownTrustAgents).containsExactly(trustAgent1, trustAgent2); } @Test @@ -192,17 +237,17 @@ public class TrustManagerServiceTest { "com.android/.SystemTrustAgent"); ComponentName trustAgent2 = ComponentName.unflattenFromString( "com.android/.AnotherSystemTrustAgent"); - initializeEnabledAgents(trustAgent1); - initializeKnownAgents(trustAgent1); + Settings.Secure.putIntForUser(mMockContext.getContentResolver(), + Settings.Secure.TRUST_AGENTS_INITIALIZED, 1, TEST_USER_ID); + Settings.Secure.putIntForUser(mMockContext.getContentResolver(), + Settings.Secure.KNOWN_TRUST_AGENTS_INITIALIZED, 1, TEST_USER_ID); addTrustAgent(trustAgent1, /* isSystemApp= */ true); addTrustAgent(trustAgent2, /* isSystemApp= */ true); bootService(); - assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).containsExactly( - trustAgent1, trustAgent2); - assertThat(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).containsExactly( - trustAgent1, trustAgent2); + assertThat(mEnabledTrustAgents).containsExactly(trustAgent1, trustAgent2); + assertThat(mKnownTrustAgents).containsExactly(trustAgent1, trustAgent2); } @Test @@ -214,10 +259,8 @@ public class TrustManagerServiceTest { mMockContext.sendPackageChangedBroadcast(newAgentComponentName); - assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).containsExactly( - newAgentComponentName); - assertThat(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).containsExactly( - newAgentComponentName); + assertThat(mEnabledTrustAgents).containsExactly(newAgentComponentName); + assertThat(mKnownTrustAgents).containsExactly(newAgentComponentName); } @Test @@ -235,10 +278,8 @@ public class TrustManagerServiceTest { mMockContext.sendPackageChangedBroadcast(newAgentComponentName); - assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).containsExactly( - defaultTrustAgent); - assertThat(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).containsExactly( - defaultTrustAgent, newAgentComponentName); + assertThat(mEnabledTrustAgents).containsExactly(defaultTrustAgent); + assertThat(mKnownTrustAgents).containsExactly(defaultTrustAgent, newAgentComponentName); } @Test @@ -250,9 +291,8 @@ public class TrustManagerServiceTest { mMockContext.sendPackageChangedBroadcast(newAgentComponentName); - assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).isEmpty(); - assertThat(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).containsExactly( - newAgentComponentName); + assertThat(mEnabledTrustAgents).isEmpty(); + assertThat(mKnownTrustAgents).containsExactly(newAgentComponentName); } @Test @@ -265,50 +305,21 @@ public class TrustManagerServiceTest { addTrustAgent(systemTrustAgent2, /* isSystemApp= */ true); bootService(); // Simulate user turning off systemTrustAgent2 - mLockPatternUtils.setEnabledTrustAgents(Collections.singletonList(systemTrustAgent1), - TEST_USER_ID); + mLockPatternUtils.setEnabledTrustAgents(List.of(systemTrustAgent1), TEST_USER_ID); mMockContext.sendPackageChangedBroadcast(systemTrustAgent2); - assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).containsExactly( - systemTrustAgent1); + assertThat(mEnabledTrustAgents).containsExactly(systemTrustAgent1); } @Test public void reportEnabledTrustAgentsChangedInformsListener() throws RemoteException { - final LockPatternUtils utils = mock(LockPatternUtils.class); - final TrustManagerService service = new TrustManagerService(mMockContext, - new TrustManagerService.Injector(utils, mLooper.getLooper())); final ITrustListener trustListener = mock(ITrustListener.class); - final IWindowManager windowManager = mock(IWindowManager.class); - final int userId = new Random().nextInt(); - - mMockContext.getTestablePermissions().setPermission(Manifest.permission.TRUST_LISTENER, - PERMISSION_GRANTED); - - when(utils.getKnownTrustAgents(anyInt())).thenReturn(new ArrayList<>()); - - MockitoSession mockSession = mockitoSession() - .initMocks(this) - .mockStatic(ServiceManager.class) - .mockStatic(WindowManagerGlobal.class) - .startMocking(); - - doReturn(windowManager).when(() -> { - WindowManagerGlobal.getWindowManagerService(); - }); - - service.onStart(); - ArgumentCaptor<IBinder> binderArgumentCaptor = ArgumentCaptor.forClass(IBinder.class); - verify(() -> ServiceManager.addService(eq(Context.TRUST_SERVICE), - binderArgumentCaptor.capture(), anyBoolean(), anyInt())); - ITrustManager manager = ITrustManager.Stub.asInterface(binderArgumentCaptor.getValue()); - manager.registerTrustListener(trustListener); - mLooper.dispatchAll(); - manager.reportEnabledTrustAgentsChanged(userId); - mLooper.dispatchAll(); - verify(trustListener).onEnabledTrustAgentsChanged(eq(userId)); - mockSession.finishMocking(); + mTrustManager.registerTrustListener(trustListener); + mService.waitForIdle(); + mTrustManager.reportEnabledTrustAgentsChanged(TEST_USER_ID); + mService.waitForIdle(); + verify(trustListener).onEnabledTrustAgentsChanged(TEST_USER_ID); } private void addTrustAgent(ComponentName agentComponentName, boolean isSystemApp) { @@ -327,27 +338,16 @@ public class TrustManagerServiceTest { mTrustAgentResolveInfoList.add(resolveInfo); } - private void initializeEnabledAgents(ComponentName... enabledAgents) { - mLockPatternUtils.setEnabledTrustAgents(Lists.newArrayList(enabledAgents), TEST_USER_ID); - Settings.Secure.putIntForUser(mMockContext.getContentResolver(), - Settings.Secure.TRUST_AGENTS_INITIALIZED, 1, TEST_USER_ID); - } - - private void initializeKnownAgents(ComponentName... knownAgents) { - mLockPatternUtils.setKnownTrustAgents(Lists.newArrayList(knownAgents), TEST_USER_ID); - Settings.Secure.putIntForUser(mMockContext.getContentResolver(), - Settings.Secure.KNOWN_TRUST_AGENTS_INITIALIZED, 1, TEST_USER_ID); - } - private void bootService() { mService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY); mService.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START); mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); + mMockContext.sendUserStartedBroadcast(); } - private void resetTrustAgentLockSettings() { - mLockPatternUtils.setEnabledTrustAgents(Collections.emptyList(), TEST_USER_ID); - mLockPatternUtils.setKnownTrustAgents(Collections.emptyList(), TEST_USER_ID); + private void grantPermission(String permission) { + mMockContext.getTestablePermissions().setPermission( + permission, PackageManager.PERMISSION_GRANTED); } /** A mock Context that allows the test process to send protected broadcasts. */ @@ -355,6 +355,8 @@ public class TrustManagerServiceTest { private final ArrayList<BroadcastReceiver> mPackageChangedBroadcastReceivers = new ArrayList<>(); + private final ArrayList<BroadcastReceiver> mUserStartedBroadcastReceivers = + new ArrayList<>(); MockContext(Context base) { super(base); @@ -369,6 +371,9 @@ public class TrustManagerServiceTest { if (filter.hasAction(Intent.ACTION_PACKAGE_CHANGED)) { mPackageChangedBroadcastReceivers.add(receiver); } + if (filter.hasAction(Intent.ACTION_USER_STARTED)) { + mUserStartedBroadcastReceivers.add(receiver); + } return super.registerReceiverAsUser(receiver, user, filter, broadcastPermission, scheduler); } @@ -386,5 +391,13 @@ public class TrustManagerServiceTest { receiver.onReceive(this, intent); } } + + void sendUserStartedBroadcast() { + Intent intent = new Intent(Intent.ACTION_USER_STARTED) + .putExtra(Intent.EXTRA_USER_HANDLE, TEST_USER_ID); + for (BroadcastReceiver receiver : mUserStartedBroadcastReceivers) { + receiver.onReceive(this, intent); + } + } } } diff --git a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java index 91d8ceb28ec9..a9ff3a133f6e 100644 --- a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java +++ b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import android.content.Context; @@ -113,7 +114,8 @@ public class NotifierTest { mTestExecutor.simulateAsyncExecutionOfLastCommand(); // THEN the device vibrates once - verify(mVibrator, times(1)).vibrate(any(), any(VibrationAttributes.class)); + verify(mVibrator, times(1)).vibrate(anyInt(), any(), any(), any(), + any(VibrationAttributes.class)); } @Test @@ -129,7 +131,7 @@ public class NotifierTest { mTestExecutor.simulateAsyncExecutionOfLastCommand(); // THEN the device doesn't vibrate - verify(mVibrator, never()).vibrate(any(), any(VibrationAttributes.class)); + verifyZeroInteractions(mVibrator); } @Test @@ -145,14 +147,15 @@ public class NotifierTest { mTestExecutor.simulateAsyncExecutionOfLastCommand(); // THEN the device vibrates once - verify(mVibrator, times(1)).vibrate(any(), any(VibrationAttributes.class)); + verify(mVibrator, times(1)).vibrate(anyInt(), any(), any(), any(), + any(VibrationAttributes.class)); } @Test public void testVibrateDisabled_wirelessCharging() { createNotifier(); - // GIVEN the charging vibration is disabeld + // GIVEN the charging vibration is disabled enableChargingVibration(false); // WHEN wireless charging starts @@ -161,7 +164,7 @@ public class NotifierTest { mTestExecutor.simulateAsyncExecutionOfLastCommand(); // THEN the device doesn't vibrate - verify(mVibrator, never()).vibrate(any(), any(VibrationAttributes.class)); + verifyZeroInteractions(mVibrator); } @Test diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java index d752ae4487cc..aec896f383c4 100644 --- a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java +++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java @@ -97,6 +97,7 @@ import android.view.DisplayInfo; import androidx.test.core.app.ApplicationProvider; import com.android.internal.app.IBatteryStats; +import com.android.internal.foldables.FoldGracePeriodProvider; import com.android.internal.util.test.FakeSettingsProvider; import com.android.server.LocalServices; import com.android.server.SystemService; @@ -164,6 +165,7 @@ public class PowerManagerServiceTest { @Mock private AttentionManagerInternal mAttentionManagerInternalMock; @Mock private DreamManagerInternal mDreamManagerInternalMock; @Mock private PowerManagerService.NativeWrapper mNativeWrapperMock; + @Mock private FoldGracePeriodProvider mFoldGracePeriodProvider; @Mock private Notifier mNotifierMock; @Mock private WirelessChargerDetector mWirelessChargerDetectorMock; @Mock private AmbientDisplayConfiguration mAmbientDisplayConfigurationMock; @@ -359,6 +361,11 @@ public class PowerManagerServiceTest { DeviceConfigParameterProvider createDeviceConfigParameterProvider() { return mDeviceParameterProvider; } + + @Override + FoldGracePeriodProvider createFoldGracePeriodProvider() { + return mFoldGracePeriodProvider; + } }); return mService; } @@ -520,6 +527,8 @@ public class PowerManagerServiceTest { @Test public void testWakefulnessSleep_SoftSleepFlag_NoWakelocks() { + when(mFoldGracePeriodProvider.isEnabled()).thenReturn(false); + createService(); // Start with AWAKE state startSystem(); @@ -533,6 +542,23 @@ public class PowerManagerServiceTest { } @Test + public void testWakefulnessAwakeShowKeyguard_SoftSleepFlag_NoWakelocks() { + when(mFoldGracePeriodProvider.isEnabled()).thenReturn(true); + + createService(); + // Start with AWAKE state + startSystem(); + assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); + + // Take a nap and verify we stay awake and the keyguard is requested + mService.getBinderServiceInstance().goToSleep(mClock.now(), + PowerManager.GO_TO_SLEEP_REASON_APPLICATION, + PowerManager.GO_TO_SLEEP_FLAG_SOFT_SLEEP); + assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); + verify(mNotifierMock).showDismissibleKeyguard(); + } + + @Test public void testWakefulnessSleep_SoftSleepFlag_WithPartialWakelock() { createService(); // Start with AWAKE state @@ -548,7 +574,7 @@ public class PowerManagerServiceTest { null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null /* callback */); - // Take a nap and verify we stay awake. + // Take a nap and verify we go to sleep. mService.getBinderServiceInstance().goToSleep(mClock.now(), PowerManager.GO_TO_SLEEP_REASON_APPLICATION, PowerManager.GO_TO_SLEEP_FLAG_SOFT_SLEEP); diff --git a/services/tests/servicestests/jni/Android.bp b/services/tests/servicestests/jni/Android.bp index 174beb81d3eb..c30e4eb666b4 100644 --- a/services/tests/servicestests/jni/Android.bp +++ b/services/tests/servicestests/jni/Android.bp @@ -23,6 +23,7 @@ cc_library_shared { ":lib_cachedAppOptimizer_native", ":lib_gameManagerService_native", ":lib_oomConnection_native", + ":lib_anrTimer_native", "onload.cpp", ], @@ -55,4 +56,4 @@ cc_library_shared { "android.hardware.graphics.mapper@4.0", "android.hidl.token@1.0-utils", ], -}
\ No newline at end of file +} diff --git a/services/tests/servicestests/jni/onload.cpp b/services/tests/servicestests/jni/onload.cpp index f160b3d97367..25487c5aabbe 100644 --- a/services/tests/servicestests/jni/onload.cpp +++ b/services/tests/servicestests/jni/onload.cpp @@ -27,6 +27,7 @@ namespace android { int register_android_server_am_CachedAppOptimizer(JNIEnv* env); int register_android_server_app_GameManagerService(JNIEnv* env); int register_android_server_am_OomConnection(JNIEnv* env); +int register_android_server_utils_AnrTimer(JNIEnv *env); }; using namespace android; @@ -44,5 +45,6 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) register_android_server_am_CachedAppOptimizer(env); register_android_server_app_GameManagerService(env); register_android_server_am_OomConnection(env); + register_android_server_utils_AnrTimer(env); return JNI_VERSION_1_4; } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java index efcdbd488a39..1cd61e90126e 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java @@ -44,10 +44,6 @@ import android.content.Context; import android.graphics.PointF; import android.os.Looper; import android.os.SystemClock; -import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.annotations.RequiresFlagsEnabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.DexmakerShareClassLoaderRule; import android.view.InputDevice; import android.view.MotionEvent; @@ -60,7 +56,6 @@ import androidx.test.runner.AndroidJUnit4; import com.android.server.accessibility.AccessibilityManagerService; import com.android.server.accessibility.AccessibilityTraceManager; import com.android.server.accessibility.EventStreamTransformation; -import com.android.server.accessibility.Flags; import com.android.server.accessibility.utils.GestureLogParser; import com.android.server.testutils.OffsettableClock; @@ -81,7 +76,6 @@ import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; - @RunWith(AndroidJUnit4.class) public class TouchExplorerTest { @@ -125,9 +119,6 @@ public class TouchExplorerTest { public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule = new DexmakerShareClassLoaderRule(); - @Rule - public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); - /** * {@link TouchExplorer#sendDownForAllNotInjectedPointers} injecting events with the same object * is resulting {@link ArgumentCaptor} to capture events with last state. Before implementation @@ -170,42 +161,11 @@ public class TouchExplorerTest { goFromStateClearTo(STATE_TOUCH_EXPLORING_1FINGER); // Wait for transiting to touch exploring state. mHandler.fastForward(2 * USER_INTENT_TIMEOUT); - assertState(STATE_TOUCH_EXPLORING); - // Manually construct the next move event. Using moveEachPointers() will batch the move - // event which produces zero movement for some reason. - float[] x = new float[1]; - float[] y = new float[1]; - x[0] = mLastEvent.getX(0) + mTouchSlop; - y[0] = mLastEvent.getY(0) + mTouchSlop; - send(manyPointerEvent(ACTION_MOVE, x, y)); - goToStateClearFrom(STATE_TOUCH_EXPLORING_1FINGER); - assertCapturedEvents(ACTION_HOVER_ENTER, ACTION_HOVER_MOVE, ACTION_HOVER_EXIT); - } - - /** - * Test the case where ACTION_DOWN is followed by a number of ACTION_MOVE events that do not - * change the coordinates. - */ - @Test - @RequiresFlagsEnabled(Flags.FLAG_REDUCE_TOUCH_EXPLORATION_SENSITIVITY) - public void testOneFingerMoveWithExtraMoveEvents_generatesOneMoveEvent() { - goFromStateClearTo(STATE_TOUCH_EXPLORING_1FINGER); - // Inject a set of move events that have the same coordinates as the down event. - moveEachPointers(mLastEvent, p(0, 0)); - send(mLastEvent); - // Wait for transition to touch exploring state. - mHandler.fastForward(2 * USER_INTENT_TIMEOUT); - // Now move for real. - moveAtLeastTouchSlop(mLastEvent); - send(mLastEvent); - // One more move event with no change. - moveEachPointers(mLastEvent, p(0, 0)); + moveEachPointers(mLastEvent, p(10, 10)); send(mLastEvent); goToStateClearFrom(STATE_TOUCH_EXPLORING_1FINGER); - assertCapturedEvents( - ACTION_HOVER_ENTER, - ACTION_HOVER_MOVE, - ACTION_HOVER_EXIT); + assertCapturedEvents(ACTION_HOVER_ENTER, ACTION_HOVER_MOVE, ACTION_HOVER_EXIT); + assertState(STATE_TOUCH_EXPLORING); } /** @@ -213,8 +173,7 @@ public class TouchExplorerTest { * change the coordinates. */ @Test - @RequiresFlagsDisabled(Flags.FLAG_REDUCE_TOUCH_EXPLORATION_SENSITIVITY) - public void testOneFingerMoveWithExtraMoveEvents_generatesThreeMoveEvent() { + public void testOneFingerMoveWithExtraMoveEvents() { goFromStateClearTo(STATE_TOUCH_EXPLORING_1FINGER); // Inject a set of move events that have the same coordinates as the down event. moveEachPointers(mLastEvent, p(0, 0)); @@ -222,7 +181,7 @@ public class TouchExplorerTest { // Wait for transition to touch exploring state. mHandler.fastForward(2 * USER_INTENT_TIMEOUT); // Now move for real. - moveAtLeastTouchSlop(mLastEvent); + moveEachPointers(mLastEvent, p(10, 10)); send(mLastEvent); // One more move event with no change. moveEachPointers(mLastEvent, p(0, 0)); @@ -283,7 +242,7 @@ public class TouchExplorerTest { moveEachPointers(mLastEvent, p(0, 0), p(0, 0)); send(mLastEvent); // Now move for real. - moveEachPointers(mLastEvent, p(mTouchSlop, mTouchSlop), p(mTouchSlop, mTouchSlop)); + moveEachPointers(mLastEvent, p(10, 10), p(10, 10)); send(mLastEvent); goToStateClearFrom(STATE_DRAGGING_2FINGERS); assertCapturedEvents(ACTION_DOWN, ACTION_MOVE, ACTION_MOVE, ACTION_MOVE, ACTION_UP); @@ -292,7 +251,7 @@ public class TouchExplorerTest { @Test public void testUpEvent_OneFingerMove_clearStateAndInjectHoverEvents() { goFromStateClearTo(STATE_TOUCH_EXPLORING_1FINGER); - moveAtLeastTouchSlop(mLastEvent); + moveEachPointers(mLastEvent, p(10, 10)); send(mLastEvent); // Wait 10 ms to make sure that hover enter and exit are not scheduled for the same moment. mHandler.fastForward(10); @@ -318,7 +277,7 @@ public class TouchExplorerTest { // Wait for the finger moving to the second view. mHandler.fastForward(oneThirdUserIntentTimeout); - moveAtLeastTouchSlop(mLastEvent); + moveEachPointers(mLastEvent, p(10, 10)); send(mLastEvent); // Wait for the finger lifting from the second view. @@ -443,6 +402,7 @@ public class TouchExplorerTest { // Manually construct the next move event. Using moveEachPointers() will batch the move // event onto the pointer up event which will mean that the move event still has a pointer // count of 3. + // Todo: refactor to avoid using batching as there is no special reason to do it that way. float[] x = new float[2]; float[] y = new float[2]; x[0] = mLastEvent.getX(0) + 100; @@ -774,9 +734,6 @@ public class TouchExplorerTest { } } - private void moveAtLeastTouchSlop(MotionEvent event) { - moveEachPointers(event, p(2 * mTouchSlop, 0)); - } /** * A {@link android.os.Handler} that doesn't process messages until {@link #fastForward(int)} is * invoked. diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java index 59c94dc1f250..89a49615dbe1 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java @@ -65,7 +65,6 @@ import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintProvide import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintResetLockoutClient; import org.junit.Before; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; @@ -248,7 +247,6 @@ public class HidlToAidlSensorAdapterTest { } @Test - @Ignore("b/317403648") public void lockoutPermanentResetViaClient() { setLockoutPermanent(); diff --git a/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java b/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java index 861d14a2cf66..6c085e085f4e 100644 --- a/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java +++ b/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java @@ -23,17 +23,21 @@ import static org.junit.Assert.assertTrue; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.util.Log; import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; import com.android.internal.annotations.GuardedBy; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; +import java.io.PrintWriter; +import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -45,6 +49,9 @@ import java.util.concurrent.TimeUnit; @RunWith(Parameterized.class) public class AnrTimerTest { + // A log tag. + private static final String TAG = "AnrTimerTest"; + // The commonly used message timeout key. private static final int MSG_TIMEOUT = 1; @@ -63,9 +70,7 @@ public class AnrTimerTest { } } - /** - * The test handler is a self-contained object for a single test. - */ + /** The test helper is a self-contained object for a single test. */ private static class Helper { final Object mLock = new Object(); @@ -114,7 +119,7 @@ public class AnrTimerTest { /** * Force AnrTimer to use the test parameter for the feature flag. */ - class TestInjector extends AnrTimer.Injector { + private class TestInjector extends AnrTimer.Injector { @Override boolean anrTimerServiceEnabled() { return mEnabled; @@ -124,9 +129,9 @@ public class AnrTimerTest { /** * An instrumented AnrTimer. */ - private static class TestAnrTimer extends AnrTimer<TestArg> { + private class TestAnrTimer extends AnrTimer<TestArg> { private TestAnrTimer(Handler h, int key, String tag) { - super(h, key, tag); + super(h, key, tag, false, new TestInjector()); } TestAnrTimer(Helper helper) { @@ -173,35 +178,103 @@ public class AnrTimerTest { @Test public void testSimpleTimeout() throws Exception { Helper helper = new Helper(1); - TestAnrTimer timer = new TestAnrTimer(helper); - TestArg t = new TestArg(1, 1); - timer.start(t, 10); - // Delivery is immediate but occurs on a different thread. - assertTrue(helper.await(5000)); - TestArg[] result = helper.messages(1); - validate(t, result[0]); + try (TestAnrTimer timer = new TestAnrTimer(helper)) { + // One-time check that the injector is working as expected. + assertEquals(mEnabled, timer.serviceEnabled()); + TestArg t = new TestArg(1, 1); + timer.start(t, 10); + // Delivery is immediate but occurs on a different thread. + assertTrue(helper.await(5000)); + TestArg[] result = helper.messages(1); + validate(t, result[0]); + } } /** - * Verify that if three timers are scheduled, they are delivered in time order. + * Verify that a restarted timer is delivered exactly once. The initial timer value is very + * large, to ensure it does not expire before the timer can be restarted. + */ + @Test + public void testTimerRestart() throws Exception { + Helper helper = new Helper(1); + try (TestAnrTimer timer = new TestAnrTimer(helper)) { + TestArg t = new TestArg(1, 1); + timer.start(t, 10000); + // Briefly pause. + assertFalse(helper.await(10)); + timer.start(t, 10); + // Delivery is immediate but occurs on a different thread. + assertTrue(helper.await(5000)); + TestArg[] result = helper.messages(1); + validate(t, result[0]); + } + } + + /** + * Verify that a restarted timer is delivered exactly once. The initial timer value is very + * large, to ensure it does not expire before the timer can be restarted. + */ + @Test + public void testTimerZero() throws Exception { + Helper helper = new Helper(1); + try (TestAnrTimer timer = new TestAnrTimer(helper)) { + TestArg t = new TestArg(1, 1); + timer.start(t, 0); + // Delivery is immediate but occurs on a different thread. + assertTrue(helper.await(5000)); + TestArg[] result = helper.messages(1); + validate(t, result[0]); + } + } + + /** + * Verify that if three timers are scheduled on a single AnrTimer, they are delivered in time + * order. */ @Test public void testMultipleTimers() throws Exception { // Expect three messages. Helper helper = new Helper(3); - TestAnrTimer timer = new TestAnrTimer(helper); TestArg t1 = new TestArg(1, 1); TestArg t2 = new TestArg(1, 2); TestArg t3 = new TestArg(1, 3); - timer.start(t1, 50); - timer.start(t2, 60); - timer.start(t3, 40); - // Delivery is immediate but occurs on a different thread. - assertTrue(helper.await(5000)); - TestArg[] result = helper.messages(3); - validate(t3, result[0]); - validate(t1, result[1]); - validate(t2, result[2]); + try (TestAnrTimer timer = new TestAnrTimer(helper)) { + timer.start(t1, 50); + timer.start(t2, 60); + timer.start(t3, 40); + // Delivery is immediate but occurs on a different thread. + assertTrue(helper.await(5000)); + TestArg[] result = helper.messages(3); + validate(t3, result[0]); + validate(t1, result[1]); + validate(t2, result[2]); + } + } + + /** + * Verify that if three timers are scheduled on three separate AnrTimers, they are delivered + * in time order. + */ + @Test + public void testMultipleServices() throws Exception { + // Expect three messages. + Helper helper = new Helper(3); + TestArg t1 = new TestArg(1, 1); + TestArg t2 = new TestArg(1, 2); + TestArg t3 = new TestArg(1, 3); + try (TestAnrTimer x1 = new TestAnrTimer(helper); + TestAnrTimer x2 = new TestAnrTimer(helper); + TestAnrTimer x3 = new TestAnrTimer(helper)) { + x1.start(t1, 50); + x2.start(t2, 60); + x3.start(t3, 40); + // Delivery is immediate but occurs on a different thread. + assertTrue(helper.await(5000)); + TestArg[] result = helper.messages(3); + validate(t3, result[0]); + validate(t1, result[1]); + validate(t2, result[2]); + } } /** @@ -211,20 +284,109 @@ public class AnrTimerTest { public void testCancelTimer() throws Exception { // Expect two messages. Helper helper = new Helper(2); - TestAnrTimer timer = new TestAnrTimer(helper); TestArg t1 = new TestArg(1, 1); TestArg t2 = new TestArg(1, 2); TestArg t3 = new TestArg(1, 3); - timer.start(t1, 50); - timer.start(t2, 60); - timer.start(t3, 40); - // Briefly pause. - assertFalse(helper.await(10)); - timer.cancel(t1); - // Delivery is immediate but occurs on a different thread. - assertTrue(helper.await(5000)); - TestArg[] result = helper.messages(2); - validate(t3, result[0]); - validate(t2, result[1]); + try (TestAnrTimer timer = new TestAnrTimer(helper)) { + timer.start(t1, 50); + timer.start(t2, 60); + timer.start(t3, 40); + // Briefly pause. + assertFalse(helper.await(10)); + timer.cancel(t1); + // Delivery is immediate but occurs on a different thread. + assertTrue(helper.await(5000)); + TestArg[] result = helper.messages(2); + validate(t3, result[0]); + validate(t2, result[1]); + } + } + + /** + * Return the dump string. + */ + private String getDumpOutput() { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + AnrTimer.dump(pw, true, new TestInjector()); + pw.close(); + return sw.getBuffer().toString(); + } + + /** + * Verify the dump output. + */ + @Test + public void testDumpOutput() throws Exception { + String r1 = getDumpOutput(); + assertEquals(false, r1.contains("timer:")); + + Helper helper = new Helper(2); + TestArg t1 = new TestArg(1, 1); + TestArg t2 = new TestArg(1, 2); + TestArg t3 = new TestArg(1, 3); + try (TestAnrTimer timer = new TestAnrTimer(helper)) { + timer.start(t1, 5000); + timer.start(t2, 5000); + timer.start(t3, 5000); + + String r2 = getDumpOutput(); + // There are timers in the list if and only if the feature is enabled. + final boolean expected = mEnabled; + assertEquals(expected, r2.contains("timer:")); + } + + String r3 = getDumpOutput(); + assertEquals(false, r3.contains("timer:")); + } + + /** + * Verify that GC works as expected. This test will almost certainly be flaky, since it + * relies on the finalizers running, which is a best-effort on the part of the JVM. + * Therefore, the test is marked @Ignore. Remove that annotation to run the test locally. + */ + @Ignore + @Test + public void testGarbageCollection() throws Exception { + if (!mEnabled) return; + + String r1 = getDumpOutput(); + assertEquals(false, r1.contains("timer:")); + + Helper helper = new Helper(2); + TestArg t1 = new TestArg(1, 1); + TestArg t2 = new TestArg(1, 2); + TestArg t3 = new TestArg(1, 3); + // The timer is explicitly not closed. It is, however, scoped to the next block. + { + TestAnrTimer timer = new TestAnrTimer(helper); + timer.start(t1, 5000); + timer.start(t2, 5000); + timer.start(t3, 5000); + + String r2 = getDumpOutput(); + // There are timers in the list if and only if the feature is enabled. + final boolean expected = mEnabled; + assertEquals(expected, r2.contains("timer:")); + } + + // Try to make finalizers run. The timer object above should be a candidate. Finalizers + // are run on their own thread, so pause this thread to give that thread some time. + String r3 = getDumpOutput(); + for (int i = 0; i < 10 && r3.contains("timer:"); i++) { + Log.i(TAG, "requesting finalization " + i); + System.gc(); + System.runFinalization(); + Thread.sleep(4 * 1000); + r3 = getDumpOutput(); + } + + // The timer was not explicitly closed but it should have been implicitly closed by GC. + assertEquals(false, r3.contains("timer:")); + } + + // TODO: [b/302724778] Remove manual JNI load + static { + System.loadLibrary("servicestestjni"); } } 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 39779b00f62f..f1edd9a59b99 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -303,7 +303,6 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.ClassRule; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; @@ -14061,7 +14060,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @Ignore("b/316989461") public void cancelNotificationsFromListener_rapidClear_oldNew_cancelOne() throws RemoteException { mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags @@ -14073,7 +14071,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.addNotification(nr1); // Create old notification. - final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0); + final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, + System.currentTimeMillis() - 60000); mService.addNotification(nr2); // Cancel specific notifications via listener. @@ -14091,16 +14090,17 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @Ignore("b/316989461") public void cancelNotificationsFromListener_rapidClear_old_cancelOne() throws RemoteException { mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags .FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED); // Create old notifications. - final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel, 0); + final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel, + System.currentTimeMillis() - 60000); mService.addNotification(nr1); - final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0); + final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, + System.currentTimeMillis() - 60000); mService.addNotification(nr2); // Cancel specific notifications via listener. @@ -14119,7 +14119,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @Ignore("b/316989461") public void cancelNotificationsFromListener_rapidClear_oldNew_cancelOne_flagDisabled() throws RemoteException { mSetFlagsRule.disableFlags(android.view.contentprotection.flags.Flags @@ -14131,7 +14130,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.addNotification(nr1); // Create old notification. - final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0); + final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, + System.currentTimeMillis() - 60000); mService.addNotification(nr2); // Cancel specific notifications via listener. @@ -14150,7 +14150,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @Ignore("b/316989461") public void cancelNotificationsFromListener_rapidClear_oldNew_cancelAll() throws RemoteException { mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags @@ -14162,7 +14161,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.addNotification(nr1); // Create old notification. - final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0); + final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, + System.currentTimeMillis() - 60000); mService.addNotification(nr2); // Cancel all notifications via listener. @@ -14179,16 +14179,17 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @Ignore("b/316989461") public void cancelNotificationsFromListener_rapidClear_old_cancelAll() throws RemoteException { mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags .FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED); // Create old notifications. - final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel, 0); + final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel, + System.currentTimeMillis() - 60000); mService.addNotification(nr1); - final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0); + final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, + System.currentTimeMillis() - 60000); mService.addNotification(nr2); // Cancel all notifications via listener. @@ -14206,7 +14207,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @Ignore("b/316989461") public void cancelNotificationsFromListener_rapidClear_oldNew_cancelAll_flagDisabled() throws RemoteException { mSetFlagsRule.disableFlags(android.view.contentprotection.flags.Flags @@ -14218,7 +14218,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.addNotification(nr1); // Create old notification. - final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0); + final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, + System.currentTimeMillis() - 60000); mService.addNotification(nr2); // Cancel all notifications via listener. diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java index 6cc1c4365fca..08af09c20de5 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java @@ -20,7 +20,11 @@ import static com.android.server.notification.ZenAdapters.notificationPolicyToZe import static com.google.common.truth.Truth.assertThat; +import android.app.Flags; import android.app.NotificationManager.Policy; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.service.notification.ZenPolicy; import androidx.test.filters.SmallTest; @@ -28,6 +32,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.server.UiServiceTestCase; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -35,6 +40,9 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class ZenAdaptersTest extends UiServiceTestCase { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Test public void notificationPolicyToZenPolicy_allCallers() { Policy policy = new Policy(Policy.PRIORITY_CATEGORY_CALLS, Policy.PRIORITY_SENDERS_ANY, 0); @@ -127,4 +135,35 @@ public class ZenAdaptersTest extends UiServiceTestCase { assertThat(zenPolicy.getVisualEffectPeek()).isEqualTo(ZenPolicy.STATE_UNSET); assertThat(zenPolicy.getVisualEffectStatusBar()).isEqualTo(ZenPolicy.STATE_UNSET); } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void notificationPolicyToZenPolicy_modesApi_priorityChannels() { + Policy policy = new Policy(0, 0, 0, 0, + Policy.policyState(false, true), 0); + + ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy); + assertThat(zenPolicy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY); + + Policy notAllowed = new Policy(0, 0, 0, 0, + Policy.policyState(false, false), 0); + ZenPolicy zenPolicyNotAllowed = notificationPolicyToZenPolicy(notAllowed); + assertThat(zenPolicyNotAllowed.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_NONE); + } + + @Test + @DisableFlags(Flags.FLAG_MODES_API) + public void notificationPolicyToZenPolicy_noModesApi_priorityChannelsUnset() { + Policy policy = new Policy(0, 0, 0, 0, + Policy.policyState(false, true), 0); + + ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy); + assertThat(zenPolicy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_UNSET); + + Policy notAllowed = new Policy(0, 0, 0, 0, + Policy.policyState(false, false), 0); + ZenPolicy zenPolicyNotAllowed = notificationPolicyToZenPolicy(notAllowed); + assertThat(zenPolicyNotAllowed.getAllowedChannels()) + .isEqualTo(ZenPolicy.CHANNEL_TYPE_UNSET); + } } 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 25c0cd9fae25..f84d8e95e426 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -3876,6 +3876,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .allowCalls(PEOPLE_TYPE_CONTACTS) .allowConversations(CONVERSATION_SENDERS_IMPORTANT) .hideAllVisualEffects() + .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY) .build(); assertThat(mZenModeHelper.mConfig.automaticRules.values()) .comparingElementsUsing(IGNORE_TIMESTAMPS) @@ -3907,6 +3908,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .allowCalls(PEOPLE_TYPE_STARRED) .allowConversations(CONVERSATION_SENDERS_IMPORTANT) .hideAllVisualEffects() + .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY) .build(); assertThat(mZenModeHelper.mConfig.automaticRules.values()) .comparingElementsUsing(IGNORE_TIMESTAMPS) diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java index 8a9c05d07b26..c82f7513e347 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java @@ -88,6 +88,7 @@ import static org.mockito.ArgumentMatchers.notNull; import android.app.ActivityOptions; import android.app.AppOpsManager; import android.app.BackgroundStartPrivileges; +import android.app.ComponentOptions.BackgroundActivityStartMode; import android.app.IApplicationThread; import android.app.PictureInPictureParams; import android.content.ComponentName; @@ -914,24 +915,78 @@ public class ActivityStarterTests extends WindowTestsBase { .mockStatic(FrameworkStatsLog.class) .strictness(Strictness.LENIENT) .startMocking(); - doReturn(PERMISSION_GRANTED).when(() -> ActivityTaskManagerService.checkPermission( - eq(START_ACTIVITIES_FROM_BACKGROUND), - anyInt(), anyInt())); - runAndVerifyBackgroundActivityStartsSubtest( - "allowed_notAborted", false, - UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, - UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, true, false, false, false, false, false, false); - verify(() -> FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED, - "", // activity name - BackgroundActivityStartController.BAL_ALLOW_PERMISSION, - UNIMPORTANT_UID, - UNIMPORTANT_UID2)); - mockingSession.finishMocking(); + try { + doReturn(PERMISSION_GRANTED).when(() -> ActivityTaskManagerService.checkPermission( + eq(START_ACTIVITIES_FROM_BACKGROUND), + anyInt(), anyInt())); + runAndVerifyBackgroundActivityStartsSubtest( + "allowed_notAborted", false, + UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, + UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, + false, true, false, false, false, false, false, false); + verify(() -> FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED, + "", // activity name + BackgroundActivityStartController.BAL_ALLOW_PERMISSION, + UNIMPORTANT_UID, + UNIMPORTANT_UID2, + BackgroundActivityStartController.BAL_ALLOW_PERMISSION, + true, // opt in + false, // but no explicit opt in + BackgroundActivityStartController.BAL_BLOCK, + true, // opt in + false // but no explicit opt in + )); + } finally { + mockingSession.finishMocking(); + } + } + + /** + * This test ensures proper logging for BAL_ALLOW_PENDING_INTENT, when the PendingIntent sender + * is the only reason BAL is allowed. + */ + @Test + public void testBackgroundActivityStartsAllowed_loggingOnlyPendingIntentAllowed() { + doReturn(false).when(mAtm).isBackgroundActivityStartsEnabled(); + MockitoSession mockingSession = mockitoSession() + .mockStatic(ActivityTaskManagerService.class) + .mockStatic(FrameworkStatsLog.class) + .mockStatic(PendingIntentRecord.class) + .strictness(Strictness.LENIENT) + .startMocking(); + try { + doReturn(PERMISSION_GRANTED).when(() -> ActivityTaskManagerService.checkPermission( + eq(START_ACTIVITIES_FROM_BACKGROUND), + anyInt(), anyInt())); + doReturn(BackgroundStartPrivileges.allowBackgroundActivityStarts(null)).when( + () -> PendingIntentRecord.getBackgroundStartPrivilegesAllowedByCaller( + anyObject(), anyInt(), anyObject())); + runAndVerifyBackgroundActivityStartsSubtest( + "allowed_notAborted", false, + UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, + Process.SYSTEM_UID, true, PROCESS_STATE_BOUND_TOP, + false, true, false, false, false, false, false, false, + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED); + verify(() -> FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED, + DEFAULT_COMPONENT_PACKAGE_NAME + "/" + DEFAULT_COMPONENT_PACKAGE_NAME, + BackgroundActivityStartController.BAL_ALLOW_PENDING_INTENT, + UNIMPORTANT_UID, + Process.SYSTEM_UID, + BackgroundActivityStartController.BAL_ALLOW_PERMISSION, + false, // opt in + true, // explicit opt out + BackgroundActivityStartController.BAL_ALLOW_VISIBLE_WINDOW, + true, // opt in + false // but no explicit opt in + )); + } finally { + mockingSession.finishMocking(); + } } /** - * This test ensures proper logging for BAL_ALLOW_PENDING_INTENT. + * This test ensures proper logging for BAL_ALLOW_PENDING_INTENT, when the PendingIntent sender + * is not the primary reason to allow BAL (but the creator). */ @Test public void testBackgroundActivityStartsAllowed_loggingPendingIntentAllowed() { @@ -942,23 +997,34 @@ public class ActivityStarterTests extends WindowTestsBase { .mockStatic(PendingIntentRecord.class) .strictness(Strictness.LENIENT) .startMocking(); - doReturn(PERMISSION_GRANTED).when(() -> ActivityTaskManagerService.checkPermission( - eq(START_ACTIVITIES_FROM_BACKGROUND), - anyInt(), anyInt())); - doReturn(BackgroundStartPrivileges.allowBackgroundActivityStarts(null)).when( - () -> PendingIntentRecord.getBackgroundStartPrivilegesAllowedByCaller( - anyObject(), anyInt(), anyObject())); - runAndVerifyBackgroundActivityStartsSubtest( - "allowed_notAborted", false, - UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, - Process.SYSTEM_UID, true, PROCESS_STATE_BOUND_TOP, - false, true, false, false, false, false, false, false); - verify(() -> FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED, - DEFAULT_COMPONENT_PACKAGE_NAME + "/" + DEFAULT_COMPONENT_PACKAGE_NAME, - BackgroundActivityStartController.BAL_ALLOW_PENDING_INTENT, - UNIMPORTANT_UID, - Process.SYSTEM_UID)); - mockingSession.finishMocking(); + try { + doReturn(PERMISSION_GRANTED).when(() -> ActivityTaskManagerService.checkPermission( + eq(START_ACTIVITIES_FROM_BACKGROUND), + anyInt(), anyInt())); + doReturn(BackgroundStartPrivileges.allowBackgroundActivityStarts(null)).when( + () -> PendingIntentRecord.getBackgroundStartPrivilegesAllowedByCaller( + anyObject(), anyInt(), anyObject())); + runAndVerifyBackgroundActivityStartsSubtest( + "allowed_notAborted", false, + UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, + Process.SYSTEM_UID, true, PROCESS_STATE_BOUND_TOP, + false, true, false, false, false, false, false, false, + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); + verify(() -> FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED, + "", + BackgroundActivityStartController.BAL_ALLOW_PERMISSION, + UNIMPORTANT_UID, + Process.SYSTEM_UID, + BackgroundActivityStartController.BAL_ALLOW_PERMISSION, + true, // opt in + true, // explicit opt in + BackgroundActivityStartController.BAL_ALLOW_VISIBLE_WINDOW, + true, // opt in + false // but no explicit opt in + )); + } finally { + mockingSession.finishMocking(); + } } private void runAndVerifyBackgroundActivityStartsSubtest(String name, boolean shouldHaveAborted, @@ -971,6 +1037,27 @@ public class ActivityStarterTests extends WindowTestsBase { boolean isCallingUidAffiliatedProfileOwner, boolean isPinnedSingleInstance, boolean hasSystemExemptAppOp) { + runAndVerifyBackgroundActivityStartsSubtest(name, shouldHaveAborted, callingUid, + callingUidHasVisibleWindow, callingUidProcState, realCallingUid, + realCallingUidHasVisibleWindow, realCallingUidProcState, hasForegroundActivities, + callerIsRecents, callerIsTempAllowed, + callerIsInstrumentingWithBackgroundActivityStartPrivileges, + isCallingUidDeviceOwner, isCallingUidAffiliatedProfileOwner, isPinnedSingleInstance, + hasSystemExemptAppOp, + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED); + } + + private void runAndVerifyBackgroundActivityStartsSubtest(String name, boolean shouldHaveAborted, + int callingUid, boolean callingUidHasVisibleWindow, int callingUidProcState, + int realCallingUid, boolean realCallingUidHasVisibleWindow, int realCallingUidProcState, + boolean hasForegroundActivities, boolean callerIsRecents, + boolean callerIsTempAllowed, + boolean callerIsInstrumentingWithBackgroundActivityStartPrivileges, + boolean isCallingUidDeviceOwner, + boolean isCallingUidAffiliatedProfileOwner, + boolean isPinnedSingleInstance, + boolean hasSystemExemptAppOp, + @BackgroundActivityStartMode int pendingIntentCreatorBackgroundActivityStartMode) { // window visibility doReturn(callingUidHasVisibleWindow).when(mAtm).hasActiveVisibleWindow(callingUid); doReturn(realCallingUidHasVisibleWindow).when(mAtm).hasActiveVisibleWindow(realCallingUid); @@ -1022,7 +1109,10 @@ public class ActivityStarterTests extends WindowTestsBase { launchMode = LAUNCH_SINGLE_INSTANCE; } - final ActivityOptions options = spy(ActivityOptions.makeBasic()); + ActivityOptions rawOptions = ActivityOptions.makeBasic() + .setPendingIntentCreatorBackgroundActivityStartMode( + pendingIntentCreatorBackgroundActivityStartMode); + final ActivityOptions options = spy(rawOptions); ActivityRecord[] outActivity = new ActivityRecord[1]; ActivityStarter starter = prepareStarter( FLAG_ACTIVITY_NEW_TASK, true, launchMode) diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 6497ee9cb1f2..782d89cdcd29 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -115,6 +115,9 @@ import android.os.Binder; import android.os.RemoteException; import android.os.SystemClock; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.util.ArraySet; import android.util.DisplayMetrics; import android.view.Display; @@ -146,6 +149,7 @@ import com.android.server.LocalServices; import com.android.server.policy.WindowManagerPolicy; import com.android.server.wm.utils.WmDisplayCutout; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -172,6 +176,10 @@ import java.util.concurrent.TimeoutException; @RunWith(WindowTestRunner.class) public class DisplayContentTests extends WindowTestsBase { + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + @SetupWindows(addAllCommonWindows = true) @Test public void testForAllWindows() { @@ -508,6 +516,7 @@ public class DisplayContentTests extends WindowTestsBase { * Tests tapping on a root task in different display results in window gaining focus. */ @Test + @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_REMOVE_POINTER_EVENT_TRACKING_IN_WM) public void testInputEventBringsCorrectDisplayInFocus() { DisplayContent dc0 = mWm.getDefaultDisplayContentLocked(); // Create a second display diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index d36ee2c62fd2..a88285ac4c8f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -34,6 +34,7 @@ import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_TOP_OF_TAS import static android.window.TaskFragmentOperation.OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_DIM_ON_TASK; import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_OP_TYPE; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_THROWABLE; @@ -870,12 +871,19 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { .setAnimationParams(animationParams) .build(); mTransaction.addTaskFragmentOperation(mFragmentToken, operation); + final TaskFragmentOperation dimOperation = new TaskFragmentOperation.Builder( + OP_TYPE_SET_DIM_ON_TASK) + .setDimOnTask(true) + .build(); + mTransaction.addTaskFragmentOperation(mFragmentToken, dimOperation); mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE, false /* shouldApplyIndependently */); assertApplyTransactionAllowed(mTransaction); assertEquals(animationParams, mTaskFragment.getAnimationParams()); assertEquals(Color.GREEN, mTaskFragment.getAnimationParams().getAnimationBackgroundColor()); + + assertTrue(mTaskFragment.isDimmingOnParentTask()); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java index 875e708ce1da..e9fe4bb91329 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -684,6 +684,9 @@ public class TaskFragmentTest extends WindowTestsBase { // Return Task bounds if dimming on parent Task. final Rect dimBounds = new Rect(); mTaskFragment.setEmbeddedDimArea(EMBEDDED_DIM_AREA_PARENT_TASK); + final Dimmer dimmer = mTaskFragment.getDimmer(); + spyOn(dimmer); + doReturn(taskBounds).when(dimmer).getDimBounds(); mTaskFragment.getDimBounds(dimBounds); assertEquals(taskBounds, dimBounds); diff --git a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java index 52e2d8abc2d5..7551b1650ad0 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java +++ b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java @@ -330,6 +330,10 @@ class TestWindowManagerPolicy implements WindowManagerPolicy { } @Override + public void showDismissibleKeyguard() { + } + + @Override public void setPipVisibilityLw(boolean visible) { } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index 8bf4833bc2ca..21fee7286a7b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -37,6 +37,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG; import static android.view.WindowManager.LayoutParams.TYPE_TOAST; import static android.window.DisplayAreaOrganizer.FEATURE_VENDOR_FIRST; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.never; @@ -85,6 +86,7 @@ import android.view.IWindow; import android.view.InputChannel; import android.view.InsetsSourceControl; import android.view.InsetsState; +import android.view.Surface; import android.view.SurfaceControl; import android.view.View; import android.view.WindowInsets; @@ -952,6 +954,57 @@ public class WindowManagerServiceTests extends WindowTestsBase { } @Test + public void testDrawMagnifiedViewport() { + final int displayId = mDisplayContent.mDisplayId; + // Use real surface, so ViewportWindow's BlastBufferQueue can be created. + final ArrayList<SurfaceControl> surfaceControls = new ArrayList<>(); + mWm.mSurfaceControlFactory = s -> new SurfaceControl.Builder() { + @Override + public SurfaceControl build() { + final SurfaceControl sc = super.build(); + surfaceControls.add(sc); + return sc; + } + }; + mWm.mAccessibilityController.setMagnificationCallbacks(displayId, + mock(WindowManagerInternal.MagnificationCallbacks.class)); + final boolean[] lockCanvasInWmLock = { false }; + final Surface surface = mWm.mAccessibilityController.forceShowMagnifierSurface(displayId); + spyOn(surface); + doAnswer(invocationOnMock -> { + lockCanvasInWmLock[0] |= Thread.holdsLock(mWm.mGlobalLock); + invocationOnMock.callRealMethod(); + return null; + }).when(surface).lockCanvas(any()); + mWm.mAccessibilityController.drawMagnifiedRegionBorderIfNeeded(displayId, mTransaction); + waitUntilHandlersIdle(); + try { + verify(surface).lockCanvas(any()); + + clearInvocations(surface); + // Invalidate and redraw. + mWm.mAccessibilityController.onDisplaySizeChanged(mDisplayContent); + mWm.mAccessibilityController.drawMagnifiedRegionBorderIfNeeded(displayId, mTransaction); + // Turn off magnification to release surface. + mWm.mAccessibilityController.setMagnificationCallbacks(displayId, null); + if (!com.android.window.flags.Flags.drawMagnifierBorderOutsideWmlock()) { + verify(surface).release(); + assertTrue(lockCanvasInWmLock[0]); + return; + } + waitUntilHandlersIdle(); + // lockCanvas must not be called after releasing. + verify(surface, never()).lockCanvas(any()); + verify(surface).release(); + assertFalse(lockCanvasInWmLock[0]); + } finally { + for (int i = surfaceControls.size() - 1; i >= 0; --i) { + surfaceControls.get(i).release(); + } + } + } + + @Test public void testRequestKeyboardShortcuts_noWindow() { doNothing().when(mWm.mContext).enforceCallingOrSelfPermission(anyString(), anyString()); doReturn(null).when(mWm).getFocusedWindowLocked(); diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java index d05eb5cd2d4c..57b13e960093 100644 --- a/telecomm/java/android/telecom/TelecomManager.java +++ b/telecomm/java/android/telecom/TelecomManager.java @@ -422,8 +422,8 @@ public class TelecomManager { "android.telecom.extra.CALL_CREATED_TIME_MILLIS"; /** - * The extra for call log uri that was used to mark missed calls as read when dialer gets the - * notification on reboot. + * Extra URI that is used by a dialer to query the {@link android.provider.CallLog} content + * provider and associate a missed call notification with a call log entry. */ @FlaggedApi(Flags.FLAG_ADD_CALL_URI_FOR_MISSED_CALLS) public static final String EXTRA_CALL_LOG_URI = diff --git a/telephony/java/android/telephony/SecurityAlgorithmUpdate.java b/telephony/java/android/telephony/SecurityAlgorithmUpdate.java index 61d7ead67a21..c902016d2cf0 100644 --- a/telephony/java/android/telephony/SecurityAlgorithmUpdate.java +++ b/telephony/java/android/telephony/SecurityAlgorithmUpdate.java @@ -169,6 +169,7 @@ public final class SecurityAlgorithmUpdate implements Parcelable { public static final int SECURITY_ALGORITHM_NEA1 = 56; public static final int SECURITY_ALGORITHM_NEA2 = 57; public static final int SECURITY_ALGORITHM_NEA3 = 58; + public static final int SECURITY_ALGORITHM_IMS_NULL = 67; public static final int SECURITY_ALGORITHM_SIP_NULL = 68; public static final int SECURITY_ALGORITHM_AES_GCM = 69; public static final int SECURITY_ALGORITHM_AES_GMAC = 70; @@ -176,9 +177,8 @@ public final class SecurityAlgorithmUpdate implements Parcelable { public static final int SECURITY_ALGORITHM_DES_EDE3_CBC = 72; public static final int SECURITY_ALGORITHM_AES_EDE3_CBC = 73; public static final int SECURITY_ALGORITHM_HMAC_SHA1_96 = 74; - public static final int SECURITY_ALGORITHM_HMAC_SHA1_96_NULL = 75; - public static final int SECURITY_ALGORITHM_HMAC_MD5_96 = 76; - public static final int SECURITY_ALGORITHM_HMAC_MD5_96_NULL = 77; + public static final int SECURITY_ALGORITHM_HMAC_MD5_96 = 75; + public static final int SECURITY_ALGORITHM_SRTP_NULL = 86; public static final int SECURITY_ALGORITHM_SRTP_AES_COUNTER = 87; public static final int SECURITY_ALGORITHM_SRTP_AES_F8 = 88; public static final int SECURITY_ALGORITHM_SRTP_HMAC_SHA1 = 89; @@ -199,15 +199,15 @@ public final class SecurityAlgorithmUpdate implements Parcelable { SECURITY_ALGORITHM_UEA2, SECURITY_ALGORITHM_EEA0, SECURITY_ALGORITHM_EEA1, SECURITY_ALGORITHM_EEA2, SECURITY_ALGORITHM_EEA3, SECURITY_ALGORITHM_NEA0, SECURITY_ALGORITHM_NEA1, SECURITY_ALGORITHM_NEA2, SECURITY_ALGORITHM_NEA3, - SECURITY_ALGORITHM_SIP_NULL, SECURITY_ALGORITHM_AES_GCM, + SECURITY_ALGORITHM_IMS_NULL, SECURITY_ALGORITHM_SIP_NULL, SECURITY_ALGORITHM_AES_GCM, SECURITY_ALGORITHM_AES_GMAC, SECURITY_ALGORITHM_AES_CBC, SECURITY_ALGORITHM_DES_EDE3_CBC, SECURITY_ALGORITHM_AES_EDE3_CBC, - SECURITY_ALGORITHM_HMAC_SHA1_96, SECURITY_ALGORITHM_HMAC_SHA1_96_NULL, - SECURITY_ALGORITHM_HMAC_MD5_96, SECURITY_ALGORITHM_HMAC_MD5_96_NULL, - SECURITY_ALGORITHM_SRTP_AES_COUNTER, SECURITY_ALGORITHM_SRTP_AES_F8, - SECURITY_ALGORITHM_SRTP_HMAC_SHA1, SECURITY_ALGORITHM_ENCR_AES_GCM_16, - SECURITY_ALGORITHM_ENCR_AES_CBC, SECURITY_ALGORITHM_AUTH_HMAC_SHA2_256_128, - SECURITY_ALGORITHM_UNKNOWN, SECURITY_ALGORITHM_OTHER, SECURITY_ALGORITHM_ORYX}) + SECURITY_ALGORITHM_HMAC_SHA1_96, SECURITY_ALGORITHM_HMAC_MD5_96, + SECURITY_ALGORITHM_SRTP_NULL, SECURITY_ALGORITHM_SRTP_AES_COUNTER, + SECURITY_ALGORITHM_SRTP_AES_F8, SECURITY_ALGORITHM_SRTP_HMAC_SHA1, + SECURITY_ALGORITHM_ENCR_AES_GCM_16, SECURITY_ALGORITHM_ENCR_AES_CBC, + SECURITY_ALGORITHM_AUTH_HMAC_SHA2_256_128, SECURITY_ALGORITHM_UNKNOWN, + SECURITY_ALGORITHM_OTHER, SECURITY_ALGORITHM_ORYX}) public @interface SecurityAlgorithm { } diff --git a/telephony/java/android/telephony/SmsManager.java b/telephony/java/android/telephony/SmsManager.java index df349f89fbf8..c958aba1d758 100644 --- a/telephony/java/android/telephony/SmsManager.java +++ b/telephony/java/android/telephony/SmsManager.java @@ -563,7 +563,10 @@ public final class SmsManager { * raw pdu of the status report is in the extended data ("pdu"). * * @throws IllegalArgumentException if destinationAddress or text are empty + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. */ + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public void sendTextMessage( String destinationAddress, String scAddress, String text, PendingIntent sentIntent, PendingIntent deliveryIntent) { @@ -581,8 +584,11 @@ public final class SmsManager { * Used for logging and diagnostics purposes. The id may be 0. * * @throws IllegalArgumentException if destinationAddress or text are empty + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. * */ + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public void sendTextMessage( @NonNull String destinationAddress, @Nullable String scAddress, @NonNull String text, @Nullable PendingIntent sentIntent, @Nullable PendingIntent deliveryIntent, @@ -788,12 +794,16 @@ public final class SmsManager { * </p> * * @see #sendTextMessage(String, String, String, PendingIntent, PendingIntent) + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(allOf = { android.Manifest.permission.MODIFY_PHONE_STATE, android.Manifest.permission.SEND_SMS }) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public void sendTextMessageWithoutPersisting( String destinationAddress, String scAddress, String text, PendingIntent sentIntent, PendingIntent deliveryIntent) { @@ -908,7 +918,10 @@ public final class SmsManager { * {@link #RESULT_REMOTE_EXCEPTION} for error. * * @throws IllegalArgumentException if the format is invalid. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. */ + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public void injectSmsPdu( byte[] pdu, @SmsMessage.Format String format, PendingIntent receivedIntent) { if (!format.equals(SmsMessage.FORMAT_3GPP) && !format.equals(SmsMessage.FORMAT_3GPP2)) { @@ -940,6 +953,7 @@ public final class SmsManager { * @return an <code>ArrayList</code> of strings that, in order, comprise the original message. * @throws IllegalArgumentException if text is null. */ + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public ArrayList<String> divideMessage(String text) { if (null == text) { throw new IllegalArgumentException("text is null"); @@ -1046,7 +1060,10 @@ public final class SmsManager { * extended data ("pdu"). * * @throws IllegalArgumentException if destinationAddress or data are empty + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. */ + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public void sendMultipartTextMessage( String destinationAddress, String scAddress, ArrayList<String> parts, ArrayList<PendingIntent> sentIntents, ArrayList<PendingIntent> deliveryIntents) { @@ -1062,8 +1079,10 @@ public final class SmsManager { * Used for logging and diagnostics purposes. The id may be 0. * * @throws IllegalArgumentException if destinationAddress or data are empty - * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. */ + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public void sendMultipartTextMessage( @NonNull String destinationAddress, @Nullable String scAddress, @NonNull List<String> parts, @Nullable List<PendingIntent> sentIntents, @@ -1089,7 +1108,11 @@ public final class SmsManager { * * @param packageName serves as the default package name if the package name that is * associated with the user id is null. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. */ + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public void sendMultipartTextMessage( @NonNull String destinationAddress, @Nullable String scAddress, @NonNull List<String> parts, @Nullable List<PendingIntent> sentIntents, @@ -1191,10 +1214,14 @@ public final class SmsManager { * </p> * * @see #sendMultipartTextMessage(String, String, ArrayList, ArrayList, ArrayList) + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. * @hide **/ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public void sendMultipartTextMessageWithoutPersisting( String destinationAddress, String scAddress, List<String> parts, List<PendingIntent> sentIntents, List<PendingIntent> deliveryIntents) { @@ -1498,7 +1525,10 @@ public final class SmsManager { * raw pdu of the status report is in the extended data ("pdu"). * * @throws IllegalArgumentException if destinationAddress or data are empty + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. */ + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public void sendDataMessage( String destinationAddress, String scAddress, short destinationPort, byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) { @@ -1609,6 +1639,7 @@ public final class SmsManager { * .{@link #createForSubscriptionId createForSubscriptionId(subId)} instead */ @Deprecated + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public static SmsManager getSmsManagerForSubscriptionId(int subId) { return getSmsManagerForContextAndSubscriptionId(null, subId); } @@ -1626,6 +1657,7 @@ public final class SmsManager { * @see SubscriptionManager#getActiveSubscriptionInfoList() * @see SubscriptionManager#getDefaultSmsSubscriptionId() */ + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public @NonNull SmsManager createForSubscriptionId(int subId) { return getSmsManagerForContextAndSubscriptionId(mContext, subId); } @@ -1651,7 +1683,11 @@ public final class SmsManager { * @return associated subscription ID or {@link SubscriptionManager#INVALID_SUBSCRIPTION_ID} if * the default subscription id cannot be determined or the device has multiple active * subscriptions and and no default is set ("ask every time") by the user. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. */ + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public int getSubscriptionId() { try { return (mSubId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) @@ -2018,10 +2054,14 @@ public final class SmsManager { * * @throws IllegalArgumentException if endMessageId < startMessageId * @deprecated Use {@link TelephonyManager#setCellBroadcastIdRanges} instead. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. * {@hide} */ @Deprecated @SystemApi + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public boolean enableCellBroadcastRange(int startMessageId, int endMessageId, @android.telephony.SmsCbMessage.MessageFormat int ranType) { boolean success = false; @@ -2079,11 +2119,15 @@ public final class SmsManager { * @see #enableCellBroadcastRange(int, int, int) * * @throws IllegalArgumentException if endMessageId < startMessageId + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. + * * @deprecated Use {@link TelephonyManager#setCellBroadcastIdRanges} instead. * {@hide} */ @Deprecated @SystemApi + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public boolean disableCellBroadcastRange(int startMessageId, int endMessageId, @android.telephony.SmsCbMessage.MessageFormat int ranType) { boolean success = false; @@ -2223,7 +2267,11 @@ public final class SmsManager { * @return the user-defined default SMS subscription id, or the active subscription id if * there's only one active subscription available, otherwise * {@link SubscriptionManager#INVALID_SUBSCRIPTION_ID}. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. */ + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public static int getDefaultSmsSubscriptionId() { try { return getISmsService().getPreferredSmsSubscription(); @@ -2271,10 +2319,14 @@ public final class SmsManager { * </p> * * @return the total number of SMS records which can be stored on the SIM card. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. */ @RequiresPermission(anyOf = {android.Manifest.permission.READ_PHONE_STATE, android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE}) @IntRange(from = 0) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public int getSmsCapacityOnIcc() { int ret = 0; try { @@ -2819,7 +2871,10 @@ public final class SmsManager { * <code>MMS_ERROR_DATA_DISABLED</code><br> * <code>MMS_ERROR_MMS_DISABLED_BY_CARRIER</code><br> * @throws IllegalArgumentException if contentUri is empty + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. */ + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public void sendMultimediaMessage(Context context, Uri contentUri, String locationUrl, Bundle configOverrides, PendingIntent sentIntent) { sendMultimediaMessage(context, contentUri, locationUrl, configOverrides, sentIntent, @@ -2863,7 +2918,10 @@ public final class SmsManager { * @param messageId an id that uniquely identifies the message requested to be sent. * Used for logging and diagnostics purposes. The id may be 0. * @throws IllegalArgumentException if contentUri is empty + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. */ + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public void sendMultimediaMessage(@NonNull Context context, @NonNull Uri contentUri, @Nullable String locationUrl, @SuppressWarnings("NullableCollection") @Nullable Bundle configOverrides, @@ -2922,7 +2980,10 @@ public final class SmsManager { * <code>MMS_ERROR_DATA_DISABLED</code><br> * <code>MMS_ERROR_MMS_DISABLED_BY_CARRIER</code><br> * @throws IllegalArgumentException if locationUrl or contentUri is empty + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. */ + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public void downloadMultimediaMessage(Context context, String locationUrl, Uri contentUri, Bundle configOverrides, PendingIntent downloadedIntent) { downloadMultimediaMessage(context, locationUrl, contentUri, configOverrides, @@ -2968,7 +3029,10 @@ public final class SmsManager { * @param messageId an id that uniquely identifies the message requested to be downloaded. * Used for logging and diagnostics purposes. The id may be 0. * @throws IllegalArgumentException if locationUrl or contentUri is empty + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. */ + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public void downloadMultimediaMessage(@NonNull Context context, @NonNull String locationUrl, @NonNull Uri contentUri, @SuppressWarnings("NullableCollection") @Nullable Bundle configOverrides, @@ -3079,7 +3143,11 @@ public final class SmsManager { * * @return the bundle key/values pairs that contains MMS configuration values * or an empty Bundle if they cannot be found. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. */ + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) @NonNull public Bundle getCarrierConfigValues() { try { ISms iSms = getISmsService(); @@ -3115,7 +3183,11 @@ public final class SmsManager { * * @return Token to include in an SMS message. The token will be 11 characters long. * @see android.provider.Telephony.Sms.Intents#getMessagesFromIntent + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. */ + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public String createAppSpecificSmsToken(PendingIntent intent) { try { ISms iccSms = getISmsServiceOrThrow(); @@ -3233,7 +3305,11 @@ public final class SmsManager { * message. * @param intent this intent is sent when the matching SMS message is received. * @return Token to include in an SMS message. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. */ + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) @Nullable public String createAppSpecificSmsTokenWithPackageInfo( @Nullable String prefixes, @NonNull PendingIntent intent) { @@ -3393,9 +3469,13 @@ public final class SmsManager { * </p> * * @return the SMSC address string, null if failed. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. */ @SuppressAutoDoc // for carrier privileges and default SMS application. @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) @Nullable public String getSmscAddress() { String smsc = null; @@ -3430,9 +3510,13 @@ public final class SmsManager { * * @param smsc the SMSC address string. * @return true for success, false otherwise. Failure can be due modem returning an error. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. */ @SuppressAutoDoc // for carrier privileges and default SMS application. @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public boolean setSmscAddress(@NonNull String smsc) { try { ISms iSms = getISmsService(); @@ -3455,10 +3539,14 @@ public final class SmsManager { * {@link SmsManager#PREMIUM_SMS_CONSENT_ASK_USER}, * {@link SmsManager#PREMIUM_SMS_CONSENT_NEVER_ALLOW}, or * {@link SmsManager#PREMIUM_SMS_CONSENT_ALWAYS_ALLOW} + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public @PremiumSmsConsent int getPremiumSmsConsent(@NonNull String packageName) { int permission = 0; try { @@ -3479,10 +3567,14 @@ public final class SmsManager { * @param permission one of {@link SmsManager#PREMIUM_SMS_CONSENT_ASK_USER}, * {@link SmsManager#PREMIUM_SMS_CONSENT_NEVER_ALLOW}, or * {@link SmsManager#PREMIUM_SMS_CONSENT_ALWAYS_ALLOW} + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public void setPremiumSmsConsent( @NonNull String packageName, @PremiumSmsConsent int permission) { try { @@ -3498,11 +3590,15 @@ public final class SmsManager { /** * Reset all cell broadcast ranges. Previously enabled ranges will become invalid after this. * @deprecated Use {@link TelephonyManager#setCellBroadcastIdRanges} with empty list instead + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. * @hide */ @Deprecated @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_CELL_BROADCASTS) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public void resetAllCellBroadcastRanges() { try { ISms iSms = getISmsService(); @@ -3530,6 +3626,8 @@ public final class SmsManager { * available. * @throws SecurityException if the caller does not have the required permission/privileges. * @throws IllegalStateException in case of telephony service is not available. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @NonNull diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index 6c8663a8eb14..56156024fbab 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -1757,6 +1757,9 @@ public class SubscriptionManager { * * @param subId The unique SubscriptionInfo key in database. * @return SubscriptionInfo, maybe null if its not active. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) @@ -1790,6 +1793,8 @@ public class SubscriptionManager { * @param iccId the IccId of SIM card * @return SubscriptionInfo, maybe null if its not active * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @@ -1826,6 +1831,9 @@ public class SubscriptionManager { * * @param slotIndex the slot which the subscription is inserted * @return SubscriptionInfo, maybe null if its not active + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) @@ -1870,6 +1878,8 @@ public class SubscriptionManager { * {@link SubscriptionInfo#getSubscriptionId()}. * * @throws SecurityException if callers do not hold the required permission. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @NonNull @RequiresPermission(anyOf = { @@ -1929,6 +1939,9 @@ public class SubscriptionManager { * then by {@link SubscriptionInfo#getSubscriptionId}. * </li> * </ul> + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) // @RequiresPermission(TODO(b/308809058)) @@ -1972,6 +1985,8 @@ public class SubscriptionManager { * This is similar to {@link #getActiveSubscriptionInfoList} except that it will return * both active and hidden SubscriptionInfos. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ public @NonNull List<SubscriptionInfo> getCompleteActiveSubscriptionInfoList() { List<SubscriptionInfo> completeList = getActiveSubscriptionInfoList( @@ -2056,6 +2071,9 @@ public class SubscriptionManager { * <p> * Permissions android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE is required * for #getAvailableSubscriptionInfoList to be invoked. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -2097,6 +2115,9 @@ public class SubscriptionManager { * if the list is non-empty the list is sorted by {@link SubscriptionInfo#getSimSlotIndex} * then by {@link SubscriptionInfo#getSubscriptionId}. * </ul> + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public List<SubscriptionInfo> getAccessibleSubscriptionInfoList() { List<SubscriptionInfo> result = null; @@ -2125,6 +2146,8 @@ public class SubscriptionManager { * * @see TelephonyManager#getCardIdForDefaultEuicc() for more information on the card ID. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. * @hide */ @SystemApi @@ -2155,6 +2178,8 @@ public class SubscriptionManager { * * @see TelephonyManager#getCardIdForDefaultEuicc() for more information on the card ID. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. * @hide */ @SystemApi @@ -2177,6 +2202,9 @@ public class SubscriptionManager { * @return The current number of active subscriptions. * * @see #getActiveSubscriptionInfoList() + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) // @RequiresPermission(TODO(b/308809058)) @@ -2247,6 +2275,9 @@ public class SubscriptionManager { * @param slotIndex the slot assigned to this subscription. It is ignored for subscriptionType * of {@link #SUBSCRIPTION_TYPE_REMOTE_SIM}. * @param subscriptionType the {@link #SUBSCRIPTION_TYPE} + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) @@ -2289,6 +2320,8 @@ public class SubscriptionManager { * @throws NullPointerException if {@code uniqueId} is {@code null}. * @throws SecurityException if callers do not hold the required permission. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) @@ -2435,6 +2468,7 @@ public class SubscriptionManager { * @deprecated Use {@link #getSubscriptionId(int)} instead. * @hide */ + @Deprecated public static int[] getSubId(int slotIndex) { if (!isValidSlotIndex(slotIndex)) { return null; @@ -2489,6 +2523,9 @@ public class SubscriptionManager { * On a data only device or on error, will return INVALID_SUBSCRIPTION_ID. * * @return the default voice subscription Id. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ public static int getDefaultVoiceSubscriptionId() { int subId = INVALID_SUBSCRIPTION_ID; @@ -2516,6 +2553,9 @@ public class SubscriptionManager { * * @param subscriptionId A valid subscription ID to set as the system default, or * {@link SubscriptionManager#INVALID_SUBSCRIPTION_ID} + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -2535,6 +2575,9 @@ public class SubscriptionManager { /** * Same as {@link #setDefaultVoiceSubscriptionId(int)}, but preserved for backwards * compatibility. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ public void setDefaultVoiceSubId(int subId) { @@ -2578,6 +2621,8 @@ public class SubscriptionManager { * * @param subscriptionId the supplied subscription ID * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -2612,6 +2657,8 @@ public class SubscriptionManager { * * @param subscriptionId the supplied subscription ID * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -2634,6 +2681,9 @@ public class SubscriptionManager { * Will return null on voice only devices, or on error. * * @return the SubscriptionInfo for the default data subscription. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @UnsupportedAppUsage @@ -2720,6 +2770,9 @@ public class SubscriptionManager { * * @return the list of subId's that are active, * is never null but the length may be 0. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -2738,6 +2791,9 @@ public class SubscriptionManager { * * @return the list of subId's that are active, * is never null but the length may be 0. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -2987,6 +3043,9 @@ public class SubscriptionManager { * @param context Context object * @param subId Subscription Id of Subscription whose resources are required * @return Resources associated with Subscription. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @NonNull @@ -3069,6 +3128,9 @@ public class SubscriptionManager { * @return {@code true} if the supplied subscription ID corresponds to an active subscription; * {@code false} if it does not correspond to an active subscription; or throw a * SecurityException if the caller hasn't got the right permission. + *i + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public boolean isActiveSubscriptionId(int subscriptionId) { @@ -3377,6 +3439,8 @@ public class SubscriptionManager { * * @throws IllegalStateException when subscription manager service is not available. * @throws SecurityException when clients do not have MODIFY_PHONE_STATE permission. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -3454,6 +3518,9 @@ public class SubscriptionManager { * {@link TelephonyManager#hasCarrierPrivileges}). * * @return the list of opportunistic subscription info. If none exists, an empty list. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) @@ -3489,8 +3556,12 @@ public class SubscriptionManager { * PendingIntent)} and does not support Multiple Enabled Profile(MEP). Apps should use * {@link EuiccManager#switchToSubscription(int, PendingIntent)} or * {@link EuiccManager#switchToSubscription(int, int, PendingIntent)} instead. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_EUICC) @Deprecated public void switchToSubscription(int subId, @NonNull PendingIntent callbackIntent) { Preconditions.checkNotNull(callbackIntent, "callbackIntent cannot be null"); @@ -3518,6 +3589,9 @@ public class SubscriptionManager { * @param opportunistic whether it’s opportunistic subscription. * @param subId the unique SubscriptionInfo index in database * @return {@code true} if the operation is succeed, {@code false} otherwise. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @@ -3554,6 +3628,8 @@ public class SubscriptionManager { * outlined above. * @throws IllegalArgumentException if any of the subscriptions in the list doesn't exist. * @throws IllegalStateException if Telephony service is in bad state. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * * @param subIdList list of subId that will be in the same group * @return groupUUID a UUID assigned to the subscription group. @@ -3598,6 +3674,8 @@ public class SubscriptionManager { * outlined above. * @throws IllegalArgumentException if the some subscriptions in the list doesn't exist. * @throws IllegalStateException if Telephony service is in bad state. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * * @param subIdList list of subId that need adding into the group * @param groupUuid the groupUuid the subscriptions are being added to. @@ -3647,6 +3725,8 @@ public class SubscriptionManager { * @throws IllegalArgumentException if the some subscriptions in the list doesn't belong the * specified group. * @throws IllegalStateException if Telephony service is in bad state. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * * @see #createSubscriptionGroup(List) */ @@ -3696,6 +3776,8 @@ public class SubscriptionManager { * @throws IllegalStateException if Telephony service is in bad state. * @throws SecurityException if the caller doesn't meet the requirements * outlined above. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * * @param groupUuid of which list of subInfo will be returned. * @return list of subscriptionInfo that belong to the same group, including the given @@ -3785,9 +3867,9 @@ public class SubscriptionManager { Map<ParcelUuid, SubscriptionInfo> groupMap = new HashMap<>(); for (SubscriptionInfo info : availableList) { - // Opportunistic subscriptions are considered invisible + // Grouped opportunistic subscriptions are considered invisible // to users so they should never be returned. - if (!isSubscriptionVisible(info)) continue; + if (info.getGroupUuid() != null && info.isOpportunistic()) continue; ParcelUuid groupUuid = info.getGroupUuid(); if (groupUuid == null) { @@ -3817,6 +3899,8 @@ public class SubscriptionManager { * * @return whether the operation is successful. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -3844,6 +3928,9 @@ public class SubscriptionManager { * * @param subscriptionId which subscription to operate on. * @param enabled whether uicc applications are enabled or disabled. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -3872,6 +3959,8 @@ public class SubscriptionManager { * * @return whether can disable subscriptions on physical SIMs. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -3897,6 +3986,8 @@ public class SubscriptionManager { * * @param subscriptionId The subscription id. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -3923,6 +4014,8 @@ public class SubscriptionManager { * @param sharing The status sharing preference. * * @throws SecurityException if the caller doesn't have permissions required. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) public void setDeviceToDeviceStatusSharingPreference(int subscriptionId, @@ -3941,6 +4034,8 @@ public class SubscriptionManager { * @return The device to device status sharing preference * * @throws SecurityException if the caller doesn't have permissions required. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ public @DeviceToDeviceStatusSharingPreference int getDeviceToDeviceStatusSharingPreference( int subscriptionId) { @@ -3960,6 +4055,8 @@ public class SubscriptionManager { * @param contacts The list of contacts that allow device to device status sharing. * * @throws SecurityException if the caller doesn't have permissions required. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) public void setDeviceToDeviceStatusSharingContacts(int subscriptionId, @@ -3980,6 +4077,9 @@ public class SubscriptionManager { * @param subscriptionId Subscription id. * * @return The list of contacts that allow device to device status sharing. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ public @NonNull List<Uri> getDeviceToDeviceStatusSharingContacts(int subscriptionId) { String result = getStringSubscriptionProperty(mContext, subscriptionId, @@ -4012,6 +4112,8 @@ public class SubscriptionManager { * * @throws IllegalArgumentException if the provided slot index is invalid. * @throws SecurityException if callers do not hold the required permission. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * * @hide */ @@ -4152,6 +4254,8 @@ public class SubscriptionManager { * * @param data with the sim specific configs to be backed up. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -4206,6 +4310,8 @@ public class SubscriptionManager { * @throws IllegalArgumentException if {@code source} is invalid. * @throws IllegalStateException if the telephony process is not currently available. * @throws SecurityException if the caller doesn't have permissions required. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * * @see #PHONE_NUMBER_SOURCE_UICC * @see #PHONE_NUMBER_SOURCE_CARRIER @@ -4266,6 +4372,8 @@ public class SubscriptionManager { * * @throws IllegalStateException if the telephony process is not currently available. * @throws SecurityException if the caller doesn't have permissions required. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * * @see #getPhoneNumber(int, int) */ @@ -4309,6 +4417,8 @@ public class SubscriptionManager { * @throws IllegalStateException if the telephony process is not currently available. * @throws NullPointerException if {@code number} is {@code null}. * @throws SecurityException if the caller doesn't have permissions required. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresPermission("carrier privileges") public void setCarrierPhoneNumber(int subscriptionId, @NonNull String number) { diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 9e292be7f767..1b47dfe0eba1 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -510,7 +510,7 @@ public class TelephonyManager { /** @hide */ @UnsupportedAppUsage public TelephonyManager(Context context) { - this(context, SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); + this(context, SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); } /** @hide */ @@ -2140,10 +2140,14 @@ public class TelephonyManager { * the IMEI/SV for GSM phones. Return null if the software version is * not available. * <p> + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY}. */ @RequiresPermission(anyOf = { android.Manifest.permission.READ_PHONE_STATE, android.Manifest.permission.READ_BASIC_PHONE_STATE}) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY) @Nullable public String getDeviceSoftwareVersion() { return getDeviceSoftwareVersion(getSlotIndex()); @@ -2158,10 +2162,13 @@ public class TelephonyManager { * * @param slotIndex of which deviceID is returned * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY}. * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY) @Nullable public String getDeviceSoftwareVersion(int slotIndex) { ITelephony telephony = getITelephony(); @@ -2288,6 +2295,9 @@ public class TelephonyManager { * * See {@link #getImei(int)} for details on the required permissions and behavior * when the caller does not hold sufficient permissions. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_GSM}. */ @SuppressAutoDoc // No support for device / profile owner or carrier privileges (b/72967236). @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @@ -2330,6 +2340,9 @@ public class TelephonyManager { * </ul> * * @param slotIndex of which IMEI is returned + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_GSM}. */ @SuppressAutoDoc // No support for device / profile owner or carrier privileges (b/72967236). @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @@ -2350,6 +2363,9 @@ public class TelephonyManager { /** * Returns the Type Allocation Code from the IMEI. Return null if Type Allocation Code is not * available. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_GSM}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_GSM) @Nullable @@ -2362,6 +2378,9 @@ public class TelephonyManager { * available. * * @param slotIndex of which Type Allocation Code is returned + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_GSM}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_GSM) @Nullable @@ -2407,6 +2426,9 @@ public class TelephonyManager { * the READ_PHONE_STATE permission, or if the calling app is targeting API level 29 or * higher, then a SecurityException is thrown.</li> * </ul> + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CDMA}. */ @SuppressAutoDoc // No support for device / profile owner or carrier privileges (b/72967236). @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @@ -2446,6 +2468,9 @@ public class TelephonyManager { * </ul> * * @param slotIndex of which MEID is returned + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CDMA}. */ @SuppressAutoDoc // No support for device / profile owner or carrier privileges (b/72967236). @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @@ -2472,6 +2497,9 @@ public class TelephonyManager { /** * Returns the Manufacturer Code from the MEID. Return null if Manufacturer Code is not * available. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CDMA}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CDMA) @Nullable @@ -2484,6 +2512,9 @@ public class TelephonyManager { * available. * * @param slotIndex of which Type Allocation Code is returned + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CDMA}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CDMA) @Nullable @@ -2528,6 +2559,9 @@ public class TelephonyManager { * the READ_PHONE_STATE permission, or if the calling app is targeting API level 29 or * higher, then a SecurityException is thrown.</li> * </ul> + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @SuppressAutoDoc // No support for device / profile owner or carrier privileges (b/72967236). @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @@ -2563,10 +2597,14 @@ public class TelephonyManager { *<p> * @return Current location of the device or null if not available. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. + * * @deprecated use {@link #getAllCellInfo} instead, which returns a superset of this API. */ @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) public CellLocation getCellLocation() { try { ITelephony telephony = getITelephony(); @@ -2596,12 +2634,15 @@ public class TelephonyManager { * * @return List of NeighboringCellInfo or null if info unavailable. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @removed * @deprecated Use {@link #getAllCellInfo} which returns a superset of the information * from NeighboringCellInfo, including LTE cell information. */ @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) public List<NeighboringCellInfo> getNeighboringCellInfo() { try { ITelephony telephony = getITelephony(); @@ -2648,9 +2689,12 @@ public class TelephonyManager { * @see #PHONE_TYPE_CDMA * @see #PHONE_TYPE_SIP * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY}. * {@hide} */ @SystemApi + @RequiresFeature(PackageManager.FEATURE_TELEPHONY) public int getCurrentPhoneType() { return getCurrentPhoneType(getSubId()); } @@ -2663,9 +2707,13 @@ public class TelephonyManager { * @see #PHONE_TYPE_CDMA * * @param subId for which phone type is returned + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY}. * @hide */ @SystemApi + @RequiresFeature(PackageManager.FEATURE_TELEPHONY) public int getCurrentPhoneType(int subId) { int phoneId; if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { @@ -2712,7 +2760,11 @@ public class TelephonyManager { * @see #PHONE_TYPE_GSM * @see #PHONE_TYPE_CDMA * @see #PHONE_TYPE_SIP + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY}. */ + @RequiresFeature(PackageManager.FEATURE_TELEPHONY) public int getPhoneType() { if (!isVoiceCapable()) { return PHONE_TYPE_NONE; @@ -2912,6 +2964,9 @@ public class TelephonyManager { * @see CarrierConfigManager#getConfigForSubId(int) * @see #createForSubscriptionId(int) * @see #createForPhoneAccountHandle(PhoneAccountHandle) + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @WorkerThread @@ -2958,6 +3013,9 @@ public class TelephonyManager { * <p> * @return the lowercase 2 character ISO-3166-1 alpha-2 country code, or empty string if not * available. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) public String getNetworkCountryIso() { @@ -2980,6 +3038,8 @@ public class TelephonyManager { * available. * * @throws IllegalArgumentException when the slotIndex is invalid. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) @@ -3108,9 +3168,13 @@ public class TelephonyManager { * * @deprecated use {@link #getDataNetworkType()} * @return the NETWORK_TYPE_xxxx for current data connection. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @Deprecated @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) public @NetworkType int getNetworkType() { return getNetworkType(getSubId(SubscriptionManager.getActiveDataSubscriptionId())); } @@ -3199,12 +3263,15 @@ public class TelephonyManager { * @see #NETWORK_TYPE_EHRPD * @see #NETWORK_TYPE_HSPAP * @see #NETWORK_TYPE_NR + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(anyOf = { android.Manifest.permission.READ_PHONE_STATE, android.Manifest.permission.READ_BASIC_PHONE_STATE}) - @RequiresFeature(PackageManager.FEATURE_TELEPHONY_DATA) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) public @NetworkType int getDataNetworkType() { return getDataNetworkType(getSubId(SubscriptionManager.getActiveDataSubscriptionId())); } @@ -3245,6 +3312,9 @@ public class TelephonyManager { * or {@link android.Manifest.permission#READ_BASIC_PHONE_STATE * READ_BASIC_PHONE_STATE} or that the calling app has carrier privileges * (see {@link #hasCarrierPrivileges}). + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(anyOf = { @@ -3597,6 +3667,9 @@ public class TelephonyManager { * of whether an active SIM profile is present or not so this API would always return true. * * @return true if a ICC card is present. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public boolean hasIccCard() { @@ -3640,6 +3713,9 @@ public class TelephonyManager { * @see #SIM_STATE_PERM_DISABLED * @see #SIM_STATE_CARD_IO_ERROR * @see #SIM_STATE_CARD_RESTRICTED + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public @SimState int getSimState() { @@ -3681,6 +3757,8 @@ public class TelephonyManager { * @see #SIM_STATE_CARD_RESTRICTED * @see #SIM_STATE_PRESENT * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -3701,11 +3779,14 @@ public class TelephonyManager { * @see #SIM_STATE_CARD_RESTRICTED * @see #SIM_STATE_PRESENT * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide * @deprecated instead use {@link #getSimCardState(int, int)} */ @SystemApi @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) @Deprecated public @SimState int getSimCardState(int physicalSlotIndex) { int activePort = getFirstActivePortIndex(physicalSlotIndex); @@ -3727,6 +3808,8 @@ public class TelephonyManager { * @see #SIM_STATE_CARD_RESTRICTED * @see #SIM_STATE_PRESENT * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -3785,6 +3868,8 @@ public class TelephonyManager { * @see #SIM_STATE_PERM_DISABLED * @see #SIM_STATE_LOADED * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -3808,11 +3893,14 @@ public class TelephonyManager { * @see #SIM_STATE_PERM_DISABLED * @see #SIM_STATE_LOADED * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide * @deprecated instead use {@link #getSimApplicationState(int, int)} */ @SystemApi @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) @Deprecated public @SimState int getSimApplicationState(int physicalSlotIndex) { int activePort = getFirstActivePortIndex(physicalSlotIndex); @@ -3836,6 +3924,8 @@ public class TelephonyManager { * @see #SIM_STATE_PERM_DISABLED * @see #SIM_STATE_LOADED * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -3876,6 +3966,9 @@ public class TelephonyManager { * * @param appType the uicc app type like {@link APPTYPE_CSIM} * @return true if the specified type of application in UICC CARD or false if no uicc or error. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -3908,6 +4001,9 @@ public class TelephonyManager { * @see #SIM_STATE_PERM_DISABLED * @see #SIM_STATE_CARD_IO_ERROR * @see #SIM_STATE_CARD_RESTRICTED + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public @SimState int getSimState(int slotIndex) { @@ -4105,6 +4201,9 @@ public class TelephonyManager { * the READ_PHONE_STATE permission, or if the calling app is targeting API level 29 or * higher, then a SecurityException is thrown.</li> * </ul> + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @SuppressAutoDoc // No support for device / profile owner or carrier privileges (b/72967236). @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @@ -4172,6 +4271,9 @@ public class TelephonyManager { * * @return {@code true} if 3GPP and 3GPP2 radio technologies can be supported at the same time * {@code false} if not supported or unknown + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide */ @SystemApi @@ -4219,6 +4321,9 @@ public class TelephonyManager { * through a factory reset. * * @return card ID of the default eUICC card, if loaded. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_EUICC) public int getCardIdForDefaultEuicc() { @@ -4252,6 +4357,9 @@ public class TelephonyManager { * @return a list of UiccCardInfo objects, representing information on the currently inserted * UICCs and eUICCs. Each UiccCardInfo in the list will have private information filtered out if * the caller does not have adequate permissions for that card. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) @@ -4276,6 +4384,8 @@ public class TelephonyManager { * * @return UiccSlotInfo array. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -4319,6 +4429,9 @@ public class TelephonyManager { * @param physicalSlots The content of the array represents the physical slot index. The array * size should be same as {@link #getUiccSlotsInfo()}. * @return boolean Return true if the switch succeeds, false if the switch fails. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide * @deprecated {@link #setSimSlotMapping(Collection, Executor, Consumer)} */ @@ -4328,6 +4441,7 @@ public class TelephonyManager { @SystemApi @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public boolean switchSlots(int[] physicalSlots) { try { ITelephony telephony = getITelephony(); @@ -4420,6 +4534,8 @@ public class TelephonyManager { * @throws IllegalArgumentException if the caller passes in an invalid collection of * UiccSlotMapping like duplicate data, etc * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -4452,11 +4568,14 @@ public class TelephonyManager { * @return a map indicates the mapping from logical slots to physical slots. The size of the map * should be {@link #getPhoneCount()} if success, otherwise return an empty map. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide * @deprecated use {@link #getSimSlotMapping()} instead. */ @SystemApi @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) @NonNull @Deprecated public Map<Integer, Integer> getLogicalToPhysicalSlotMapping() { @@ -4484,6 +4603,9 @@ public class TelephonyManager { * * @return a collection of {@link UiccSlotMapping} which indicates the mapping from logical * slots to ports and physical slots. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -4541,6 +4663,9 @@ public class TelephonyManager { * the READ_PHONE_STATE permission, or if the calling app is targeting API level 29 or * higher, then a SecurityException is thrown.</li> * </ul> + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @SuppressAutoDoc // No support for device / profile owner or carrier privileges (b/72967236). @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @@ -4593,6 +4718,8 @@ public class TelephonyManager { * found, and the carrier does not require a key. * @throws IllegalArgumentException when an invalid key is found or when key is required but * not found. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @@ -4638,6 +4765,9 @@ public class TelephonyManager { * Requires Permission: MODIFY_PHONE_STATE. * * @see #getCarrierInfoForImsiEncryption + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @@ -4840,6 +4970,9 @@ public class TelephonyManager { * from disk, as well as on which {@code callback} will be called. * @param callback A callback called when the upload operation terminates, either in success * or in error. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) public void uploadCallComposerPicture(@NonNull Path pictureToUpload, @@ -4947,6 +5080,9 @@ public class TelephonyManager { * read, as well as on which the callback will be called. * @param callback A callback called when the upload operation terminates, either in success * or in error. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) public void uploadCallComposerPicture(@NonNull InputStream pictureToUpload, @@ -5081,6 +5217,9 @@ public class TelephonyManager { * * <p>Requires Permission: {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE} * or that the calling app has carrier privileges (see {@link #hasCarrierPrivileges}). + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) @@ -5139,6 +5278,8 @@ public class TelephonyManager { * {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE} * for apps targeting SDK API level 29 and below. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @deprecated use {@link SubscriptionManager#getPhoneNumber(int)} instead. */ @Deprecated @@ -5148,6 +5289,7 @@ public class TelephonyManager { android.Manifest.permission.READ_SMS, android.Manifest.permission.READ_PHONE_NUMBERS }) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public String getLine1Number() { return getLine1Number(getSubId()); } @@ -5214,9 +5356,13 @@ public class TelephonyManager { * @param alphaTag alpha-tagging of the dailing nubmer * @param number The dialing number * @return true if the operation was executed correctly. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @deprecated use {@link SubscriptionManager#setCarrierPhoneNumber(int, String)} instead. */ @Deprecated + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public boolean setLine1NumberForDisplay(String alphaTag, String number) { return setLine1NumberForDisplay(getSubId(), alphaTag, number); } @@ -5336,6 +5482,8 @@ public class TelephonyManager { * {@link SubscriptionManager#createSubscriptionGroup(List)} for the definition of a group, * otherwise return an empty array if there is a failure. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -5421,6 +5569,9 @@ public class TelephonyManager { * * <p>Requires Permission: {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE} * or that the calling app has carrier privileges (see {@link #hasCarrierPrivileges}). + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) @@ -5459,6 +5610,9 @@ public class TelephonyManager { * * @param alphaTag The alpha tag to display. * @param number The voicemail number. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) public boolean setVoiceMailNumber(String alphaTag, String number) { @@ -5533,6 +5687,8 @@ public class TelephonyManager { * @see #KEY_VISUAL_VOICEMAIL_ENABLED_BY_USER_BOOL * @see #KEY_VOICEMAIL_SCRAMBLED_PIN_STRING * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. * @hide */ @SystemApi @@ -5563,6 +5719,9 @@ public class TelephonyManager { * @see #createForSubscriptionId(int) * @see #createForPhoneAccountHandle(PhoneAccountHandle) * @see VisualVoicemailService + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. */ @Nullable @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @@ -5593,6 +5752,9 @@ public class TelephonyManager { * * @see TelecomManager#getDefaultDialerPackage() * @see CarrierConfigManager#KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) public void setVisualVoicemailSmsFilterSettings(VisualVoicemailSmsFilterSettings settings) { @@ -5623,6 +5785,9 @@ public class TelephonyManager { * * @see SmsManager#sendDataMessage(String, String, short, byte[], PendingIntent, PendingIntent) * @see SmsManager#sendTextMessage(String, String, String, PendingIntent, PendingIntent) + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) public void sendVisualVoicemailSms(String number, int port, String text, @@ -5808,6 +5973,9 @@ public class TelephonyManager { * @see #SIM_ACTIVATION_STATE_ACTIVATING * @see #SIM_ACTIVATION_STATE_ACTIVATED * @see #SIM_ACTIVATION_STATE_DEACTIVATED + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING} * @hide */ @SystemApi @@ -5856,6 +6024,9 @@ public class TelephonyManager { * @see #SIM_ACTIVATION_STATE_ACTIVATED * @see #SIM_ACTIVATION_STATE_DEACTIVATED * @see #SIM_ACTIVATION_STATE_RESTRICTED + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. * @hide */ @SystemApi @@ -5904,6 +6075,9 @@ public class TelephonyManager { * @see #SIM_ACTIVATION_STATE_ACTIVATING * @see #SIM_ACTIVATION_STATE_ACTIVATED * @see #SIM_ACTIVATION_STATE_DEACTIVATED + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. * @hide */ @SystemApi @@ -5954,6 +6128,9 @@ public class TelephonyManager { * @see #SIM_ACTIVATION_STATE_ACTIVATED * @see #SIM_ACTIVATION_STATE_DEACTIVATED * @see #SIM_ACTIVATION_STATE_RESTRICTED + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. * @hide */ @SystemApi @@ -6032,6 +6209,9 @@ public class TelephonyManager { * * <p>Requires Permission: {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE} * or that the calling app has carrier privileges (see {@link #hasCarrierPrivileges}). + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) @@ -6072,7 +6252,10 @@ public class TelephonyManager { * * @throws SecurityException if the caller does not have carrier privileges or is not the * current default dialer + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. */ + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) public void sendDialerSpecialCode(String inputCode) { try { final ITelephony telephony = getITelephony(); @@ -6143,6 +6326,9 @@ public class TelephonyManager { * * <p>Requires Permission: * {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE READ_PRIVILEGED_PHONE_STATE} + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @Nullable @@ -6168,6 +6354,9 @@ public class TelephonyManager { * Returns the IMS public user identities (IMPU) that were loaded from the ISIM. * @return an array of IMPU strings, with one IMPU per string, or null if * not present or not loaded + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide * @deprecated use {@link #getImsPublicUserIdentities()} */ @@ -6199,6 +6388,8 @@ public class TelephonyManager { * EF_IMPU is not available. * @throws IllegalStateException in case the ISIM hasn’t been loaded * @throws SecurityException if the caller does not have the required permission/privilege + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @NonNull @@ -6254,6 +6445,10 @@ public class TelephonyManager { * targeting API level 31+. * * @return the current call state. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELECOM}. + * * @deprecated Use {@link #getCallStateForSubscription} to retrieve the call state for a * specific telephony subscription (which allows carrier privileged apps), * {@link TelephonyCallback.CallStateListener} for real-time call state updates, or @@ -6261,6 +6456,7 @@ public class TelephonyManager { * device. */ @RequiresPermission(value = android.Manifest.permission.READ_PHONE_STATE, conditional = true) + @RequiresFeature(PackageManager.FEATURE_TELECOM) @Deprecated public @CallState int getCallState() { if (mContext != null) { @@ -6281,6 +6477,9 @@ public class TelephonyManager { * @see TelephonyManager#createForSubscriptionId(int) * @see TelephonyManager#createForPhoneAccountHandle(PhoneAccountHandle) * @return The call state of the subscription associated with this TelephonyManager instance. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. */ @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) @@ -6341,6 +6540,9 @@ public class TelephonyManager { * @see #DATA_ACTIVITY_OUT * @see #DATA_ACTIVITY_INOUT * @see #DATA_ACTIVITY_DORMANT + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_DATA) public int getDataActivity() { @@ -6414,6 +6616,9 @@ public class TelephonyManager { * @see #DATA_SUSPENDED * @see #DATA_DISCONNECTING * @see #DATA_HANDOVER_IN_PROGRESS + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_DATA) public int getDataState() { @@ -6599,10 +6804,14 @@ public class TelephonyManager { * Returns the CDMA ERI icon display number. The number is assigned by * 3GPP2 C.R1001-H v1.0 Table 8.1-1. Additionally carriers define their own ERI display numbers. * Defined values are {@link #ERI_ON}, {@link #ERI_OFF}, and {@link #ERI_FLASH}. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CDMA}. * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CDMA) public @EriIconIndex int getCdmaEnhancedRoamingIndicatorDisplayNumber() { return getCdmaEriIconIndex(getSubId()); } @@ -6810,6 +7019,9 @@ public class TelephonyManager { * * @return List of {@link android.telephony.CellInfo}; null if cell * information is unavailable. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) @@ -6905,6 +7117,9 @@ public class TelephonyManager { * * @param executor the executor on which callback will be invoked. * @param callback a callback to receive CellInfo. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) @@ -6966,6 +7181,9 @@ public class TelephonyManager { * @param workSource the requestor to whom the power consumption for this should be attributed. * @param executor the executor on which callback will be invoked. * @param callback a callback to receive CellInfo. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide */ @SystemApi @@ -7052,6 +7270,9 @@ public class TelephonyManager { /** * Returns the MMS user agent. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public String getMmsUserAgent() { @@ -7068,6 +7289,9 @@ public class TelephonyManager { /** * Returns the MMS user agent profile URL. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public String getMmsUAProfUrl() { @@ -7111,8 +7335,12 @@ public class TelephonyManager { * * @param AID Application id. See ETSI 102.221 and 101.220. * @return an IccOpenLogicalChannelResponse object. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @deprecated Replaced by {@link #iccOpenLogicalChannel(String, int)} */ + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) @Deprecated public IccOpenLogicalChannelResponse iccOpenLogicalChannel(String AID) { return iccOpenLogicalChannel(getSubId(), AID, -1); @@ -7145,6 +7373,8 @@ public class TelephonyManager { * @param aid Application id. See ETSI 102.221 and 101.220. * @param p2 P2 parameter (described in ISO 7816-4). * @return an IccOpenLogicalChannelResponse object. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide * @deprecated This API is not compatible on eUICC supporting Multiple Enabled Profile(MEP), * instead use {@link #iccOpenLogicalChannelByPort(int, int, String, int)} @@ -7200,9 +7430,13 @@ public class TelephonyManager { * @param aid Application id. See ETSI 102.221 and 101.220. * @param p2 P2 parameter (described in ISO 7816-4). * @return an IccOpenLogicalChannelResponse object. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) @SystemApi @NonNull public IccOpenLogicalChannelResponse iccOpenLogicalChannelByPort(int slotIndex, @@ -7255,6 +7489,9 @@ public class TelephonyManager { * @param AID Application id. See ETSI 102.221 and 101.220. * @param p2 P2 parameter (described in ISO 7816-4). * @return an IccOpenLogicalChannelResponse object. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public IccOpenLogicalChannelResponse iccOpenLogicalChannel(String AID, int p2) { @@ -7321,6 +7558,9 @@ public class TelephonyManager { * @param channel is the channel id to be closed as returned by a successful * iccOpenLogicalChannel. * @return true if the channel was closed successfully. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide * @deprecated This API is not compatible on eUICC supporting Multiple Enabled Profile(MEP), * instead use {@link #iccCloseLogicalChannelByPort(int, int, int)} @@ -7365,9 +7605,12 @@ public class TelephonyManager { * @throws IllegalStateException if the Telephony process is not currently available or modem * currently can't process this command. * @throws IllegalArgumentException if invalid arguments are passed. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) @SystemApi public void iccCloseLogicalChannelByPort(int slotIndex, int portIndex, int channel) { try { @@ -7403,6 +7646,8 @@ public class TelephonyManager { * iccOpenLogicalChannel. * @return true if the channel was closed successfully. * @throws IllegalArgumentException if input parameters are wrong. e.g., invalid channel + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public boolean iccCloseLogicalChannel(int channel) { @@ -7469,6 +7714,8 @@ public class TelephonyManager { * @param data Data to be sent with the APDU. * @return The APDU response from the ICC card with the status appended at the end, or null if * there is an issue connecting to the Telephony service. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide * @deprecated This API is not compatible on eUICC supporting Multiple Enabled Profile(MEP), * instead use @@ -7516,9 +7763,13 @@ public class TelephonyManager { * @param data Data to be sent with the APDU. * @return The APDU response from the ICC card with the status appended at the end, or null if * there is an issue connecting to the Telephony service. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) @SystemApi @NonNull public String iccTransmitApduLogicalChannelByPort(int slotIndex, int portIndex, int channel, @@ -7563,6 +7814,9 @@ public class TelephonyManager { * @param data Data to be sent with the APDU. * @return The APDU response from the ICC card with the status appended at * the end. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public String iccTransmitApduLogicalChannel(int channel, int cla, @@ -7628,6 +7882,9 @@ public class TelephonyManager { * @param data Data to be sent with the APDU. * @return The APDU response from the ICC card with the status appended at * the end. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide * @deprecated This API is not compatible on eUICC supporting Multiple Enabled Profile(MEP), * instead use @@ -7673,9 +7930,13 @@ public class TelephonyManager { * @param data Data to be sent with the APDU. * @return The APDU response from the ICC card with the status appended at * the end. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) @SystemApi @NonNull public String iccTransmitApduBasicChannelByPort(int slotIndex, int portIndex, int cla, @@ -7712,6 +7973,9 @@ public class TelephonyManager { * @param data Data to be sent with the APDU. * @return The APDU response from the ICC card with the status appended at * the end. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public String iccTransmitApduBasicChannel(int cla, @@ -7768,6 +8032,9 @@ public class TelephonyManager { * @param p3 P3 value of the APDU command. * @param filePath * @return The APDU response. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public byte[] iccExchangeSimIO(int fileID, int command, int p1, int p2, int p3, @@ -7817,6 +8084,9 @@ public class TelephonyManager { * @return The APDU response from the ICC card in hexadecimal format * with the last 4 bytes being the status word. If the command fails, * returns an empty string. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public String sendEnvelopeWithStatus(String content) { @@ -7978,6 +8248,8 @@ public class TelephonyManager { * * @return {@code true} on success; {@code false} on any failure. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide */ @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) @@ -8008,6 +8280,8 @@ public class TelephonyManager { * * @deprecated Using {@link #rebootModem()} instead. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide */ @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) @@ -8035,6 +8309,8 @@ public class TelephonyManager { * app has carrier privileges (see {@link #hasCarrierPrivileges}). * @throws IllegalStateException if the Telephony process is not currently available. * @throws RuntimeException + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) @@ -8166,6 +8442,9 @@ public class TelephonyManager { * {@link #getMaxNumberVerificationTimeoutMillis()}, whichever is lesser. * @param executor The {@link Executor} that callbacks should be executed on. * @param callback The callback to use for delivering results. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. */ @SystemApi @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) @@ -8378,6 +8657,9 @@ public class TelephonyManager { * See 3GPP TS 31.103 (Section 4.2.7) for the definition and more information on this table. * * @return IMS Service Table or null if not present or not loaded + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @Nullable @@ -8496,6 +8778,9 @@ public class TelephonyManager { * Key freshness failure * Authentication error, no memory space available * Authentication error, no memory space available in EFMUK + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ // TODO(b/73660190): This should probably require MODIFY_PHONE_STATE, not // READ_PRIVILEGED_PHONE_STATE. It certainly shouldn't reference the permission in Javadoc since @@ -8552,6 +8837,9 @@ public class TelephonyManager { * or that the calling app has carrier privileges (see {@link #hasCarrierPrivileges}). * * @return an array of forbidden PLMNs or null if not available + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) @@ -8602,6 +8890,9 @@ public class TelephonyManager { * @return number of PLMNs that were successfully written to the SIM FPLMN list. * This may be less than the number of PLMNs passed in where the SIM file does not have enough * room for all of the values passed in. Return -1 in the event of an unexpected failure + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @@ -8637,6 +8928,8 @@ public class TelephonyManager { * @param appType of type int of either {@link #APPTYPE_USIM} or {@link #APPTYPE_ISIM}. * @return HexString represents sim service table else null. * @throws SecurityException if the caller does not have the required permission/privileges + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @@ -8673,6 +8966,9 @@ public class TelephonyManager { * state. * * @param slotIndex the sim slot to reset the IMS stack on. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @hide */ @SystemApi @WorkerThread @@ -9084,12 +9380,15 @@ public class TelephonyManager { * * @return The bitmask of preferred network types. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide * @deprecated Use {@link #getAllowedNetworkTypesBitmask} instead. */ @Deprecated @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @SystemApi + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) public @NetworkTypeBitMask long getPreferredNetworkTypeBitmask() { return getAllowedNetworkTypesBitmask(); } @@ -9108,6 +9407,8 @@ public class TelephonyManager { * * @return The bitmask of allowed network types. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide */ @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @@ -9133,10 +9434,14 @@ public class TelephonyManager { * or that the calling app has carrier privileges (see {@link #hasCarrierPrivileges}). * * @return the allowed network type bitmask + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide * @deprecated Use {@link #getAllowedNetworkTypesForReason} instead. */ @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) @SystemApi @Deprecated public @NetworkTypeBitMask long getAllowedNetworkTypes() { @@ -9161,6 +9466,9 @@ public class TelephonyManager { * <p>Requires Permission: * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE} or that the calling * app has carrier privileges (see {@link #hasCarrierPrivileges}). + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @@ -9247,6 +9555,9 @@ public class TelephonyManager { * tasks one at a time in serial order. * @param callback Returns network scan results or errors. * @return A NetworkScan obj which contains a callback which can be used to stop the scan. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(allOf = { @@ -9295,11 +9606,15 @@ public class TelephonyManager { * tasks one at a time in serial order. * @param callback Returns network scan results or errors. * @return A NetworkScan obj which contains a callback which can be used to stop the scan. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(allOf = { android.Manifest.permission.MODIFY_PHONE_STATE }) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) public @Nullable NetworkScan requestNetworkScan( @IncludeLocationData int includeLocationData, @NonNull NetworkScanRequest request, @@ -9317,6 +9632,9 @@ public class TelephonyManager { } /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. + * * @deprecated * Use {@link * #requestNetworkScan(NetworkScanRequest, Executor, TelephonyScanManager.NetworkScanCallback)} @@ -9327,6 +9645,7 @@ public class TelephonyManager { android.Manifest.permission.MODIFY_PHONE_STATE, Manifest.permission.ACCESS_FINE_LOCATION }) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) public NetworkScan requestNetworkScan( NetworkScanRequest request, TelephonyScanManager.NetworkScanCallback callback) { return requestNetworkScan(request, AsyncTask.SERIAL_EXECUTOR, callback); @@ -9347,6 +9666,9 @@ public class TelephonyManager { * attaching to the selected PLMN until reboot; otherwise, attach to the chosen PLMN and resume * normal network selection next time. * @return {@code true} on success; {@code false} on any failure. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @@ -9378,6 +9700,9 @@ public class TelephonyManager { * {@link AccessNetworkConstants.AccessNetworkType#UNKNOWN}, modem will select * the next best RAN for network registration. * @return {@code true} on success; {@code false} on any failure. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) @@ -9430,6 +9755,9 @@ public class TelephonyManager { * or that the calling app has carrier privileges (see {@link #hasCarrierPrivileges}). * * @return the network selection mode. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @SuppressAutoDoc // No support for carrier privileges (b/72967236). @RequiresPermission(anyOf = { @@ -9459,6 +9787,9 @@ public class TelephonyManager { * (see {@link #hasCarrierPrivileges}) * * @return manually selected network info on success or empty string on failure + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @SuppressAutoDoc // No support carrier privileges (b/72967236). @RequiresPermission(android.Manifest.permission.READ_PRECISE_PHONE_STATE) @@ -9488,6 +9819,8 @@ public class TelephonyManager { * * @return {@code true} if this device is in emergency SMS mode, {@code false} otherwise. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. * @hide */ @SystemApi @@ -9557,11 +9890,15 @@ public class TelephonyManager { * * @param networkTypeBitmask The bitmask of preferred network types. * @return true on success; false on any failure. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide * @deprecated Use {@link #setAllowedNetworkTypesForReason} instead. */ @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) @SystemApi public boolean setPreferredNetworkTypeBitmask(@NetworkTypeBitMask long networkTypeBitmask) { try { @@ -9603,6 +9940,10 @@ public class TelephonyManager { * * @param allowedNetworkTypes The bitmask of allowed network types. * @return true on success; false on any failure. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. + * * @hide * @deprecated Use {@link #setAllowedNetworkTypesForReason} instead with reason * {@link #ALLOWED_NETWORK_TYPES_REASON_CARRIER}. @@ -9690,11 +10031,16 @@ public class TelephonyManager { * @throws IllegalStateException if the Telephony process is not currently available. * @throws IllegalArgumentException if invalid AllowedNetworkTypesReason is passed. * @throws SecurityException if the caller does not have the required privileges or if the + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * caller tries to use one of the following security-based reasons without * {@link android.Manifest.permission#MODIFY_PHONE_STATE} permissions. * <ol> * <li>{@code TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G}</li> * </ol> + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @RequiresFeature( @@ -9734,6 +10080,8 @@ public class TelephonyManager { * @throws IllegalStateException if the Telephony process is not currently available. * @throws IllegalArgumentException if invalid AllowedNetworkTypesReason is passed. * @throws SecurityException if the caller does not have the required permission/privileges + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @RequiresFeature( @@ -9808,6 +10156,9 @@ public class TelephonyManager { * <p>Requires that the calling app has carrier privileges (see {@link #hasCarrierPrivileges}). * * @return true on success; false on any failure. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) public boolean setPreferredNetworkTypeToGlobal() { @@ -9833,6 +10184,9 @@ public class TelephonyManager { * Requires Permission: MODIFY_PHONE_STATE. * * @return {@code true} if DUN APN is required for tethering. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. * @hide */ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @@ -9886,6 +10240,9 @@ public class TelephonyManager { * is a superset of the checks done in SubscriptionManager#canManageSubscription. * * @return true if the app has carrier privileges. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public boolean hasCarrierPrivileges() { @@ -9933,6 +10290,9 @@ public class TelephonyManager { * * @param brand The brand name to display/set. * @return true if the operation was executed correctly. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public boolean setOperatorBrandOverride(String brand) { @@ -10034,7 +10394,11 @@ public class TelephonyManager { * Expose the rest of ITelephony to @SystemApi */ - /** @hide */ + /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CDMA}. + * @hide + */ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CDMA) @@ -10042,7 +10406,11 @@ public class TelephonyManager { return getCdmaMdn(getSubId()); } - /** @hide */ + /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CDMA}. + * @hide + */ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CDMA) @@ -10059,7 +10427,11 @@ public class TelephonyManager { } } - /** @hide */ + /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CDMA}. + * @hide + */ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CDMA) @@ -10067,7 +10439,11 @@ public class TelephonyManager { return getCdmaMin(getSubId()); } - /** @hide */ + /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CDMA}. + * @hide + */ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CDMA) @@ -10084,7 +10460,11 @@ public class TelephonyManager { } } - /** @hide */ + /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. + * @hide + */ @SystemApi @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) @@ -10101,7 +10481,11 @@ public class TelephonyManager { return CARRIER_PRIVILEGE_STATUS_NO_ACCESS; } - /** @hide */ + /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. + * @hide + */ @SystemApi @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) @@ -10118,14 +10502,22 @@ public class TelephonyManager { return CARRIER_PRIVILEGE_STATUS_NO_ACCESS; } - /** @hide */ + /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. + * @hide + */ @SystemApi @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public List<String> getCarrierPackageNamesForIntent(Intent intent) { return getCarrierPackageNamesForIntentAndPhone(intent, getPhoneId()); } - /** @hide */ + /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. + * @hide + */ @SystemApi @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) @@ -10152,10 +10544,13 @@ public class TelephonyManager { * @return The system-selected package that provides the {@link CarrierService} implementation * for the current subscription, or {@code null} if none is resolved * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public @Nullable String getCarrierServicePackageName() { return getCarrierServicePackageNameForLogicalSlot(getPhoneId()); } @@ -10169,10 +10564,13 @@ public class TelephonyManager { * @return The system-selected package that provides the {@link CarrierService} implementation * for the slot, or {@code null} if none is resolved * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public @Nullable String getCarrierServicePackageNameForLogicalSlot(int logicalSlotIndex) { try { ITelephony telephony = getITelephony(); @@ -10206,6 +10604,8 @@ public class TelephonyManager { /** * Get the names of packages with carrier privileges for all the active subscriptions. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -10256,6 +10656,8 @@ public class TelephonyManager { * * @throws IllegalArgumentException if requested state is invalid. * @throws SecurityException if the caller does not have the permission. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. */ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) @@ -10285,6 +10687,9 @@ public class TelephonyManager { * * @return the user-set status for enriched calling with call composer, either of * {@link #CALL_COMPOSER_STATUS_ON} or {@link #CALL_COMPOSER_STATUS_OFF}. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. */ @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) @@ -10301,7 +10706,11 @@ public class TelephonyManager { return CALL_COMPOSER_STATUS_OFF; } - /** @hide */ + /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. + * @hide + */ @SystemApi @SuppressLint("RequiresPermission") @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) @@ -10316,6 +10725,9 @@ public class TelephonyManager { } /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. + * * @deprecated Use {@link android.telecom.TelecomManager#placeCall(Uri address, * Bundle extras)} instead. * @hide @@ -10323,6 +10735,7 @@ public class TelephonyManager { @Deprecated @SystemApi @RequiresPermission(android.Manifest.permission.CALL_PHONE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) public void call(String callingPackage, String number) { try { ITelephony telephony = getITelephony(); @@ -10369,6 +10782,8 @@ public class TelephonyManager { } /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELECOM}. * @deprecated Use {@link android.telecom.TelecomManager#isInCall} instead * @hide */ @@ -10378,12 +10793,15 @@ public class TelephonyManager { android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.READ_PHONE_STATE }) + @RequiresFeature(PackageManager.FEATURE_TELECOM) public boolean isOffhook() { TelecomManager tm = (TelecomManager) mContext.getSystemService(TELECOM_SERVICE); return tm.isInCall(); } /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELECOM}. * @deprecated Use {@link android.telecom.TelecomManager#isRinging} instead * @hide */ @@ -10393,12 +10811,15 @@ public class TelephonyManager { android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.READ_PHONE_STATE }) + @RequiresFeature(PackageManager.FEATURE_TELECOM) public boolean isRinging() { TelecomManager tm = (TelecomManager) mContext.getSystemService(TELECOM_SERVICE); return tm.isRinging(); } /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELECOM}. * @deprecated Use {@link android.telecom.TelecomManager#isInCall} instead * @hide */ @@ -10408,12 +10829,15 @@ public class TelephonyManager { android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.READ_PHONE_STATE }) + @RequiresFeature(PackageManager.FEATURE_TELECOM) public boolean isIdle() { TelecomManager tm = (TelecomManager) mContext.getSystemService(TELECOM_SERVICE); return !tm.isInCall(); } /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @deprecated Use {@link android.telephony.TelephonyManager#getServiceState} instead * @hide */ @@ -10423,6 +10847,7 @@ public class TelephonyManager { android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.READ_PHONE_STATE }) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) public boolean isRadioOn() { try { ITelephony telephony = getITelephony(); @@ -10434,7 +10859,11 @@ public class TelephonyManager { return false; } - /** @hide */ + /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. + * @hide + */ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) @@ -10449,7 +10878,11 @@ public class TelephonyManager { return false; } - /** @hide */ + /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. + * @hide + */ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) @@ -10465,11 +10898,15 @@ public class TelephonyManager { } /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. + * * @deprecated use {@link #supplyIccLockPin(String)} instead. * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) @Deprecated public int[] supplyPinReportResult(String pin) { try { @@ -10483,11 +10920,15 @@ public class TelephonyManager { } /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. + * * @deprecated use {@link #supplyIccLockPuk(String, String)} instead. * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) @Deprecated public int[] supplyPukReportResult(String puk, String pin) { try { @@ -10513,6 +10954,8 @@ public class TelephonyManager { * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE} or that the calling * app has carrier privileges (see {@link #hasCarrierPrivileges}). * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -10549,6 +10992,8 @@ public class TelephonyManager { * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE} or that the calling * app has carrier privileges (see {@link #hasCarrierPrivileges}). * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -10622,6 +11067,9 @@ public class TelephonyManager { * @param callback called by the framework to inform the caller of the result of executing the * USSD request (see {@link UssdResponseCallback}). * @param handler the {@link Handler} to run the request on. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @RequiresPermission(android.Manifest.permission.CALL_PHONE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) @@ -10666,6 +11114,9 @@ public class TelephonyManager { * voice and data simultaneously. This can change based on location or network condition. * * @return {@code true} if simultaneous voice and data supported, and {@code false} otherwise. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_DATA) public boolean isConcurrentVoiceAndDataSupported() { @@ -10679,9 +11130,13 @@ public class TelephonyManager { return false; } - /** @hide */ + /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. + * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) public boolean handlePinMmi(String dialString) { try { ITelephony telephony = getITelephony(); @@ -10693,9 +11148,14 @@ public class TelephonyManager { return false; } - /** @hide */ + /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. + * @hide + */ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) public boolean handlePinMmiForSubscriber(int subId, String dialString) { try { ITelephony telephony = getITelephony(); @@ -10707,7 +11167,11 @@ public class TelephonyManager { return false; } - /** @hide */ + /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. + * @hide + */ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) @@ -10725,6 +11189,8 @@ public class TelephonyManager { * @deprecated - use the APIs {@link requestRadioPowerOffForReason} and * {@link clearRadioPowerOffForReason}. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide */ @Deprecated @@ -10752,6 +11218,8 @@ public class TelephonyManager { * @deprecated - use the APIs {@link requestRadioPowerOffForReason} and * {@link clearRadioPowerOffForReason}. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide */ @Deprecated @@ -10799,6 +11267,8 @@ public class TelephonyManager { * @throws SecurityException if the caller does not have MODIFY_PHONE_STATE permission. * @throws IllegalStateException if the Telephony service is not currently available. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide */ @SystemApi @@ -10828,6 +11298,8 @@ public class TelephonyManager { * @throws SecurityException if the caller does not have MODIFY_PHONE_STATE permission. * @throws IllegalStateException if the Telephony service is not currently available. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide */ @SystemApi @@ -10857,6 +11329,8 @@ public class TelephonyManager { * @throws SecurityException if the caller does not have READ_PRIVILEGED_PHONE_STATE permission. * @throws IllegalStateException if the Telephony service is not currently available. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide */ @SystemApi @@ -10886,6 +11360,8 @@ public class TelephonyManager { * <p>To know when the radio has completed powering off, use * {@link PhoneStateListener#LISTEN_SERVICE_STATE LISTEN_SERVICE_STATE}. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide */ @SystemApi @@ -10907,6 +11383,9 @@ public class TelephonyManager { * Check if any radio is on over all the slot indexes. * * @return {@code true} if any radio is on over any slot index. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide */ @SystemApi @@ -10953,6 +11432,8 @@ public class TelephonyManager { * {@link android.Manifest.permission#READ_PHONE_STATE} or that the calling * app has carrier privileges (see {@link #hasCarrierPrivileges}). * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide */ @SystemApi @@ -10982,7 +11463,11 @@ public class TelephonyManager { Log.e(TAG, "Do not call TelephonyManager#updateServiceLocation()"); } - /** @hide */ + /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. + * @hide + */ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_DATA) @@ -10997,7 +11482,11 @@ public class TelephonyManager { return false; } - /** @hide */ + /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. + * @hide + */ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_DATA) @@ -11012,7 +11501,11 @@ public class TelephonyManager { return false; } - /** @hide */ + /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. + * @hide + */ @SystemApi @RequiresFeature(PackageManager.FEATURE_TELEPHONY_DATA) public boolean isDataConnectivityPossible() { @@ -11027,7 +11520,11 @@ public class TelephonyManager { return false; } - /** @hide */ + /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. + * @hide + */ @SystemApi @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) public boolean needsOtaServiceProvisioning() { @@ -11078,23 +11575,29 @@ public class TelephonyManager { * app has carrier privileges (see {@link #hasCarrierPrivileges}). * * @param enable Whether to enable mobile data. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. * @deprecated use setDataEnabledForReason with reason DATA_ENABLED_REASON_USER instead. * */ @Deprecated @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_DATA) public void setDataEnabled(boolean enable) { setDataEnabled(getSubId(SubscriptionManager.getDefaultDataSubscriptionId()), enable); } /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. * @hide * @deprecated use {@link #setDataEnabledForReason(int, boolean)} instead. */ @SystemApi @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_DATA) public void setDataEnabled(int subId, boolean enable) { try { setDataEnabledForReason(subId, DATA_ENABLED_REASON_USER, enable); @@ -11105,10 +11608,14 @@ public class TelephonyManager { /** * @deprecated use {@link #isDataEnabled()} instead. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. * @hide */ @SystemApi @Deprecated + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_DATA) public boolean getDataEnabled() { return isDataEnabled(); } @@ -11132,6 +11639,9 @@ public class TelephonyManager { * {@link ConnectivityManager#getRestrictBackgroundStatus}. * * @return true if mobile data is enabled. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. */ @RequiresPermission(anyOf = {android.Manifest.permission.ACCESS_NETWORK_STATE, android.Manifest.permission.MODIFY_PHONE_STATE, @@ -11162,6 +11672,9 @@ public class TelephonyManager { * has carrier privileges (see {@link #hasCarrierPrivileges}). * * @return {@code true} if the data roaming is enabled on the subscription, otherwise return + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. * {@code false}. */ @RequiresPermission(anyOf = {android.Manifest.permission.ACCESS_NETWORK_STATE, @@ -11201,6 +11714,8 @@ public class TelephonyManager { * {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE READ_PRIVILEGED_PHONE_STATE} * or that the calling app has carrier privileges (see {@link #hasCarrierPrivileges}). * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CDMA}. * @hide */ @SystemApi @@ -11243,6 +11758,8 @@ public class TelephonyManager { * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE} or that the calling * app has carrier privileges (see {@link #hasCarrierPrivileges}). * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CDMA}. * @hide */ @SystemApi @@ -11311,6 +11828,8 @@ public class TelephonyManager { * {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE READ_PRIVILEGED_PHONE_STATE} * or that the calling app has carrier privileges (see {@link #hasCarrierPrivileges}). * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CDMA}. * @hide */ @SystemApi @@ -11349,6 +11868,8 @@ public class TelephonyManager { * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE} or that the calling * app has carrier privileges (see {@link #hasCarrierPrivileges}). * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CDMA}. * @hide */ @SystemApi @@ -11384,6 +11905,8 @@ public class TelephonyManager { * * @param isEnabled {@code true} to enable mobile data roaming, otherwise disable it. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. * @hide */ @SystemApi @@ -11402,11 +11925,15 @@ public class TelephonyManager { } /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. + * * @deprecated use {@link #isDataEnabled()} instead. * @hide */ @Deprecated @SystemApi + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_DATA) public boolean getDataEnabled(int subId) { try { return isDataEnabledForReason(subId, DATA_ENABLED_REASON_USER); @@ -11417,6 +11944,8 @@ public class TelephonyManager { } /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @deprecated Use {@link android.telephony.ims.ImsMmTelManager#setVtSettingEnabled(boolean)} * instead. * @hide @@ -11424,6 +11953,7 @@ public class TelephonyManager { @Deprecated @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_IMS) public void enableVideoCalling(boolean enable) { try { ITelephony telephony = getITelephony(); @@ -11435,6 +11965,8 @@ public class TelephonyManager { } /** + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @deprecated Use {@link ImsMmTelManager#isVtSettingEnabled()} instead to check if the user * has enabled the Video Calling setting, {@link ImsMmTelManager#isAvailable(int, int)} to * determine if video calling is available, or {@link ImsMmTelManager#isCapable(int, int)} to @@ -11447,6 +11979,7 @@ public class TelephonyManager { android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.READ_PHONE_STATE }) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_IMS) public boolean isVideoCallingEnabled() { try { ITelephony telephony = getITelephony(); @@ -11462,6 +11995,9 @@ public class TelephonyManager { * Whether the device supports configuring the DTMF tone length. * * @return {@code true} if the DTMF tone length can be changed, and {@code false} otherwise. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) public boolean canChangeDtmfToneLength() { @@ -11483,7 +12019,11 @@ public class TelephonyManager { * Whether the device is a world phone. * * @return {@code true} if the device is a world phone, and {@code false} otherwise. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY}. */ + @RequiresFeature(PackageManager.FEATURE_TELEPHONY) public boolean isWorldPhone() { try { ITelephony telephony = getITelephony(); @@ -11504,8 +12044,11 @@ public class TelephonyManager { * * @return {@code true} if the device supports TTY mode, and {@code false} otherwise. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELECOM}. */ @Deprecated + @RequiresFeature(PackageManager.FEATURE_TELECOM) public boolean isTtyModeSupported() { try { TelecomManager telecomManager = null; @@ -11526,6 +12069,9 @@ public class TelephonyManager { * support for the feature and device firmware support. * * @return {@code true} if the device and carrier both support RTT, {@code false} otherwise. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_IMS) public boolean isRttSupported() { @@ -11546,6 +12092,9 @@ public class TelephonyManager { * * @return {@code true} if the device supports hearing aid compatibility, and {@code false} * otherwise. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) public boolean isHearingAidCompatibilitySupported() { @@ -11808,11 +12357,14 @@ public class TelephonyManager { * <p>Requires Permission: * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE} * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * {@hide} **/ @SystemApi @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public void setSimPowerState(int state) { setSimPowerStateForSlot(getSlotIndex(), state); } @@ -11834,11 +12386,14 @@ public class TelephonyManager { * <p>Requires Permission: * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE} * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * {@hide} **/ @SystemApi @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public void setSimPowerStateForSlot(int slotIndex, int state) { try { ITelephony telephony = getITelephony(); @@ -11871,6 +12426,8 @@ public class TelephonyManager { * <p>Requires Permission: * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE} * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * {@hide} **/ @SystemApi @@ -11901,6 +12458,8 @@ public class TelephonyManager { * <p>Requires Permission: * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE} * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * {@hide} **/ @SystemApi @@ -12055,6 +12614,9 @@ public class TelephonyManager { * application currently configured for this user. * @return component name of the app and class to direct Respond Via Message intent to, or * {@code null} if the functionality is not supported. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. * @hide */ @SystemApi @@ -12077,6 +12639,9 @@ public class TelephonyManager { * user associated with this subscription. * @return component name of the app and class to direct Respond Via Message intent to, or * {@code null} if the functionality is not supported. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. * @hide */ @SystemApi @@ -12213,9 +12778,13 @@ public class TelephonyManager { * @return The {@link PhoneAccountHandle} associated with the TelphonyManager, or {@code null} * if there is no associated {@link PhoneAccountHandle}; this can happen if the subscription is * data-only or an opportunistic subscription. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) public @Nullable PhoneAccountHandle getPhoneAccountHandle() { return getPhoneAccountHandleForSubscriptionId(getSubId()); } @@ -12277,10 +12846,14 @@ public class TelephonyManager { * Resets Telephony and IMS settings back to factory defaults only for the subscription * associated with this instance. * @see #createForSubscriptionId(int) + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY}. * @hide */ @SystemApi @RequiresPermission(Manifest.permission.CONNECTIVITY_INTERNAL) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY) public void resetSettings() { try { Log.d(TAG, "resetSettings: subId=" + getSubId()); @@ -12302,6 +12875,8 @@ public class TelephonyManager { * * @see Locale#toLanguageTag() * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -12394,10 +12969,14 @@ public class TelephonyManager { * @param callback A callback object to which the result will be delivered. If there was an * error processing the request, {@link OutcomeReceiver#onError} will be called * with more details about the error. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY}. * @hide */ @SystemApi @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY) public void requestModemActivityInfo(@NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<ModemActivityInfo, ModemActivityInfoException> callback) { Objects.requireNonNull(executor); @@ -12484,6 +13063,9 @@ public class TelephonyManager { * and {@link android.Manifest.permission#ACCESS_COARSE_LOCATION}. * May return {@code null} when the subscription is inactive or when there was an error * communicating with the phone process. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(allOf = { @@ -12516,12 +13098,16 @@ public class TelephonyManager { * location related information. * May return {@code null} when the subscription is inactive or when there was an error * communicating with the phone process. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(allOf = { Manifest.permission.READ_PHONE_STATE, Manifest.permission.ACCESS_COARSE_LOCATION }) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) public @Nullable ServiceState getServiceState(@IncludeLocationData int includeLocationData) { return getServiceStateForSubscriber(getSubId(), includeLocationData != INCLUDE_LOCATION_DATA_FINE, @@ -12580,6 +13166,9 @@ public class TelephonyManager { * voicemail ringtone. * @return The URI for the ringtone to play when receiving a voicemail from a specific * PhoneAccount. May be {@code null} if no ringtone is set. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) public @Nullable Uri getVoicemailRingtoneUri(PhoneAccountHandle accountHandle) { @@ -12606,10 +13195,13 @@ public class TelephonyManager { * @param uri The URI for the ringtone to play when receiving a voicemail from a specific * PhoneAccount. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. * @deprecated Use {@link android.provider.Settings#ACTION_CHANNEL_NOTIFICATION_SETTINGS} * instead. */ @Deprecated + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) public void setVoicemailRingtoneUri(PhoneAccountHandle phoneAccountHandle, Uri uri) { try { ITelephony service = getITelephony(); @@ -12627,6 +13219,9 @@ public class TelephonyManager { * @param accountHandle The handle for the {@link PhoneAccount} for which to retrieve the * voicemail vibration setting. * @return {@code true} if the vibration is set for this PhoneAccount, {@code false} otherwise. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) public boolean isVoicemailVibrationEnabled(PhoneAccountHandle accountHandle) { @@ -12653,10 +13248,13 @@ public class TelephonyManager { * @param enabled Whether to enable or disable vibration for voicemail notifications from a * specific PhoneAccount. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. * @deprecated Use {@link android.provider.Settings#ACTION_CHANNEL_NOTIFICATION_SETTINGS} * instead. */ @Deprecated + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) public void setVoicemailVibrationEnabled(PhoneAccountHandle phoneAccountHandle, boolean enabled) { try { @@ -12682,6 +13280,9 @@ public class TelephonyManager { * * @return Carrier id of the current subscription. Return {@link #UNKNOWN_CARRIER_ID} if the * subscription is unavailable or the carrier cannot be identified. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public int getSimCarrierId() { @@ -12707,6 +13308,9 @@ public class TelephonyManager { * * @return Carrier name of the current subscription. Return {@code null} if the subscription is * unavailable or the carrier cannot be identified. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public @Nullable CharSequence getSimCarrierIdName() { @@ -12745,6 +13349,9 @@ public class TelephonyManager { * @return Returns fine-grained carrier id of the current subscription. * Return {@link #UNKNOWN_CARRIER_ID} if the subscription is unavailable or the carrier cannot * be identified. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public int getSimSpecificCarrierId() { @@ -12771,6 +13378,9 @@ public class TelephonyManager { * * @return user-facing name of the subscription specific carrier id. Return {@code null} if the * subscription is unavailable or the carrier cannot be identified. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public @Nullable CharSequence getSimSpecificCarrierIdName() { @@ -12799,6 +13409,9 @@ public class TelephonyManager { * * @return matching carrier id from sim MCCMNC. Return {@link #UNKNOWN_CARRIER_ID} if the * subscription is unavailable or the carrier cannot be identified. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public int getCarrierIdFromSimMccMnc() { @@ -12874,6 +13487,9 @@ public class TelephonyManager { * * @param appType the uicc app type. * @return Application ID for specified app type or {@code null} if no uicc or error. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @Nullable @@ -12939,6 +13555,9 @@ public class TelephonyManager { * Requires that the calling app has READ_PRIVILEGED_PHONE_STATE permission * * @return PRLVersion or null if error. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CDMA}. * @hide */ @SystemApi @@ -13004,6 +13623,9 @@ public class TelephonyManager { * * @return The number of carriers set successfully. Should be length of * carrierList on success; -1 if carrierList null or on error. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CARRIERLOCK}. * @hide */ @SystemApi @@ -13130,6 +13752,9 @@ public class TelephonyManager { * @return {@link #SET_CARRIER_RESTRICTION_SUCCESS} in case of success. * {@link #SET_CARRIER_RESTRICTION_NOT_SUPPORTED} if the modem does not support the * configuration. {@link #SET_CARRIER_RESTRICTION_ERROR} in all other error cases. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CARRIERLOCK}. * @hide */ @SystemApi @@ -13163,11 +13788,15 @@ public class TelephonyManager { * * @return List of {@link android.telephony.CarrierIdentifier}; empty list * means all carriers are allowed. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CARRIERLOCK}. * @hide */ @Deprecated @SystemApi @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CARRIERLOCK) public List<CarrierIdentifier> getAllowedCarriers(int slotIndex) { if (SubscriptionManager.isValidPhoneId(slotIndex)) { CarrierRestrictionRules carrierRestrictionRule = getCarrierRestrictionRules(); @@ -13189,6 +13818,9 @@ public class TelephonyManager { * @return {@link CarrierRestrictionRules} which contains the allowed carrier list and the * excluded carrier list with the priority between the two lists. Returns {@code null} * in case of error. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CARRIERLOCK}. * @hide */ @SystemApi @@ -13257,6 +13889,8 @@ public class TelephonyManager { * status result fetched from the radio * @throws SecurityException if the caller does not have the required permission/privileges or * if the caller is not pre-registered. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) @@ -13323,11 +13957,15 @@ public class TelephonyManager { * @see #resetAllCarrierActions() * @deprecated use {@link #setDataEnabledForReason(int, boolean) with * reason {@link #DATA_ENABLED_REASON_CARRIER}} instead. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. * @hide */ @Deprecated @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_DATA) public void setCarrierDataEnabled(boolean enabled) { try { setDataEnabledForReason(DATA_ENABLED_REASON_CARRIER, enabled); @@ -13351,6 +13989,8 @@ public class TelephonyManager { * @deprecated - use the APIs {@link requestRadioPowerOffForReason} and * {@link clearRadioPowerOffForReason}. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide */ @Deprecated @@ -13470,6 +14110,9 @@ public class TelephonyManager { * * @param report control start/stop reporting network status. * @see #resetAllCarrierActions() + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide */ @SystemApi @@ -13496,6 +14139,8 @@ public class TelephonyManager { * * <p>Requires Permission: * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -13618,6 +14263,8 @@ public class TelephonyManager { * has {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE} irrespective of * the reason. * @throws IllegalStateException if the Telephony process is not currently available. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. */ @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_DATA) @@ -13661,6 +14308,8 @@ public class TelephonyManager { * {@link android.Manifest.permission#MODIFY_PHONE_STATE} * {@link android.Manifest.permission#READ_BASIC_PHONE_STATE} * @throws IllegalStateException if the Telephony process is not currently available. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. */ @RequiresPermission(anyOf = {android.Manifest.permission.ACCESS_NETWORK_STATE, android.Manifest.permission.READ_PHONE_STATE, @@ -13717,6 +14366,9 @@ public class TelephonyManager { * given subId. Otherwise, applies to {@link SubscriptionManager#getDefaultSubscriptionId()} * * @return true if phone is in emergency callback mode. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. * @hide */ @SystemApi @@ -13756,6 +14408,9 @@ public class TelephonyManager { * given subId. Otherwise, applies to {@link SubscriptionManager#getDefaultSubscriptionId()}. * * @return {@code true} if manual network selection is allowed, otherwise return {@code false}. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @SuppressAutoDoc // No support carrier privileges (b/72967236). @RequiresPermission(anyOf = {android.Manifest.permission.READ_PRECISE_PHONE_STATE, @@ -13779,6 +14434,9 @@ public class TelephonyManager { * Get the most recent SignalStrength information reported by the modem. Due * to power saving this information may not always be current. * @return the most recent cached signal strength info from the modem + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @Nullable @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) @@ -13805,6 +14463,9 @@ public class TelephonyManager { * <LI>And possibly others.</LI> * </UL> * @return {@code true} if the overall data connection is allowed; {@code false} if not. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. */ @RequiresPermission(anyOf = {android.Manifest.permission.ACCESS_NETWORK_STATE, android.Manifest.permission.READ_PHONE_STATE, @@ -13975,6 +14636,9 @@ public class TelephonyManager { * * @param enable enable(True) or disable(False) * @return returns true if successfully set. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide */ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @@ -14004,6 +14668,9 @@ public class TelephonyManager { * <p> * Requires Permission: * {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE READ_PRIVILEGED_PHONE_STATE} + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide */ @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @@ -14205,6 +14872,8 @@ public class TelephonyManager { * that the calling app has carrier privileges (see {@link #hasCarrierPrivileges}). * * @throws SecurityException if the caller does not have the required permission + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) @@ -14240,6 +14909,8 @@ public class TelephonyManager { * <p> Requires permission: * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE} * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. * @hide */ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @@ -14267,6 +14938,8 @@ public class TelephonyManager { * <p> Requires permission: * {@link android.Manifest.permission#READ_ACTIVE_EMERGENCY_SESSION} * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. * @hide */ @RequiresPermission(android.Manifest.permission.READ_ACTIVE_EMERGENCY_SESSION) @@ -14293,6 +14966,8 @@ public class TelephonyManager { * <p> Requires permission: * {@link android.Manifest.permission#READ_ACTIVE_EMERGENCY_SESSION} * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. * @hide */ @RequiresPermission(android.Manifest.permission.READ_ACTIVE_EMERGENCY_SESSION) @@ -14353,6 +15028,9 @@ public class TelephonyManager { * subscription, the key is {@link SubscriptionManager#getDefaultSubscriptionId}) and the value * as the list of {@link EmergencyNumber}; empty Map if this information is not available; * or throw a SecurityException if the caller does not have the permission. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. */ @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) @NonNull @@ -14409,6 +15087,8 @@ public class TelephonyManager { * as the list of {@link EmergencyNumber}; empty Map if this information is not available; * or throw a SecurityException if the caller does not have the permission. * @throws IllegalStateException if the Telephony process is not currently available. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. */ @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) @NonNull @@ -14477,6 +15157,8 @@ public class TelephonyManager { * @return {@code true} if the given number is an emergency number based on current locale, * SIM card(s), Android database, modem, network or defaults; {@code false} otherwise. * @throws IllegalStateException if the Telephony process is not currently available. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) public boolean isEmergencyNumber(@NonNull String number) { @@ -14514,6 +15196,8 @@ public class TelephonyManager { * network; {@code false} if it is not; or throw an SecurityException if the caller does not * have the required permission/privileges * @throws IllegalStateException if the Telephony process is not currently available. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. * * @deprecated Please use {@link TelephonyManager#isEmergencyNumber(String)} instead. * @hide @@ -14543,6 +15227,8 @@ public class TelephonyManager { * <p>Requires Permission: * {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE READ_PRIVILEGED_PHONE_STATE} * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. * @hide */ @SystemApi @@ -14694,6 +15380,9 @@ public class TelephonyManager { * @param callback Callback will be triggered once it succeeds or failed. * See the {@code SET_OPPORTUNISTIC_SUB_*} constants * for more details. Pass null if don't care about the result. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_DATA) public void setPreferredOpportunisticDataSubscription(int subId, boolean needValidation, @@ -14754,6 +15443,8 @@ public class TelephonyManager { * {@link SubscriptionManager#DEFAULT_SUBSCRIPTION_ID} if there are no preferred * subscription id * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. */ @RequiresPermission(anyOf = { android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, @@ -14793,6 +15484,8 @@ public class TelephonyManager { * @param executor The executor of where the callback will execute. * @param callback Callback will be triggered once it succeeds or failed. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) @@ -14853,10 +15546,13 @@ public class TelephonyManager { * @param enable whether to enable or disable the modem stack. * @return whether the operation is successful. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY}. * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY) public boolean enableModemForSlot(int slotIndex, boolean enable) { boolean ret = false; try { @@ -14879,10 +15575,14 @@ public class TelephonyManager { * {@link #hasCarrierPrivileges()}). * * @param slotIndex which slot it's checking. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(anyOf = {android.Manifest.permission.READ_PHONE_STATE, android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE}) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY) public boolean isModemEnabledForSlot(int slotIndex) { try { ITelephony telephony = getITelephony(); @@ -14945,6 +15645,8 @@ public class TelephonyManager { * @param isMultiSimCarrierRestricted true if usage of multiple SIMs is restricted, false * otherwise. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CARRIERLOCK}. * @hide */ @SystemApi @@ -15000,6 +15702,9 @@ public class TelephonyManager { * {@link #MULTISIM_NOT_SUPPORTED_BY_HARDWARE} if the device does not support multiple SIMs. * {@link #MULTISIM_NOT_SUPPORTED_BY_CARRIER} in the device supports multiple SIMs, but the * functionality is restricted by the carrier. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) @@ -15031,6 +15736,8 @@ public class TelephonyManager { * * @param numOfSims number of live SIMs we want to switch to * @throws android.os.RemoteException + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @@ -15058,6 +15765,9 @@ public class TelephonyManager { * * @return {@code true} if reboot will be triggered after making changes to modem * configurations, otherwise return {@code false}. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresPermission(Manifest.permission.READ_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) @@ -15220,6 +15930,8 @@ public class TelephonyManager { * {@link #CARRIER_PRIVILEGE_STATUS_RULES_NOT_LOADED}, or * {@link #CARRIER_PRIVILEGE_STATUS_ERROR_LOADING_RULES} * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -15334,6 +16046,8 @@ public class TelephonyManager { * @param apnType Value indicating the apn type. Apn types are defined in {@link ApnSetting}. * @return whether data is enabled for a apn type. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. * @hide */ @SystemApi @@ -15356,6 +16070,8 @@ public class TelephonyManager { * Whether an APN type is metered or not. It will be evaluated with the subId associated * with the TelephonyManager instance. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. * @hide */ @SystemApi @@ -15385,6 +16101,9 @@ public class TelephonyManager { * @param executor The executor to execute the callback on * @param callback The callback that gets invoked when the radio responds to the request. Called * with {@code true} if the request succeeded, {@code false} otherwise. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide */ @SystemApi @@ -15403,6 +16122,9 @@ public class TelephonyManager { * Same as {@link #setSystemSelectionChannels(List, Executor, Consumer<Boolean>)}, but to be * used when the caller does not need feedback on the results of the operation. * @param specifiers which bands to scan. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide */ @SystemApi @@ -15450,6 +16172,8 @@ public class TelephonyManager { * @return a list of {@link RadioAccessSpecifier}, or an empty list if no bands are specified. * @throws IllegalStateException if the Telephony process is not currently available. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide */ @SystemApi @@ -15478,6 +16202,8 @@ public class TelephonyManager { * @return {@code true} if input mccmnc and mvno matches with data from sim operator. * {@code false} otherwise. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * {@hide} */ @SystemApi @@ -15568,6 +16294,8 @@ public class TelephonyManager { * {@link CallForwardingInfo#REASON_UNCONDITIONAL}, {@link CallForwardingInfo#REASON_BUSY}, * {@link CallForwardingInfo#REASON_NO_REPLY}, {@link CallForwardingInfo#REASON_NOT_REACHABLE}, * {@link CallForwardingInfo#REASON_ALL}, or {@link CallForwardingInfo#REASON_ALL_CONDITIONAL} + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. * * @hide */ @@ -15645,6 +16373,8 @@ public class TelephonyManager { * <li>{@link CallForwardingInfo#getTimeoutSeconds()} returns a non-positive value when * enabling call forwarding</li> * </ul> + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. * @hide */ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @@ -15769,6 +16499,9 @@ public class TelephonyManager { * <li>{@link #CALL_WAITING_STATUS_NOT_SUPPORTED}}</li> * <li>{@link #CALL_WAITING_STATUS_FDN_CHECK_FAILURE}}</li> * </ul> + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. * @hide */ @SystemApi @@ -15819,6 +16552,9 @@ public class TelephonyManager { * {@link #CALL_WAITING_STATUS_NOT_SUPPORTED} or * {@link #CALL_WAITING_STATUS_UNKNOWN_ERROR} or * {@link #CALL_WAITING_STATUS_FDN_CHECK_FAILURE} if it failed. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_CALLING}. * @hide */ @SystemApi @@ -15919,6 +16655,9 @@ public class TelephonyManager { * * @param policy The data policy to enable. * @param enabled Whether to enable or disable the policy. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. * @hide */ @SystemApi @@ -15940,6 +16679,9 @@ public class TelephonyManager { * * @param policy The data policy that you want the status for. * @return {@code true} if enabled, {@code false} otherwise. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. * @hide */ @SystemApi @@ -15975,6 +16717,8 @@ public class TelephonyManager { * {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE READ_PRIVILEGED_PHONE_STATE} * or that the calling app has carrier privileges (see {@link #hasCarrierPrivileges}). * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @WorkerThread @@ -16009,6 +16753,8 @@ public class TelephonyManager { * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE} or that the calling * app has carrier privileges (see {@link #hasCarrierPrivileges}). * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -16051,6 +16797,8 @@ public class TelephonyManager { * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE} or that the calling * app has carrier privileges (see {@link #hasCarrierPrivileges}). * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -16199,6 +16947,8 @@ public class TelephonyManager { * </ol> * @return operation result. * @throws IllegalStateException if the Telephony process is not currently available. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide */ @SystemApi @@ -16233,6 +16983,8 @@ public class TelephonyManager { * connectivity is active. It means the device is allowed to connect to both primary and * secondary cell. * @throws IllegalStateException if the Telephony process is not currently available. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide */ @SystemApi @@ -16470,6 +17222,8 @@ public class TelephonyManager { * * @throws IllegalStateException if the Telephony process is not currently available. * @throws SecurityException if the caller doesn't have the permission. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * */ @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) @@ -16586,6 +17340,9 @@ public class TelephonyManager { * * @param capability the name of the capability to check for * @return the availability of the capability + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS) public boolean isRadioInterfaceCapabilitySupported( @@ -16705,6 +17462,8 @@ public class TelephonyManager { * @throws IllegalArgumentException if the thermalMitigationRequest had invalid parameters or * if the device's modem does not support data throttling. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. * @hide */ @SystemApi @@ -17037,6 +17796,9 @@ public class TelephonyManager { * contain the GBA Ks_NAF/Ks_ext_NAF when available. If the NAF keys are * available and valid at the time of call and bootstrapping is not requested, * then the callback shall be invoked with the available keys. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @@ -17135,6 +17897,8 @@ public class TelephonyManager { * @param request the SignalStrengthUpdateRequest to be set into the System * * @throws IllegalStateException if a new request is set with same subId from the same caller + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @@ -17165,6 +17929,9 @@ public class TelephonyManager { * @see #setSignalStrengthUpdateRequest(SignalStrengthUpdateRequest) * * @param request the SignalStrengthUpdateRequest to be cleared from the System + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @@ -17188,10 +17955,14 @@ public class TelephonyManager { * @return the PhoneCapability which describes the data connection capability of modem. * It's used to evaluate possible phone config change, for example from single * SIM device to multi-SIM device. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public @NonNull PhoneCapability getPhoneCapability() { try { ITelephony telephony = getITelephony(); @@ -17256,11 +18027,15 @@ public class TelephonyManager { * at least one SIM card for which the user needs to manually enter the PIN * code after the reboot. {@link #PREPARE_UNATTENDED_REBOOT_ERROR} in case * of error. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.REBOOT) @PrepareUnattendedRebootResult + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public int prepareForUnattendedReboot() { try { ITelephony service = getITelephony(); @@ -17363,6 +18138,9 @@ public class TelephonyManager { * * @param executor the executor on which callback will be invoked. * @param callback a callback to receive the current slicing configuration. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}. */ @RequiresFeature( enforcement = "android.telephony.TelephonyManager#isRadioInterfaceCapabilitySupported", @@ -17444,8 +18222,11 @@ public class TelephonyManager { * @param capability The premium capability to check. * @return Whether the given premium capability is available to purchase. * @throws SecurityException if the caller does not hold permission READ_BASIC_PHONE_STATE. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. */ @RequiresPermission(android.Manifest.permission.READ_BASIC_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_DATA) public boolean isPremiumCapabilityAvailableForPurchase(@PremiumCapability int capability) { try { ITelephony telephony = getITelephony(); @@ -17685,10 +18466,13 @@ public class TelephonyManager { * @param callback The result of the purchase request. * @throws SecurityException if the caller does not hold permissions * READ_BASIC_PHONE_STATE or INTERNET. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_DATA}. * @see #isPremiumCapabilityAvailableForPurchase(int) to check whether the capability is valid. */ @RequiresPermission(allOf = {android.Manifest.permission.READ_BASIC_PHONE_STATE, android.Manifest.permission.INTERNET}) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_DATA) public void purchasePremiumCapability(@PremiumCapability int capability, @NonNull @CallbackExecutor Executor executor, @NonNull @PurchasePremiumCapabilityResult Consumer<Integer> callback) { @@ -18142,10 +18926,14 @@ public class TelephonyManager { * Get current cell broadcast message identifier ranges. * * @throws SecurityException if the caller does not have the required permission + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. + * * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_CELL_BROADCASTS) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) @NonNull public List<CellBroadcastIdRange> getCellBroadcastIdRanges() { try { @@ -18299,10 +19087,13 @@ public class TelephonyManager { * the result when the operation completes. * @throws SecurityException if the caller does not have the required permission * @throws IllegalArgumentException when the ranges are invalid. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}. * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_CELL_BROADCASTS) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public void setCellBroadcastIdRanges(@NonNull List<CellBroadcastIdRange> ranges, @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<Integer> callback) { @@ -18378,7 +19169,8 @@ public class TelephonyManager { * </ul> * * @return Primary IMEI of type string - * @throws UnsupportedOperationException if the radio doesn't support this feature. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_GSM}. * @throws SecurityException if the caller does not have the required permission/privileges */ @NonNull diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java index 60b5ce75e2f7..80c1e5be3a32 100644 --- a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java +++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java @@ -810,6 +810,7 @@ public class GraphicsActivity extends Activity { private FpsRange convertCategory(int category) { switch (category) { + case Surface.FRAME_RATE_CATEGORY_HIGH_HINT: case Surface.FRAME_RATE_CATEGORY_HIGH: return FRAME_RATE_CATEGORY_HIGH; case Surface.FRAME_RATE_CATEGORY_NORMAL: diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java index 4b56c107cf22..caaee634c57a 100644 --- a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java +++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java @@ -93,6 +93,12 @@ public class SurfaceControlTest { } @Test + public void testSurfaceControlFrameRateCategoryHighHint() throws InterruptedException { + GraphicsActivity activity = mActivityRule.getActivity(); + activity.testSurfaceControlFrameRateCategory(Surface.FRAME_RATE_CATEGORY_HIGH_HINT); + } + + @Test public void testSurfaceControlFrameRateCategoryNormal() throws InterruptedException { GraphicsActivity activity = mActivityRule.getActivity(); activity.testSurfaceControlFrameRateCategory(Surface.FRAME_RATE_CATEGORY_NORMAL); diff --git a/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt b/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt new file mode 100644 index 000000000000..e2b0c36ae694 --- /dev/null +++ b/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt @@ -0,0 +1,215 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.input + +import android.content.Context +import android.content.ContextWrapper +import android.os.Handler +import android.os.HandlerExecutor +import android.os.test.TestLooper +import android.platform.test.annotations.Presubmit +import android.platform.test.flag.junit.SetFlagsRule +import android.view.KeyEvent +import androidx.test.core.app.ApplicationProvider +import com.android.server.testutils.any +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnitRunner +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.test.fail + +/** + * Tests for [InputManager.StickyModifierStateListener]. + * + * Build/Install/Run: + * atest InputTests:StickyModifierStateListenerTest + */ +@Presubmit +@RunWith(MockitoJUnitRunner::class) +class StickyModifierStateListenerTest { + + @get:Rule + val rule = SetFlagsRule() + + private val testLooper = TestLooper() + private val executor = HandlerExecutor(Handler(testLooper.looper)) + private var registeredListener: IStickyModifierStateListener? = null + private lateinit var context: Context + private lateinit var inputManager: InputManager + private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession + + @Mock + private lateinit var iInputManagerMock: IInputManager + + @Before + fun setUp() { + // Enable Sticky keys feature + rule.enableFlags(com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG) + rule.enableFlags(com.android.input.flags.Flags.FLAG_ENABLE_INPUT_FILTER_RUST_IMPL) + + context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) + inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManagerMock) + inputManager = InputManager(context) + `when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) + .thenReturn(inputManager) + + // Handle sticky modifier state listener registration. + doAnswer { + val listener = it.getArgument(0) as IStickyModifierStateListener + if (registeredListener != null && + registeredListener!!.asBinder() != listener.asBinder()) { + // There can only be one registered sticky modifier state listener per process. + fail("Trying to register a new listener when one already exists") + } + registeredListener = listener + null + }.`when`(iInputManagerMock).registerStickyModifierStateListener(any()) + + // Handle sticky modifier state listener being unregistered. + doAnswer { + val listener = it.getArgument(0) as IStickyModifierStateListener + if (registeredListener == null || + registeredListener!!.asBinder() != listener.asBinder()) { + fail("Trying to unregister a listener that is not registered") + } + registeredListener = null + null + }.`when`(iInputManagerMock).unregisterStickyModifierStateListener(any()) + } + + @After + fun tearDown() { + if (this::inputManagerGlobalSession.isInitialized) { + inputManagerGlobalSession.close() + } + } + + private fun notifyStickyModifierStateChanged(modifierState: Int, lockedModifierState: Int) { + registeredListener!!.onStickyModifierStateChanged(modifierState, lockedModifierState) + } + + @Test + fun testListenerIsNotifiedOnModifierStateChanged() { + var callbackCount = 0 + + // Add a sticky modifier state listener + inputManager.registerStickyModifierStateListener(executor) { + callbackCount++ + } + + // Notifying sticky modifier state change will notify the listener. + notifyStickyModifierStateChanged(0, 0) + testLooper.dispatchNext() + assertEquals(1, callbackCount) + } + + @Test + fun testListenerHasCorrectModifierStateNotified() { + // Add a sticky modifier state listener + inputManager.registerStickyModifierStateListener(executor) { + state: StickyModifierState -> + assertTrue(state.isAltModifierOn) + assertTrue(state.isAltModifierLocked) + assertTrue(state.isShiftModifierOn) + assertTrue(!state.isShiftModifierLocked) + assertTrue(!state.isCtrlModifierOn) + assertTrue(!state.isCtrlModifierLocked) + assertTrue(!state.isMetaModifierOn) + assertTrue(!state.isMetaModifierLocked) + assertTrue(!state.isAltGrModifierOn) + assertTrue(!state.isAltGrModifierLocked) + } + + // Notifying sticky modifier state change will notify the listener. + notifyStickyModifierStateChanged( + KeyEvent.META_ALT_ON or KeyEvent.META_ALT_LEFT_ON or + KeyEvent.META_SHIFT_ON or KeyEvent.META_SHIFT_LEFT_ON, + KeyEvent.META_ALT_ON or KeyEvent.META_ALT_LEFT_ON + ) + testLooper.dispatchNext() + } + + @Test + fun testAddingListenersRegistersInternalCallbackListener() { + // Set up two callbacks. + val callback1 = InputManager.StickyModifierStateListener {} + val callback2 = InputManager.StickyModifierStateListener {} + + assertNull(registeredListener) + + // Adding the listener should register the callback with InputManagerService. + inputManager.registerStickyModifierStateListener(executor, callback1) + assertNotNull(registeredListener) + + // Adding another listener should not register new internal listener. + val currListener = registeredListener + inputManager.registerStickyModifierStateListener(executor, callback2) + assertEquals(currListener, registeredListener) + } + + @Test + fun testRemovingListenersUnregistersInternalCallbackListener() { + // Set up two callbacks. + val callback1 = InputManager.StickyModifierStateListener {} + val callback2 = InputManager.StickyModifierStateListener {} + + inputManager.registerStickyModifierStateListener(executor, callback1) + inputManager.registerStickyModifierStateListener(executor, callback2) + + // Only removing all listeners should remove the internal callback + inputManager.unregisterStickyModifierStateListener(callback1) + assertNotNull(registeredListener) + inputManager.unregisterStickyModifierStateListener(callback2) + assertNull(registeredListener) + } + + @Test + fun testMultipleListeners() { + // Set up two callbacks. + var callbackCount1 = 0 + var callbackCount2 = 0 + val callback1 = InputManager.StickyModifierStateListener { _ -> callbackCount1++ } + val callback2 = InputManager.StickyModifierStateListener { _ -> callbackCount2++ } + + // Add both sticky modifier state listeners + inputManager.registerStickyModifierStateListener(executor, callback1) + inputManager.registerStickyModifierStateListener(executor, callback2) + + // Notifying sticky modifier state change trigger the both callbacks. + notifyStickyModifierStateChanged(0, 0) + testLooper.dispatchAll() + assertEquals(1, callbackCount1) + assertEquals(1, callbackCount2) + + inputManager.unregisterStickyModifierStateListener(callback2) + // Notifying sticky modifier state change should still trigger callback1 but not callback2. + notifyStickyModifierStateChanged(0, 0) + testLooper.dispatchAll() + assertEquals(2, callbackCount1) + assertEquals(1, callbackCount2) + } +} diff --git a/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt index 9c335768de55..bbd4567a4454 100644 --- a/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt @@ -36,12 +36,12 @@ import android.util.proto.ProtoOutputStream import android.view.InputDevice import android.view.inputmethod.InputMethodInfo import android.view.inputmethod.InputMethodSubtype -import androidx.test.core.R import androidx.test.core.app.ApplicationProvider import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.internal.os.KeyboardConfiguredProto import com.android.internal.util.FrameworkStatsLog import com.android.modules.utils.testing.ExtendedMockitoRule +import com.android.test.input.R import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals |