diff options
1251 files changed, 32882 insertions, 12647 deletions
diff --git a/.gitignore b/.gitignore index b093c811abd5..b4af5676e9d2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,8 @@ gen/ .vscode/ *.code-workspace .gradle/ +# .classpath and .settings/ are configurations +# used by the IDE and java development tools +# to configure filepaths + project settings +.classpath +.settings/ diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg index e862cd9e0a95..843fde7baf2b 100644 --- a/PREUPLOAD.cfg +++ b/PREUPLOAD.cfg @@ -19,7 +19,7 @@ clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp tests/ tools/ bpfmt = -d -ktfmt = --kotlinlang-style --include-dirs=services/permission,packages/SystemUI,libs/WindowManager/Shell/src/com/android/wm/shell/freeform,libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode,libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode,apct-tests +ktfmt = --kotlinlang-style --include-dirs=services/permission,packages/SystemUI,libs/WindowManager/Shell/src/com/android/wm/shell/freeform,libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode,libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode,apct-tests,tests/Input [Hook Scripts] checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT} diff --git a/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java b/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java index f6ae56f01758..5b3b876edd3a 100644 --- a/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java +++ b/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java @@ -32,11 +32,13 @@ import android.os.RemoteCallback; import android.os.RemoteException; import android.os.UserHandle; +import com.android.internal.util.Preconditions; import com.android.internal.util.function.pooled.PooledLambda; import java.io.Closeable; import java.io.IOException; import java.util.List; +import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; @@ -153,6 +155,26 @@ public class BlobStoreManager { private final Context mContext; private final IBlobStoreManager mService; + // TODO: b/404309424 - Make these constants available using a test-api to avoid hardcoding + // them in tests. + /** + * The maximum allowed length for the package name, provided using + * {@link BlobStoreManager.Session#allowPackageAccess(String, byte[])}. + * + * This is the same limit that is already used for limiting the length of the package names + * at android.content.pm.parsing.FrameworkParsingPackageUtils#MAX_FILE_NAME_SIZE. + * + * @hide + */ + public static final int MAX_PACKAGE_NAME_LENGTH = 223; + /** + * The maximum allowed length for the certificate, provided using + * {@link BlobStoreManager.Session#allowPackageAccess(String, byte[])}. + * + * @hide + */ + public static final int MAX_CERTIFICATE_LENGTH = 32; + /** @hide */ public BlobStoreManager(@NonNull Context context, @NonNull IBlobStoreManager service) { mContext = context; @@ -786,6 +808,12 @@ public class BlobStoreManager { */ public void allowPackageAccess(@NonNull String packageName, @NonNull byte[] certificate) throws IOException { + Objects.requireNonNull(packageName); + Preconditions.checkArgument(packageName.length() <= MAX_PACKAGE_NAME_LENGTH, + "packageName is longer than " + MAX_PACKAGE_NAME_LENGTH + " chars"); + Objects.requireNonNull(certificate); + Preconditions.checkArgument(certificate.length <= MAX_CERTIFICATE_LENGTH, + "certificate is longer than " + MAX_CERTIFICATE_LENGTH + " chars"); try { mSession.allowPackageAccess(packageName, certificate); } catch (ParcelableException e) { diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java index ede29ec168c0..790d4e934317 100644 --- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java +++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java @@ -16,6 +16,8 @@ package com.android.server.blob; import static android.app.blob.BlobStoreManager.COMMIT_RESULT_ERROR; +import static android.app.blob.BlobStoreManager.MAX_CERTIFICATE_LENGTH; +import static android.app.blob.BlobStoreManager.MAX_PACKAGE_NAME_LENGTH; import static android.app.blob.XmlTags.ATTR_CREATION_TIME_MS; import static android.app.blob.XmlTags.ATTR_ID; import static android.app.blob.XmlTags.ATTR_PACKAGE; @@ -328,6 +330,11 @@ class BlobStoreSession extends IBlobStoreSession.Stub { @NonNull byte[] certificate) { assertCallerIsOwner(); Objects.requireNonNull(packageName, "packageName must not be null"); + Preconditions.checkArgument(packageName.length() <= MAX_PACKAGE_NAME_LENGTH, + "packageName is longer than " + MAX_PACKAGE_NAME_LENGTH + " chars"); + Objects.requireNonNull(certificate, "certificate must not be null"); + Preconditions.checkArgument(certificate.length <= MAX_CERTIFICATE_LENGTH, + "certificate is longer than " + MAX_CERTIFICATE_LENGTH + " chars"); synchronized (mSessionLock) { if (mState != STATE_OPENED) { throw new IllegalStateException("Not allowed to change access type in state: " diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java index 5dfb3754e8fb..7e421676b3c9 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java @@ -2039,8 +2039,8 @@ class JobConcurrencyManager { DeviceConfig.Properties properties = DeviceConfig.getProperties(DeviceConfig.NAMESPACE_JOB_SCHEDULER); - // Concurrency limit should be in the range [8, MAX_CONCURRENCY_LIMIT]. - mSteadyStateConcurrencyLimit = Math.max(8, Math.min(MAX_CONCURRENCY_LIMIT, + // Concurrency limit should be in the range [1, MAX_CONCURRENCY_LIMIT]. + mSteadyStateConcurrencyLimit = Math.max(1, Math.min(MAX_CONCURRENCY_LIMIT, properties.getInt(KEY_CONCURRENCY_LIMIT, DEFAULT_CONCURRENCY_LIMIT))); mScreenOffAdjustmentDelayMs = properties.getLong( diff --git a/api/coverage/tools/ExtractFlaggedApis.kt b/api/coverage/tools/ExtractFlaggedApis.kt index 0a3ae4f790b0..e50f7f876f51 100644 --- a/api/coverage/tools/ExtractFlaggedApis.kt +++ b/api/coverage/tools/ExtractFlaggedApis.kt @@ -88,6 +88,6 @@ fun getFlagAnnotation(item: Item): String? { return item.modifiers .findAnnotation("android.annotation.FlaggedApi") ?.findAttribute("value") - ?.value + ?.legacyValue ?.value() as? String } diff --git a/cmds/app_process/Android.bp b/cmds/app_process/Android.bp index 3c7609e1d8ed..a1575173ded6 100644 --- a/cmds/app_process/Android.bp +++ b/cmds/app_process/Android.bp @@ -56,7 +56,6 @@ cc_binary { "libsigchain", "libutils", - "libutilscallstack", // This is a list of libraries that need to be included in order to avoid // bad apps. This prevents a library from having a mismatch when resolving diff --git a/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java b/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java index d3e62d5351f0..017d9563b9a8 100644 --- a/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java +++ b/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java @@ -61,17 +61,18 @@ public class EvemuParser implements EventParser { mReader = in; } - private @Nullable String findNextLine() throws IOException { + private void findNextLine() throws IOException { String line = ""; while (line != null && line.length() == 0) { String unstrippedLine = mReader.readLine(); if (unstrippedLine == null) { mAtEndOfFile = true; - return null; + mNextLine = null; + return; } line = stripComments(unstrippedLine); } - return line; + mNextLine = line; } private static String stripComments(String line) { @@ -92,7 +93,7 @@ public class EvemuParser implements EventParser { */ public @Nullable String peekLine() throws IOException { if (mNextLine == null && !mAtEndOfFile) { - mNextLine = findNextLine(); + findNextLine(); } return mNextLine; } @@ -103,7 +104,10 @@ public class EvemuParser implements EventParser { mNextLine = null; } - public boolean isAtEndOfFile() { + public boolean isAtEndOfFile() throws IOException { + if (mNextLine == null && !mAtEndOfFile) { + findNextLine(); + } return mAtEndOfFile; } diff --git a/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java b/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java index 5239fbc7e0a8..f18cab51fb4d 100644 --- a/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java +++ b/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java @@ -216,6 +216,9 @@ public class EvemuParserTest { assertInjectEvent(parser.getNextEvent(), 0x2, 0x0, 1, -1); assertInjectEvent(parser.getNextEvent(), 0x2, 0x1, -2); assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0); + + // Now we should be at the end of the file. + assertThat(parser.getNextEvent()).isNull(); } @Test @@ -246,6 +249,8 @@ public class EvemuParserTest { assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 1, 1_000_000); assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0); + + assertThat(parser.getNextEvent()).isNull(); } @Test diff --git a/core/api/current.txt b/core/api/current.txt index bba21f418e41..07224db7dcd3 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -273,6 +273,7 @@ package android { field public static final String READ_SYNC_SETTINGS = "android.permission.READ_SYNC_SETTINGS"; field public static final String READ_SYNC_STATS = "android.permission.READ_SYNC_STATS"; field @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public static final String READ_SYSTEM_PREFERENCES = "android.permission.READ_SYSTEM_PREFERENCES"; + field @FlaggedApi("com.android.update_engine.minor_changes_2025q4") public static final String READ_UPDATE_ENGINE_LOGS = "android.permission.READ_UPDATE_ENGINE_LOGS"; field public static final String READ_VOICEMAIL = "com.android.voicemail.permission.READ_VOICEMAIL"; field public static final String REBOOT = "android.permission.REBOOT"; field public static final String RECEIVE_BOOT_COMPLETED = "android.permission.RECEIVE_BOOT_COMPLETED"; @@ -286,7 +287,7 @@ package android { field public static final String REQUEST_COMPANION_PROFILE_COMPUTER = "android.permission.REQUEST_COMPANION_PROFILE_COMPUTER"; field public static final String REQUEST_COMPANION_PROFILE_GLASSES = "android.permission.REQUEST_COMPANION_PROFILE_GLASSES"; field public static final String REQUEST_COMPANION_PROFILE_NEARBY_DEVICE_STREAMING = "android.permission.REQUEST_COMPANION_PROFILE_NEARBY_DEVICE_STREAMING"; - field @FlaggedApi("android.companion.virtualdevice.flags.enable_limited_vdm_role") public static final String REQUEST_COMPANION_PROFILE_SENSOR_DEVICE_STREAMING = "android.permission.REQUEST_COMPANION_PROFILE_SENSOR_DEVICE_STREAMING"; + field @FlaggedApi("android.companion.virtualdevice.flags.enable_limited_vdm_role") public static final String REQUEST_COMPANION_PROFILE_VIRTUAL_DEVICE = "android.permission.REQUEST_COMPANION_PROFILE_VIRTUAL_DEVICE"; field public static final String REQUEST_COMPANION_PROFILE_WATCH = "android.permission.REQUEST_COMPANION_PROFILE_WATCH"; field public static final String REQUEST_COMPANION_RUN_IN_BACKGROUND = "android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND"; field public static final String REQUEST_COMPANION_SELF_MANAGED = "android.permission.REQUEST_COMPANION_SELF_MANAGED"; @@ -2483,7 +2484,6 @@ package android { field public static final int primary = 16908300; // 0x102000c field public static final int progress = 16908301; // 0x102000d field public static final int redo = 16908339; // 0x1020033 - field @FlaggedApi("android.appwidget.flags.engagement_metrics") public static final int remoteViewsMetricsId; field public static final int replaceText = 16908340; // 0x1020034 field public static final int secondaryProgress = 16908303; // 0x102000f field public static final int selectAll = 16908319; // 0x102001f @@ -10086,7 +10086,7 @@ package android.companion { field @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_PROFILE_COMPUTER) public static final String DEVICE_PROFILE_COMPUTER = "android.app.role.COMPANION_DEVICE_COMPUTER"; field @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_PROFILE_GLASSES) public static final String DEVICE_PROFILE_GLASSES = "android.app.role.COMPANION_DEVICE_GLASSES"; field @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_PROFILE_NEARBY_DEVICE_STREAMING) public static final String DEVICE_PROFILE_NEARBY_DEVICE_STREAMING = "android.app.role.COMPANION_DEVICE_NEARBY_DEVICE_STREAMING"; - field @FlaggedApi("android.companion.virtualdevice.flags.enable_limited_vdm_role") @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_PROFILE_SENSOR_DEVICE_STREAMING) public static final String DEVICE_PROFILE_SENSOR_DEVICE_STREAMING = "android.app.role.COMPANION_DEVICE_SENSOR_DEVICE_STREAMING"; + field @FlaggedApi("android.companion.virtualdevice.flags.enable_limited_vdm_role") @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_PROFILE_VIRTUAL_DEVICE) public static final String DEVICE_PROFILE_VIRTUAL_DEVICE = "android.app.role.COMPANION_DEVICE_VIRTUAL_DEVICE"; field public static final String DEVICE_PROFILE_WATCH = "android.app.role.COMPANION_DEVICE_WATCH"; } @@ -12367,6 +12367,7 @@ package android.content.om { method @NonNull public void setResourceValue(@NonNull String, @NonNull android.os.ParcelFileDescriptor, @Nullable String); method @FlaggedApi("android.content.res.asset_file_descriptor_frro") @NonNull public void setResourceValue(@NonNull String, @NonNull android.content.res.AssetFileDescriptor, @Nullable String); method @FlaggedApi("android.content.res.dimension_frro") public void setResourceValue(@NonNull String, float, int, @Nullable String); + method @FlaggedApi("android.content.res.dimension_frro") public void setResourceValue(@NonNull String, float, @Nullable String); method public void setTargetOverlayable(@Nullable String); } @@ -27223,12 +27224,34 @@ package android.media.projection { method public void onStop(); } + @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public final class MediaProjectionAppContent implements android.os.Parcelable { + ctor public MediaProjectionAppContent(@NonNull android.graphics.Bitmap, @NonNull CharSequence, int); + method public int describeContents(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.media.projection.MediaProjectionAppContent> CREATOR; + } + public final class MediaProjectionConfig implements android.os.Parcelable { method @NonNull public static android.media.projection.MediaProjectionConfig createConfigForDefaultDisplay(); method @NonNull public static android.media.projection.MediaProjectionConfig createConfigForUserChoice(); method public int describeContents(); + method @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public int getInitiallySelectedSource(); + method @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public int getProjectionSources(); + method @FlaggedApi("com.android.media.projection.flags.app_content_sharing") @Nullable public CharSequence getRequesterHint(); + method @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public boolean isSourceEnabled(int); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.media.projection.MediaProjectionConfig> CREATOR; + field @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public static final int PROJECTION_SOURCE_APP = 8; // 0x8 + field @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public static final int PROJECTION_SOURCE_APP_CONTENT = 16; // 0x10 + field @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public static final int PROJECTION_SOURCE_DISPLAY = 2; // 0x2 + } + + @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public static final class MediaProjectionConfig.Builder { + ctor public MediaProjectionConfig.Builder(); + method @NonNull public android.media.projection.MediaProjectionConfig build(); + method @NonNull public android.media.projection.MediaProjectionConfig.Builder setInitiallySelectedSource(int); + method @NonNull public android.media.projection.MediaProjectionConfig.Builder setRequesterHint(@Nullable String); + method @NonNull public android.media.projection.MediaProjectionConfig.Builder setSourceEnabled(int, boolean); } public final class MediaProjectionManager { @@ -44051,6 +44074,7 @@ package android.telecom { field public static final String EVENT_CALL_PULL_FAILED = "android.telecom.event.CALL_PULL_FAILED"; field public static final String EVENT_CALL_REMOTELY_HELD = "android.telecom.event.CALL_REMOTELY_HELD"; field public static final String EVENT_CALL_REMOTELY_UNHELD = "android.telecom.event.CALL_REMOTELY_UNHELD"; + field @FlaggedApi("com.android.server.telecom.flags.call_sequencing_call_resume_failed") public static final String EVENT_CALL_RESUME_FAILED = "android.telecom.event.CALL_RESUME_FAILED"; field public static final String EVENT_CALL_SWITCH_FAILED = "android.telecom.event.CALL_SWITCH_FAILED"; field public static final String EVENT_MERGE_COMPLETE = "android.telecom.event.MERGE_COMPLETE"; field public static final String EVENT_MERGE_START = "android.telecom.event.MERGE_START"; @@ -56114,6 +56138,7 @@ package android.view { @FlaggedApi("android.xr.xr_manifest_entries") public final class XrWindowProperties { field @FlaggedApi("android.xr.xr_manifest_entries") public static final String PROPERTY_XR_ACTIVITY_START_MODE = "android.window.PROPERTY_XR_ACTIVITY_START_MODE"; field @FlaggedApi("android.xr.xr_manifest_entries") public static final String PROPERTY_XR_BOUNDARY_TYPE_RECOMMENDED = "android.window.PROPERTY_XR_BOUNDARY_TYPE_RECOMMENDED"; + field @FlaggedApi("android.xr.xr_manifest_entries") public static final String PROPERTY_XR_USES_CUSTOM_FULL_SPACE_MANAGED_ANIMATION = "android.window.PROPERTY_XR_USES_CUSTOM_FULL_SPACE_MANAGED_ANIMATION"; field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_ACTIVITY_START_MODE_FULL_SPACE_MANAGED = "XR_ACTIVITY_START_MODE_FULL_SPACE_MANAGED"; field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_ACTIVITY_START_MODE_FULL_SPACE_UNMANAGED = "XR_ACTIVITY_START_MODE_FULL_SPACE_UNMANAGED"; field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_ACTIVITY_START_MODE_HOME_SPACE = "XR_ACTIVITY_START_MODE_HOME_SPACE"; @@ -61755,6 +61780,7 @@ package android.widget { method public void setTextViewText(@IdRes int, CharSequence); method public void setTextViewTextSize(@IdRes int, int, float); method public void setUri(@IdRes int, String, android.net.Uri); + method @FlaggedApi("android.appwidget.flags.engagement_metrics") public void setUsageEventTag(@IdRes int, int); method public void setViewLayoutHeight(@IdRes int, float, int); method public void setViewLayoutHeightAttr(@IdRes int, @AttrRes int); method public void setViewLayoutHeightDimen(@IdRes int, @DimenRes int); diff --git a/core/api/lint-baseline.txt b/core/api/lint-baseline.txt index 577113b80d84..3895a512abc7 100644 --- a/core/api/lint-baseline.txt +++ b/core/api/lint-baseline.txt @@ -1,4 +1,3 @@ - // Baseline format: 1.0 BroadcastBehavior: android.app.AlarmManager#ACTION_NEXT_ALARM_CLOCK_CHANGED: Field 'ACTION_NEXT_ALARM_CLOCK_CHANGED' is missing @BroadcastBehavior @@ -244,6 +243,8 @@ BroadcastBehavior: android.telephony.TelephonyManager#ACTION_SUBSCRIPTION_SPECIF Field 'ACTION_SUBSCRIPTION_SPECIFIC_CARRIER_IDENTITY_CHANGED' is missing @BroadcastBehavior BroadcastBehavior: android.telephony.euicc.EuiccManager#ACTION_NOTIFY_CARRIER_SETUP_INCOMPLETE: Field 'ACTION_NOTIFY_CARRIER_SETUP_INCOMPLETE' is missing @BroadcastBehavior + + DeprecationMismatch: android.accounts.AccountManager#newChooseAccountIntent(android.accounts.Account, java.util.ArrayList<android.accounts.Account>, String[], boolean, String, String, String[], android.os.Bundle): Method android.accounts.AccountManager.newChooseAccountIntent(android.accounts.Account, java.util.ArrayList<android.accounts.Account>, String[], boolean, String, String, String[], android.os.Bundle): @Deprecated annotation (present) and @deprecated doc tag (not present) do not match DeprecationMismatch: android.app.Activity#enterPictureInPictureMode(): @@ -380,6 +381,8 @@ DeprecationMismatch: android.webkit.WebViewDatabase#hasFormData(): Method android.webkit.WebViewDatabase.hasFormData(): @Deprecated annotation (present) and @deprecated doc tag (not present) do not match DeprecationMismatch: javax.microedition.khronos.egl.EGL10#eglCreatePixmapSurface(javax.microedition.khronos.egl.EGLDisplay, javax.microedition.khronos.egl.EGLConfig, Object, int[]): Method javax.microedition.khronos.egl.EGL10.eglCreatePixmapSurface(javax.microedition.khronos.egl.EGLDisplay, javax.microedition.khronos.egl.EGLConfig, Object, int[]): @Deprecated annotation (present) and @deprecated doc tag (not present) do not match + + FlaggedApiLiteral: android.Manifest.permission#BIND_APP_FUNCTION_SERVICE: @FlaggedApi contains a string literal, but should reference the field generated by aconfig (android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER). FlaggedApiLiteral: android.Manifest.permission#BIND_TV_AD_SERVICE: @@ -390,6 +393,8 @@ FlaggedApiLiteral: android.Manifest.permission#QUERY_ADVANCED_PROTECTION_MODE: @FlaggedApi contains a string literal, but should reference the field generated by aconfig (android.security.Flags.FLAG_AAPM_API). FlaggedApiLiteral: android.Manifest.permission#RANGING: @FlaggedApi contains a string literal, but should reference the field generated by aconfig (android.permission.flags.Flags.FLAG_RANGING_PERMISSION_ENABLED). +FlaggedApiLiteral: android.Manifest.permission#READ_UPDATE_ENGINE_LOGS: + @FlaggedApi contains a string literal, but should reference the field generated by aconfig (com.android.update_engine.Flags.FLAG_MINOR_CHANGES_2025Q4, however this flag doesn't seem to exist). FlaggedApiLiteral: android.Manifest.permission#REQUEST_OBSERVE_DEVICE_UUID_PRESENCE: @FlaggedApi contains a string literal, but should reference the field generated by aconfig (android.companion.Flags.FLAG_DEVICE_PRESENCE). FlaggedApiLiteral: android.R.attr#adServiceTypes: @@ -1110,10 +1115,14 @@ RequiresPermission: android.webkit.WebSettings#setBlockNetworkLoads(boolean): Method 'setBlockNetworkLoads' documentation mentions permissions without declaring @RequiresPermission RequiresPermission: android.webkit.WebSettings#setGeolocationEnabled(boolean): Method 'setGeolocationEnabled' documentation mentions permissions without declaring @RequiresPermission + + Todo: android.hardware.camera2.params.StreamConfigurationMap: Documentation mentions 'TODO' Todo: android.provider.ContactsContract.RawContacts#newEntityIterator(android.database.Cursor): Documentation mentions 'TODO' + + UnflaggedApi: android.R.color#on_surface_disabled_material: New API must be flagged with @FlaggedApi: field android.R.color.on_surface_disabled_material UnflaggedApi: android.R.color#outline_disabled_material: diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 42c60b0ba0da..a0547411cd9e 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -2950,6 +2950,7 @@ package android.app.supervision { @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") public class SupervisionManager { method @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public android.content.Intent createConfirmSupervisionCredentialsIntent(); method @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isSupervisionEnabled(); + method @FlaggedApi("android.permission.flags.enable_system_supervision_role_behavior") @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public boolean shouldAllowBypassingSupervisionRoleQualification(); } } @@ -18977,10 +18978,8 @@ package android.view { public static class WindowManager.LayoutParams extends android.view.ViewGroup.LayoutParams implements android.os.Parcelable { method public final long getUserActivityTimeout(); - method @FlaggedApi("com.android.hardware.input.override_power_key_behavior_in_focused_window") @RequiresPermission(android.Manifest.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) public boolean isReceivePowerKeyDoublePressEnabled(); method public boolean isSystemApplicationOverlay(); method @FlaggedApi("android.companion.virtualdevice.flags.status_bar_and_insets") public void setInsetsParams(@NonNull java.util.List<android.view.WindowManager.InsetsParams>); - method @FlaggedApi("com.android.hardware.input.override_power_key_behavior_in_focused_window") @RequiresPermission(android.Manifest.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) public void setReceivePowerKeyDoublePressEnabled(boolean); method @RequiresPermission(android.Manifest.permission.SYSTEM_APPLICATION_OVERLAY) public void setSystemApplicationOverlay(boolean); method public final void setUserActivityTimeout(long); field @RequiresPermission(android.Manifest.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS) public static final int SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS = 524288; // 0x80000 diff --git a/core/api/test-current.txt b/core/api/test-current.txt index daa1902edf02..1e21991cd380 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -369,7 +369,7 @@ package android.app { } public final class NotificationChannel implements android.os.Parcelable { - method @FlaggedApi("android.service.notification.notification_conversation_channel_management") @NonNull public android.app.NotificationChannel copy(); + method @NonNull public android.app.NotificationChannel copy(); method public int getOriginalImportance(); method public boolean isImportanceLockedByCriticalDeviceFunction(); method public void lockFields(int); diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index d5df48a2ea22..c129fde3f819 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -3174,6 +3174,15 @@ public class Activity extends ContextThemeWrapper throw new IllegalArgumentException("Expected non-null picture-in-picture params"); } if (!mCanEnterPictureInPicture) { + if (isTvImplicitEnterPipProhibited()) { + // Don't throw exception on TV so that apps don't crash when not adapted to new + // restrictions. + Log.e(TAG, + "Activity must be resumed to enter picture-in-picture and not about to be" + + " paused. Implicit app entry is only permitted on TV if android" + + ".permission.TV_IMPLICIT_ENTER_PIP is held by the app."); + return false; + } throw new IllegalStateException("Activity must be resumed to enter" + " picture-in-picture"); } @@ -3212,7 +3221,7 @@ public class Activity extends ContextThemeWrapper return ActivityTaskManager.getMaxNumPictureInPictureActions(this); } - private boolean isImplicitEnterPipProhibited() { + private boolean isTvImplicitEnterPipProhibited() { PackageManager pm = getPackageManager(); if (android.app.Flags.enableTvImplicitEnterPipRestriction()) { return pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK) @@ -9346,7 +9355,7 @@ public class Activity extends ContextThemeWrapper + mComponent.getClassName()); } - if (isImplicitEnterPipProhibited()) { + if (isTvImplicitEnterPipProhibited()) { mCanEnterPictureInPicture = false; } @@ -9376,7 +9385,7 @@ public class Activity extends ContextThemeWrapper final void performUserLeaving() { onUserInteraction(); - if (isImplicitEnterPipProhibited()) { + if (isTvImplicitEnterPipProhibited()) { mCanEnterPictureInPicture = false; } onUserLeaveHint(); diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index f44c2305591d..4987624be719 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -40,6 +40,7 @@ import static android.window.ConfigurationHelper.isDifferentDisplay; import static android.window.ConfigurationHelper.shouldUpdateResources; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; +import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; import static com.android.internal.os.SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL; import android.annotation.NonNull; @@ -116,6 +117,7 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.HardwareRenderer; import android.graphics.Typeface; +import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManagerGlobal; import android.media.MediaFrameworkInitializer; import android.media.MediaFrameworkPlatformInitializer; @@ -863,7 +865,8 @@ public final class ActivityThread extends ClientTransactionHandler } } - static final class ReceiverData extends BroadcastReceiver.PendingResult { + @VisibleForTesting(visibility = PACKAGE) + public static final class ReceiverData extends BroadcastReceiver.PendingResult { public ReceiverData(Intent intent, int resultCode, String resultData, Bundle resultExtras, boolean ordered, boolean sticky, boolean assumeDelivered, IBinder token, int sendingUser, int sendingUid, String sendingPackage) { @@ -871,6 +874,11 @@ public final class ActivityThread extends ClientTransactionHandler assumeDelivered, token, sendingUser, intent.getFlags(), sendingUid, sendingPackage); this.intent = intent; + if (com.android.window.flags.Flags.supportWidgetIntentsOnConnectedDisplay()) { + mOptions = ActivityOptions.fromBundle(resultExtras); + } else { + mOptions = null; + } } @UnsupportedAppUsage @@ -879,12 +887,16 @@ public final class ActivityThread extends ClientTransactionHandler ActivityInfo info; @UnsupportedAppUsage CompatibilityInfo compatInfo; + @Nullable + final ActivityOptions mOptions; + public String toString() { return "ReceiverData{intent=" + intent + " packageName=" + info.packageName + " resultCode=" + getResultCode() + " resultData=" + getResultData() + " resultExtras=" + getResultExtras(false) + " sentFromUid=" - + getSentFromUid() + " sentFromPackage=" + getSentFromPackage() + "}"; + + getSentFromUid() + " sentFromPackage=" + getSentFromPackage() + + " mOptions=" + mOptions + "}"; } } @@ -4985,6 +4997,7 @@ public final class ActivityThread extends ClientTransactionHandler final String attributionTag = data.info.attributionTags[0]; context = (ContextImpl) context.createAttributionContext(attributionTag); } + context = (ContextImpl) createDisplayContextIfNeeded(context, data); java.lang.ClassLoader cl = context.getClassLoader(); data.intent.setExtrasClassLoader(cl); data.intent.prepareToEnterProcess( @@ -5033,6 +5046,54 @@ public final class ActivityThread extends ClientTransactionHandler } } + /** + * Creates a display context if the broadcast was initiated with a launch display ID. + * + * <p>When a broadcast initiates from a widget on a secondary display, the originating + * display ID is included as an extra in the intent. This is accomplished by + * {@link PendingIntentRecord#createSafeActivityOptionsBundle}, which transfers the launch + * display ID from ActivityOptions into the intent's extras bundle. This method checks for + * the presence of that extra and creates a display context associated with the initiated + * display if it exists. This ensures that when the {@link BroadcastReceiver} invokes + * {@link Context#startActivity(Intent)}, the activity is launched on the correct display. + * + * @param context The original context of the receiver. + * @param data The {@link ReceiverData} containing optional display information. + * @return A display context if applicable; otherwise the original context. + */ + @NonNull + @VisibleForTesting(visibility = PRIVATE) + public Context createDisplayContextIfNeeded(@NonNull Context context, + @NonNull ReceiverData data) { + if (!com.android.window.flags.Flags.supportWidgetIntentsOnConnectedDisplay()) { + return context; + } + + final ActivityOptions options = data.mOptions; + if (options == null) { + return context; + } + + final int launchDisplayId = options.getLaunchDisplayId(); + if (launchDisplayId == INVALID_DISPLAY) { + return context; + } + + final DisplayManager dm = context.getSystemService(DisplayManager.class); + if (dm == null) { + return context; + } + + final Display display = dm.getDisplay(launchDisplayId); + if (display == null) { + Slog.w(TAG, "Unable to create a display context for nonexistent display " + + launchDisplayId); + return context; + } + + return context.createDisplayContext(display); + } + // Instantiate a BackupAgent and tell it that it's alive private void handleCreateBackupAgent(CreateBackupAgentData data) { if (DEBUG_BACKUP) Slog.v(TAG, "handleCreateBackupAgent: " + data); diff --git a/core/java/android/app/FullscreenRequestHandler.java b/core/java/android/app/FullscreenRequestHandler.java index c78c66aa62c0..5529349dea70 100644 --- a/core/java/android/app/FullscreenRequestHandler.java +++ b/core/java/android/app/FullscreenRequestHandler.java @@ -18,6 +18,7 @@ package android.app; import static android.app.Activity.FULLSCREEN_MODE_REQUEST_EXIT; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import android.annotation.IntDef; import android.annotation.NonNull; @@ -27,6 +28,7 @@ import android.os.Bundle; import android.os.IBinder; import android.os.IRemoteCallback; import android.os.OutcomeReceiver; +import android.window.DesktopModeFlags; /** * @hide @@ -35,13 +37,15 @@ public class FullscreenRequestHandler { @IntDef(prefix = { "RESULT_" }, value = { RESULT_APPROVED, RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY, - RESULT_FAILED_NOT_TOP_FOCUSED + RESULT_FAILED_NOT_TOP_FOCUSED, + RESULT_FAILED_ALREADY_FULLY_EXPANDED }) public @interface RequestResult {} public static final int RESULT_APPROVED = 0; public static final int RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY = 1; public static final int RESULT_FAILED_NOT_TOP_FOCUSED = 2; + public static final int RESULT_FAILED_ALREADY_FULLY_EXPANDED = 3; public static final String REMOTE_CALLBACK_RESULT_KEY = "result"; @@ -87,6 +91,9 @@ public class FullscreenRequestHandler { case RESULT_FAILED_NOT_TOP_FOCUSED: e = new IllegalStateException("The window is not the top focused window."); break; + case RESULT_FAILED_ALREADY_FULLY_EXPANDED: + e = new IllegalStateException("The window is already fully expanded."); + break; default: callback.onResult(null); break; @@ -101,6 +108,12 @@ public class FullscreenRequestHandler { if (windowingMode != WINDOWING_MODE_FULLSCREEN) { return RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY; } + return RESULT_APPROVED; + } + if (DesktopModeFlags.ENABLE_REQUEST_FULLSCREEN_BUGFIX.isTrue() + && (windowingMode == WINDOWING_MODE_FULLSCREEN + || windowingMode == WINDOWING_MODE_MULTI_WINDOW)) { + return RESULT_FAILED_ALREADY_FULLY_EXPANDED; } return RESULT_APPROVED; } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 7c293cb9cb3b..521b70b599f6 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -1983,7 +1983,7 @@ public class Notification implements Parcelable * treatment. * @hide */ - public static final String EXTRA_IS_MAGIC = "android.extra.IS_MAGIC"; + public static final String EXTRA_IS_ANIMATED = "android.extra.IS_ANIMATED"; private final Bundle mExtras; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) @@ -3253,9 +3253,24 @@ public class Notification implements Parcelable * @hide */ public boolean hasTitle() { - return extras != null - && (!TextUtils.isEmpty(extras.getCharSequence(EXTRA_TITLE)) - || !TextUtils.isEmpty(extras.getCharSequence(EXTRA_TITLE_BIG))); + if (extras == null) { + return false; + } + // CallStyle notifications only use the other person's name as the title. + if (isStyle(CallStyle.class)) { + Person person = extras.getParcelable(EXTRA_CALL_PERSON, Person.class); + return person != null && !TextUtils.isEmpty(person.getName()); + } + // non-CallStyle notifications can use EXTRA_TITLE + if (!TextUtils.isEmpty(extras.getCharSequence(EXTRA_TITLE))) { + return true; + } + // BigTextStyle notifications first use EXTRA_TITLE_BIG + if (isStyle(BigTextStyle.class)) { + return !TextUtils.isEmpty(extras.getCharSequence(EXTRA_TITLE_BIG)); + } else { + return false; + } } /** @@ -3280,12 +3295,23 @@ public class Notification implements Parcelable */ @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) public boolean hasPromotableCharacteristics() { - return isColorizedRequested() - && isOngoingEvent() - && hasTitle() - && !isGroupSummary() - && !containsCustomViews() - && hasPromotableStyle(); + if (!isOngoingEvent() || isGroupSummary() || containsCustomViews() || !hasTitle()) { + return false; + } + // Only "Ongoing CallStyle" notifications are promotable without EXTRA_COLORIZED + if (isOngoingCallStyle()) { + return true; + } + return isColorizedRequested() && hasPromotableStyle(); + } + + /** Returns whether the notification is CallStyle.forOngoingCall(). */ + private boolean isOngoingCallStyle() { + if (!isStyle(CallStyle.class)) { + return false; + } + int callType = extras.getInt(EXTRA_CALL_TYPE, CallStyle.CALL_TYPE_UNKNOWN); + return callType == CallStyle.CALL_TYPE_ONGOING; } /** @@ -6096,6 +6122,21 @@ public class Notification implements Parcelable return mColors; } + private void updateHeaderBackgroundColor(RemoteViews contentView, + StandardTemplateParams p) { + if (!Flags.uiRichOngoing()) { + return; + } + if (isBackgroundColorized(p)) { + contentView.setInt(R.id.notification_header, "setBackgroundColor", + getBackgroundColor(p)); + } else { + // Clear it! + contentView.setInt(R.id.notification_header, "setBackgroundResource", + 0); + } + } + private void updateBackgroundColor(RemoteViews contentView, StandardTemplateParams p) { if (isBackgroundColorized(p)) { @@ -6900,7 +6941,7 @@ public class Notification implements Parcelable * @hide */ public RemoteViews makeNotificationGroupHeader() { - return makeNotificationHeader(mParams.reset() + return makeNotificationHeader(mParams.reset().disallowColorization() .viewType(StandardTemplateParams.VIEW_TYPE_GROUP_HEADER) .fillTextsFrom(this)); } @@ -6912,12 +6953,11 @@ public class Notification implements Parcelable * @param p the template params to inflate this with */ private RemoteViews makeNotificationHeader(StandardTemplateParams p) { - // Headers on their own are never colorized - p.disallowColorization(); RemoteViews header = new BuilderRemoteViews(mContext.getApplicationInfo(), getHeaderLayoutResource()); resetNotificationHeader(header); bindNotificationHeader(header, p); + updateHeaderBackgroundColor(header, p); if (Flags.notificationsRedesignTemplates() && (p.mViewType == StandardTemplateParams.VIEW_TYPE_MINIMIZED || p.mViewType == StandardTemplateParams.VIEW_TYPE_PUBLIC)) { @@ -7041,6 +7081,10 @@ public class Notification implements Parcelable savedBundle.getBoolean(EXTRA_SHOW_CHRONOMETER)); publicExtras.putBoolean(EXTRA_CHRONOMETER_COUNT_DOWN, savedBundle.getBoolean(EXTRA_CHRONOMETER_COUNT_DOWN)); + if (mN.isPromotedOngoing()) { + publicExtras.putBoolean(EXTRA_COLORIZED, + savedBundle.getBoolean(EXTRA_COLORIZED)); + } String appName = savedBundle.getString(EXTRA_SUBSTITUTE_APP_NAME); if (appName != null) { publicExtras.putString(EXTRA_SUBSTITUTE_APP_NAME, appName); @@ -7053,6 +7097,9 @@ public class Notification implements Parcelable if (isLowPriority) { params.highlightExpander(false); } + if (!mN.isPromotedOngoing()) { + params.disallowColorization(); + } view = makeNotificationHeader(params); view.setBoolean(R.id.notification_header, "setExpandOnlyOnButton", true); mN.extras = savedBundle; @@ -7072,7 +7119,7 @@ public class Notification implements Parcelable * @hide */ public RemoteViews makeLowPriorityContentView(boolean useRegularSubtext) { - StandardTemplateParams p = mParams.reset() + StandardTemplateParams p = mParams.reset().disallowColorization() .viewType(StandardTemplateParams.VIEW_TYPE_MINIMIZED) .highlightExpander(false) .fillTextsFrom(this); diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java index d88395331656..c1d80c93cfd6 100644 --- a/core/java/android/app/NotificationChannel.java +++ b/core/java/android/app/NotificationChannel.java @@ -508,7 +508,6 @@ public final class NotificationChannel implements Parcelable { /** @hide */ @TestApi @NonNull - @FlaggedApi(FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT) public NotificationChannel copy() { NotificationChannel copy = new NotificationChannel(mId, mName, mImportance); copy.setDescription(mDesc); diff --git a/core/java/android/app/NotificationChannelGroup.java b/core/java/android/app/NotificationChannelGroup.java index 92db8b329045..06b492c417d8 100644 --- a/core/java/android/app/NotificationChannelGroup.java +++ b/core/java/android/app/NotificationChannelGroup.java @@ -221,7 +221,10 @@ public final class NotificationChannelGroup implements Parcelable { * @hide */ public void setChannels(List<NotificationChannel> channels) { - mChannels = channels; + mChannels.clear(); + if (channels != null) { + mChannels.addAll(channels); + } } /** @@ -331,7 +334,9 @@ public final class NotificationChannelGroup implements Parcelable { NotificationChannelGroup cloned = new NotificationChannelGroup(getId(), getName()); cloned.setDescription(getDescription()); cloned.setBlocked(isBlocked()); - cloned.setChannels(getChannels()); + for (NotificationChannel c : mChannels) { + cloned.addChannel(c.copy()); + } cloned.lockFields(mUserLockedFields); return cloned; } diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 69e3ef9086d5..f24eb0a63b26 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -1317,10 +1317,16 @@ public class NotificationManager { */ public List<NotificationChannel> getNotificationChannels() { if (Flags.nmBinderPerfCacheChannels()) { - return mNotificationChannelListCache.query(new NotificationChannelQuery( - mContext.getOpPackageName(), - mContext.getPackageName(), - mContext.getUserId())); + List<NotificationChannel> channelList = mNotificationChannelListCache.query( + new NotificationChannelQuery(mContext.getOpPackageName(), + mContext.getPackageName(), mContext.getUserId())); + List<NotificationChannel> out = new ArrayList(); + if (channelList != null) { + for (NotificationChannel c : channelList) { + out.add(c.copy()); + } + } + return out; } else { INotificationManager service = service(); try { @@ -1343,7 +1349,7 @@ public class NotificationManager { } for (NotificationChannel channel : channels) { if (channelId.equals(channel.getId())) { - return channel; + return channel.copy(); } } return null; @@ -1364,12 +1370,12 @@ public class NotificationManager { for (NotificationChannel channel : channels) { if (conversationId.equals(channel.getConversationId()) && channelId.equals(channel.getParentChannelId())) { - return channel; + return channel.copy(); } else if (channelId.equals(channel.getId())) { parent = channel; } } - return parent; + return parent != null ? parent.copy() : null; } /** @@ -1405,8 +1411,9 @@ public class NotificationManager { new NotificationChannelQuery(pkgName, pkgName, mContext.getUserId())); Map<String, NotificationChannelGroup> groupHeaders = mNotificationChannelGroupsCache.query(pkgName); - return NotificationChannelGroupsHelper.getGroupWithChannels(channelGroupId, channelList, - groupHeaders, /* includeDeleted= */ false); + NotificationChannelGroup ncg = NotificationChannelGroupsHelper.getGroupWithChannels( + channelGroupId, channelList, groupHeaders, /* includeDeleted= */ false); + return ncg != null ? ncg.clone() : null; } else { INotificationManager service = service(); try { @@ -1428,8 +1435,14 @@ public class NotificationManager { new NotificationChannelQuery(pkgName, pkgName, mContext.getUserId())); Map<String, NotificationChannelGroup> groupHeaders = mNotificationChannelGroupsCache.query(pkgName); - return NotificationChannelGroupsHelper.getGroupsWithChannels(channelList, groupHeaders, - NotificationChannelGroupsHelper.Params.forAllGroups()); + List<NotificationChannelGroup> populatedGroupList = + NotificationChannelGroupsHelper.getGroupsWithChannels(channelList, groupHeaders, + NotificationChannelGroupsHelper.Params.forAllGroups()); + List<NotificationChannelGroup> out = new ArrayList<>(); + for (NotificationChannelGroup g : populatedGroupList) { + out.add(g.clone()); + } + return out; } else { INotificationManager service = service(); try { diff --git a/core/java/android/app/jank/JankTracker.java b/core/java/android/app/jank/JankTracker.java index e3f67811757c..a085701b006a 100644 --- a/core/java/android/app/jank/JankTracker.java +++ b/core/java/android/app/jank/JankTracker.java @@ -143,6 +143,13 @@ public class JankTracker { * stats */ public void mergeAppJankStats(AppJankStats appJankStats) { + if (appJankStats.getUid() != mAppUid) { + if (DEBUG) { + Log.d(DEBUG_KEY, "Reported JankStats AppUID does not match AppUID of " + + "enclosing activity."); + } + return; + } getHandler().post(new Runnable() { @Override public void run() { diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index d764c58b2b1e..7eda66e22a97 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -287,7 +287,7 @@ flag { name: "notif_entry_creation_time_use_elapsed_realtime" namespace: "systemui" description: "makes the notification entry expect its creation time to be elapsedRealtime, not uptimeMillis" - bug: "343631648" + bug: "389606876" metadata { purpose: PURPOSE_BUGFIX } diff --git a/core/java/android/app/supervision/ISupervisionManager.aidl b/core/java/android/app/supervision/ISupervisionManager.aidl index 2f67a8abcd17..801162f3cbd3 100644 --- a/core/java/android/app/supervision/ISupervisionManager.aidl +++ b/core/java/android/app/supervision/ISupervisionManager.aidl @@ -27,4 +27,6 @@ interface ISupervisionManager { boolean isSupervisionEnabledForUser(int userId); void setSupervisionEnabledForUser(int userId, boolean enabled); String getActiveSupervisionAppPackage(int userId); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS)") + boolean shouldAllowBypassingSupervisionRoleQualification(); } diff --git a/core/java/android/app/supervision/SupervisionManager.java b/core/java/android/app/supervision/SupervisionManager.java index 172ed2358a5d..76a789d3426f 100644 --- a/core/java/android/app/supervision/SupervisionManager.java +++ b/core/java/android/app/supervision/SupervisionManager.java @@ -19,6 +19,7 @@ package android.app.supervision; import static android.Manifest.permission.INTERACT_ACROSS_USERS; import static android.Manifest.permission.MANAGE_USERS; import static android.Manifest.permission.QUERY_USERS; +import static android.permission.flags.Flags.FLAG_ENABLE_SYSTEM_SUPERVISION_ROLE_BEHAVIOR; import android.annotation.FlaggedApi; import android.annotation.Nullable; @@ -193,4 +194,25 @@ public class SupervisionManager { } return null; } + + + /** + * @return {@code true} if bypassing the qualification is allowed for the specified role based + * on the current state of the device. + * + * @hide + */ + @SystemApi + @FlaggedApi(FLAG_ENABLE_SYSTEM_SUPERVISION_ROLE_BEHAVIOR) + @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) + public boolean shouldAllowBypassingSupervisionRoleQualification() { + if (mService != null) { + try { + return mService.shouldAllowBypassingSupervisionRoleQualification(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return false; + } } diff --git a/core/java/android/app/usage/IStorageStatsManager.aidl b/core/java/android/app/usage/IStorageStatsManager.aidl index b5036da33a95..390300360c8f 100644 --- a/core/java/android/app/usage/IStorageStatsManager.aidl +++ b/core/java/android/app/usage/IStorageStatsManager.aidl @@ -30,6 +30,7 @@ interface IStorageStatsManager { long getCacheBytes(String volumeUuid, String callingPackage); long getCacheQuotaBytes(String volumeUuid, int uid, String callingPackage); StorageStats queryStatsForPackage(String volumeUuid, String packageName, int userId, String callingPackage); + StorageStats queryArtManagedStats(String packageName, int userId, int uid); StorageStats queryStatsForUid(String volumeUuid, int uid, String callingPackage); StorageStats queryStatsForUser(String volumeUuid, int userId, String callingPackage); ExternalStorageStats queryExternalStatsForUser(String volumeUuid, int userId, String callingPackage); diff --git a/core/java/android/app/usage/StorageStats.java b/core/java/android/app/usage/StorageStats.java index 7bfaef4e9857..d01b423d67f2 100644 --- a/core/java/android/app/usage/StorageStats.java +++ b/core/java/android/app/usage/StorageStats.java @@ -22,7 +22,10 @@ import android.annotation.IntDef; import android.content.Context; import android.os.Parcel; import android.os.Parcelable; +import android.os.RemoteException; +import android.os.ServiceManager; import android.os.UserHandle; +import android.util.Log; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -34,6 +37,9 @@ import java.lang.annotation.RetentionPolicy; * @see StorageStatsManager */ public final class StorageStats implements Parcelable { + /** @hide */ public String packageName; + /** @hide */ public int userHandle; + /** @hide */ public int uid; /** @hide */ public long codeBytes; /** @hide */ public long dataBytes; /** @hide */ public long cacheBytes; @@ -130,6 +136,14 @@ public final class StorageStats implements Parcelable { @Retention(RetentionPolicy.SOURCE) public @interface AppDataType {} + private static final String TAG = "StorageStats"; + + /** + * artStatsFetched is only applicable when + * Flags.getAppArtManagedBytes() is true; + */ + private boolean artStatsFetched; + /** * Return the size of app. This includes {@code APK} files, optimized * compiler output, and unpacked native libraries. @@ -157,9 +171,9 @@ public final class StorageStats implements Parcelable { @FlaggedApi(Flags.FLAG_GET_APP_BYTES_BY_DATA_TYPE_API) public long getAppBytesByDataType(@AppDataType int dataType) { switch (dataType) { - case APP_DATA_TYPE_FILE_TYPE_DEXOPT_ARTIFACT: return dexoptBytes; - case APP_DATA_TYPE_FILE_TYPE_REFERENCE_PROFILE: return refProfBytes; - case APP_DATA_TYPE_FILE_TYPE_CURRENT_PROFILE: return curProfBytes; + case APP_DATA_TYPE_FILE_TYPE_DEXOPT_ARTIFACT: return getDexoptBytes(); + case APP_DATA_TYPE_FILE_TYPE_REFERENCE_PROFILE: return getRefProfBytes(); + case APP_DATA_TYPE_FILE_TYPE_CURRENT_PROFILE: return getCurProfBytes(); case APP_DATA_TYPE_FILE_TYPE_APK: return apkBytes; case APP_DATA_TYPE_LIB: return libBytes; case APP_DATA_TYPE_FILE_TYPE_DM: return dmBytes; @@ -215,6 +229,9 @@ public final class StorageStats implements Parcelable { /** {@hide} */ public StorageStats(Parcel in) { + this.packageName = in.readString8(); + this.userHandle = in.readInt(); + this.uid = in.readInt(); this.codeBytes = in.readLong(); this.dataBytes = in.readLong(); this.cacheBytes = in.readLong(); @@ -234,6 +251,9 @@ public final class StorageStats implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeString8(packageName); + dest.writeInt(userHandle); + dest.writeInt(uid); dest.writeLong(codeBytes); dest.writeLong(dataBytes); dest.writeLong(cacheBytes); @@ -257,4 +277,42 @@ public final class StorageStats implements Parcelable { return new StorageStats[size]; } }; + + private void getArtManagedStats() { + try { + IStorageStatsManager storageStatsManagerService; + // Fetch art stats only if it is not already fetched. + if (Flags.getAppArtManagedBytes() && !artStatsFetched) { + android.os.IBinder binder = ServiceManager.getService("storagestats"); + storageStatsManagerService = IStorageStatsManager.Stub.asInterface(binder); + + StorageStats newStats = + storageStatsManagerService.queryArtManagedStats(packageName, userHandle, uid); + + dexoptBytes = newStats.dexoptBytes; + curProfBytes = newStats.curProfBytes; + refProfBytes = newStats.refProfBytes; + + artStatsFetched = true; + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to get art stats", e); + e.rethrowFromSystemServer(); + } + } + + private long getDexoptBytes() { + getArtManagedStats(); + return dexoptBytes; + } + + private long getCurProfBytes() { + getArtManagedStats(); + return curProfBytes; + } + + private long getRefProfBytes() { + getArtManagedStats(); + return refProfBytes; + } } diff --git a/core/java/android/app/usage/flags.aconfig b/core/java/android/app/usage/flags.aconfig index 04c36867271c..520284993841 100644 --- a/core/java/android/app/usage/flags.aconfig +++ b/core/java/android/app/usage/flags.aconfig @@ -49,6 +49,16 @@ flag { } flag { + name: "get_app_art_managed_bytes" + namespace: "system_performance" + description: "Bug fixing flag for optional collection of app ART managed file stats" + bug: "395548922" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "disable_idle_check" namespace: "backstage_power" description: "disable idle check for USER_SYSTEM during boot up" diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java index b9b5c6a8bbc3..33326347fda0 100644 --- a/core/java/android/appwidget/AppWidgetHostView.java +++ b/core/java/android/appwidget/AppWidgetHostView.java @@ -16,10 +16,15 @@ package android.appwidget; +import static android.appwidget.flags.Flags.FLAG_ENGAGEMENT_METRICS; +import static android.appwidget.flags.Flags.engagementMetrics; + +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Activity; import android.app.ActivityOptions; +import android.app.PendingIntent; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.Context; @@ -38,6 +43,7 @@ import android.os.Build; import android.os.Bundle; import android.os.CancellationSignal; import android.os.Parcelable; +import android.util.ArraySet; import android.util.AttributeSet; import android.util.Log; import android.util.Pair; @@ -48,6 +54,7 @@ import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.AbsListView; import android.widget.Adapter; import android.widget.AdapterView; import android.widget.BaseAdapter; @@ -57,8 +64,11 @@ import android.widget.RemoteViews.InteractionHandler; import android.widget.RemoteViewsAdapter.RemoteAdapterConnectionCallback; import android.widget.TextView; +import com.android.internal.annotations.VisibleForTesting; + import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.concurrent.Executor; /** @@ -99,7 +109,8 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW int mViewMode = VIEW_MODE_NOINIT; // If true, we should not try to re-apply the RemoteViews on the next inflation. boolean mColorMappingChanged = false; - private InteractionHandler mInteractionHandler; + @NonNull + private InteractionLogger mInteractionLogger = new InteractionLogger(); private boolean mOnLightBackground; private SizeF mCurrentSize = null; private RemoteViews.ColorResources mColorResources = null; @@ -124,7 +135,7 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW */ public AppWidgetHostView(Context context, InteractionHandler handler) { this(context, android.R.anim.fade_in, android.R.anim.fade_out); - mInteractionHandler = getHandler(handler); + setInteractionHandler(handler); } /** @@ -145,13 +156,29 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW /** * Pass the given handler to RemoteViews when updating this widget. Unless this - * is done immediatly after construction, a call to {@link #updateAppWidget(RemoteViews)} + * is done immediately after construction, a call to {@link #updateAppWidget(RemoteViews)} * should be made. * * @hide */ public void setInteractionHandler(InteractionHandler handler) { - mInteractionHandler = getHandler(handler); + if (handler instanceof InteractionLogger logger) { + // Nested AppWidgetHostViews should reuse the parent logger instead of wrapping it. + mInteractionLogger = logger; + } else { + mInteractionLogger = new InteractionLogger(handler); + } + } + + /** + * Return the InteractionLogger used by this class. + * + * @hide + */ + @VisibleForTesting + @NonNull + public InteractionLogger getInteractionLogger() { + return mInteractionLogger; } /** @@ -588,7 +615,7 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW if (!mColorMappingChanged && rvToApply.canRecycleView(mView)) { try { - rvToApply.reapply(mContext, mView, mInteractionHandler, mCurrentSize, + rvToApply.reapply(mContext, mView, mInteractionLogger, mCurrentSize, mColorResources); content = mView; mLastInflatedRemoteViewsId = rvToApply.computeUniqueId(remoteViews); @@ -602,7 +629,7 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW // Try normal RemoteView inflation if (content == null) { try { - content = rvToApply.apply(mContext, this, mInteractionHandler, + content = rvToApply.apply(mContext, this, mInteractionLogger, mCurrentSize, mColorResources); mLastInflatedRemoteViewsId = rvToApply.computeUniqueId(remoteViews); if (LOGD) Log.d(TAG, "had to inflate new layout"); @@ -660,7 +687,7 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW mView, mAsyncExecutor, new ViewApplyListener(remoteViews, layoutId, true), - mInteractionHandler, + mInteractionLogger, mCurrentSize, mColorResources); } catch (Exception e) { @@ -672,7 +699,7 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW this, mAsyncExecutor, new ViewApplyListener(remoteViews, layoutId, false), - mInteractionHandler, + mInteractionLogger, mCurrentSize, mColorResources); } @@ -711,7 +738,7 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW AppWidgetHostView.this, mAsyncExecutor, new ViewApplyListener(mViews, mLayoutId, false), - mInteractionHandler, + mInteractionLogger, mCurrentSize); } else { applyContent(null, false, e); @@ -916,21 +943,6 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW return null; } - private InteractionHandler getHandler(InteractionHandler handler) { - return (view, pendingIntent, response) -> { - AppWidgetManager manager = AppWidgetManager.getInstance(mContext); - if (manager != null) { - manager.noteAppWidgetTapped(mAppWidgetId); - } - if (handler != null) { - return handler.onInteraction(view, pendingIntent, response); - } else { - return RemoteViews.startPendingIntent(view, pendingIntent, - response.getLaunchOptions(view)); - } - }; - } - /** * Set the dynamically overloaded color resources. * @@ -1016,4 +1028,83 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW post(this::handleViewError); } } + + /** + * This class is used to track user interactions with this widget. + * @hide + */ + public class InteractionLogger implements RemoteViews.InteractionHandler { + // Max number of clicked and scrolled IDs stored per impression. + public static final int MAX_NUM_ITEMS = 10; + // Clicked views + @NonNull + private final Set<Integer> mClickedIds = new ArraySet<>(MAX_NUM_ITEMS); + // Scrolled views + @NonNull + private final Set<Integer> mScrolledIds = new ArraySet<>(MAX_NUM_ITEMS); + @Nullable + private RemoteViews.InteractionHandler mInteractionHandler = null; + + InteractionLogger() { + } + + InteractionLogger(@Nullable InteractionHandler handler) { + mInteractionHandler = handler; + } + + @VisibleForTesting + @NonNull + public Set<Integer> getClickedIds() { + return mClickedIds; + } + + @VisibleForTesting + @NonNull + public Set<Integer> getScrolledIds() { + return mScrolledIds; + } + + @Override + public boolean onInteraction(View view, PendingIntent pendingIntent, + RemoteViews.RemoteResponse response) { + if (engagementMetrics() && mClickedIds.size() < MAX_NUM_ITEMS) { + mClickedIds.add(getMetricsId(view)); + } + AppWidgetManager manager = AppWidgetManager.getInstance(mContext); + if (manager != null) { + manager.noteAppWidgetTapped(mAppWidgetId); + } + + if (mInteractionHandler != null) { + return mInteractionHandler.onInteraction(view, pendingIntent, response); + } else { + return RemoteViews.startPendingIntent(view, pendingIntent, + response.getLaunchOptions(view)); + } + } + + @Override + public void onScroll(@NonNull AbsListView view) { + if (!engagementMetrics()) return; + + if (mScrolledIds.size() < MAX_NUM_ITEMS) { + mScrolledIds.add(getMetricsId(view)); + } + + if (mInteractionHandler != null) { + mInteractionHandler.onScroll(view); + } + } + + @FlaggedApi(FLAG_ENGAGEMENT_METRICS) + private int getMetricsId(@NonNull View view) { + int viewId = view.getId(); + Object metricsTag = view.getTag(com.android.internal.R.id.remoteViewsMetricsId); + if (metricsTag instanceof Integer tag) { + viewId = tag; + } + return viewId; + } + } } + diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java index b54e17beb100..52315d68afda 100644 --- a/core/java/android/appwidget/AppWidgetManager.java +++ b/core/java/android/appwidget/AppWidgetManager.java @@ -515,12 +515,13 @@ public class AppWidgetManager { /** * This bundle extra describes which views have been clicked during a single impression of the - * widget. It is an integer array of view IDs of the clicked views. + * widget. It is an integer array of view IDs of the clicked views. The array may contain up to + * 10 distinct IDs per event. * - * Widget providers may set a different ID for event purposes by setting the - * {@link android.R.id.remoteViewsMetricsId} int tag on the view. + * Widget providers may set a different ID for event logging by setting the usage event tag on + * the view with {@link RemoteViews#setUsageEventTag}. * - * @see android.views.RemoteViews.setIntTag + * @see android.widget.RemoteViews#setUsageEventTag */ @FlaggedApi(Flags.FLAG_ENGAGEMENT_METRICS) public static final String EXTRA_EVENT_CLICKED_VIEWS = @@ -528,12 +529,13 @@ public class AppWidgetManager { /** * This bundle extra describes which views have been scrolled during a single impression of the - * widget. It is an integer array of view IDs of the scrolled views. + * widget. It is an integer array of view IDs of the scrolled views. The array may contain up to + * 10 distinct IDs per event. * - * Widget providers may set a different ID for event purposes by setting the - * {@link android.R.id.remoteViewsMetricsId} int tag on the view. + * Widget providers may set a different ID for event logging by setting the usage event tag on + * the view with {@link RemoteViews#setUsageEventTag}. * - * @see android.views.RemoteViews.setIntTag + * @see android.widget.RemoteViews#setUsageEventTag */ @FlaggedApi(Flags.FLAG_ENGAGEMENT_METRICS) public static final String EXTRA_EVENT_SCROLLED_VIEWS = diff --git a/core/java/android/companion/AssociationRequest.java b/core/java/android/companion/AssociationRequest.java index 11e20e65d355..9641d7e69d4a 100644 --- a/core/java/android/companion/AssociationRequest.java +++ b/core/java/android/companion/AssociationRequest.java @@ -140,15 +140,15 @@ public final class AssociationRequest implements Parcelable { * IMU between an Android host and a nearby device. * <p> * Only applications that have been granted - * {@link android.Manifest.permission#REQUEST_COMPANION_PROFILE_SENSOR_DEVICE_STREAMING} + * {@link android.Manifest.permission#REQUEST_COMPANION_PROFILE_VIRTUAL_DEVICE} * are allowed to request to be associated with such devices. * * @see AssociationRequest.Builder#setDeviceProfile */ @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ENABLE_LIMITED_VDM_ROLE) - @RequiresPermission(Manifest.permission.REQUEST_COMPANION_PROFILE_SENSOR_DEVICE_STREAMING) - public static final String DEVICE_PROFILE_SENSOR_DEVICE_STREAMING = - "android.app.role.COMPANION_DEVICE_SENSOR_DEVICE_STREAMING"; + @RequiresPermission(Manifest.permission.REQUEST_COMPANION_PROFILE_VIRTUAL_DEVICE) + public static final String DEVICE_PROFILE_VIRTUAL_DEVICE = + "android.app.role.COMPANION_DEVICE_VIRTUAL_DEVICE"; /** * Device profile: Android Automotive Projection diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig index 161f05bc5139..c29f1528be89 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -154,7 +154,7 @@ flag { flag { namespace: "virtual_devices" name: "viewconfiguration_apis" - description: "APIs for settings ViewConfiguration attributes on virtual devices" + description: "APIs for setting ViewConfiguration attributes on virtual devices" bug: "370720522" is_exported: true } diff --git a/core/java/android/content/om/FabricatedOverlay.java b/core/java/android/content/om/FabricatedOverlay.java index 64e9c339f2d6..2f93adbb1e8c 100644 --- a/core/java/android/content/om/FabricatedOverlay.java +++ b/core/java/android/content/om/FabricatedOverlay.java @@ -490,6 +490,17 @@ public class FabricatedOverlay { return entry; } + @NonNull + private static FabricatedOverlayInternalEntry generateFabricatedOverlayInternalEntry( + @NonNull String resourceName, float value, @Nullable String configuration) { + final FabricatedOverlayInternalEntry entry = new FabricatedOverlayInternalEntry(); + entry.resourceName = resourceName; + entry.dataType = TypedValue.TYPE_FLOAT; + entry.data = Float.floatToIntBits(value); + entry.configuration = configuration; + return entry; + } + /** * Sets the resource value in the fabricated overlay for the integer-like types with the * configuration. @@ -621,4 +632,24 @@ public class FabricatedOverlay { mOverlay.entries.add(generateFabricatedOverlayInternalEntry(resourceName, dimensionValue, dimensionUnit, configuration)); } + + /** + * Sets the resource value in the fabricated overlay for the float type with the + * configuration. + * + * @param resourceName name of the target resource to overlay (in the form + * [package]:type/entry) + * @param value the float representing the new value + * @param configuration The string representation of the config this overlay is enabled for + * @throws IllegalArgumentException If the resource name is invalid + */ + @FlaggedApi(android.content.res.Flags.FLAG_DIMENSION_FRRO) + public void setResourceValue( + @NonNull String resourceName, + float value, + @Nullable String configuration) { + ensureValidResourceName(resourceName); + mOverlay.entries.add(generateFabricatedOverlayInternalEntry(resourceName, value, + configuration)); + } } diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index 3dbd5b239ae5..fefa8ab8f000 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -384,6 +384,16 @@ flag { is_fixed_read_only: true } +flag { + name: "require_pin_before_user_deletion" + namespace: "multiuser" + description: "Require credential authentication when a user tries to delete themselves or another user" + bug: "342395399" + metadata { + purpose: PURPOSE_BUGFIX + } +} + # This flag guards the private space feature and all its implementations excluding the APIs. APIs are guarded by android.os.Flags.allow_private_profile. flag { name: "enable_private_space_features" diff --git a/core/java/android/content/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java index f538e9ffffdd..3987f3abff0b 100644 --- a/core/java/android/content/res/ApkAssets.java +++ b/core/java/android/content/res/ApkAssets.java @@ -408,7 +408,7 @@ public final class ApkAssets { Objects.requireNonNull(fileName, "fileName"); synchronized (this) { long nativeXmlPtr = nativeOpenXml(mNativePtr, fileName); - try (XmlBlock block = new XmlBlock(null, nativeXmlPtr)) { + try (XmlBlock block = new XmlBlock(null, nativeXmlPtr, true)) { XmlResourceParser parser = block.newParser(); // If nativeOpenXml doesn't throw, it will always return a valid native pointer, // which makes newParser always return non-null. But let's be careful. diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java index bcb50881d327..008bf2f522c3 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -1190,7 +1190,7 @@ public final class AssetManager implements AutoCloseable { */ public @NonNull XmlResourceParser openXmlResourceParser(int cookie, @NonNull String fileName) throws IOException { - try (XmlBlock block = openXmlBlockAsset(cookie, fileName)) { + try (XmlBlock block = openXmlBlockAsset(cookie, fileName, true)) { XmlResourceParser parser = block.newParser(ID_NULL, new Validator()); // If openXmlBlockAsset doesn't throw, it will always return an XmlBlock object with // a valid native pointer, which makes newParser always return non-null. But let's @@ -1209,7 +1209,7 @@ public final class AssetManager implements AutoCloseable { * @hide */ @NonNull XmlBlock openXmlBlockAsset(@NonNull String fileName) throws IOException { - return openXmlBlockAsset(0, fileName); + return openXmlBlockAsset(0, fileName, true); } /** @@ -1218,9 +1218,11 @@ public final class AssetManager implements AutoCloseable { * * @param cookie Identifier of the package to be opened. * @param fileName Name of the asset to retrieve. + * @param usesFeatureFlags Whether the resources uses feature flags * @hide */ - @NonNull XmlBlock openXmlBlockAsset(int cookie, @NonNull String fileName) throws IOException { + @NonNull XmlBlock openXmlBlockAsset(int cookie, @NonNull String fileName, + boolean usesFeatureFlags) throws IOException { Objects.requireNonNull(fileName, "fileName"); synchronized (this) { ensureOpenLocked(); @@ -1229,7 +1231,8 @@ public final class AssetManager implements AutoCloseable { if (xmlBlock == 0) { throw new FileNotFoundException("Asset XML file: " + fileName); } - final XmlBlock block = new XmlBlock(this, xmlBlock); + + final XmlBlock block = new XmlBlock(this, xmlBlock, usesFeatureFlags); incRefsLocked(block.hashCode()); return block; } diff --git a/core/java/android/content/res/OWNERS b/core/java/android/content/res/OWNERS index 141d58d51353..f3394c3932ba 100644 --- a/core/java/android/content/res/OWNERS +++ b/core/java/android/content/res/OWNERS @@ -2,6 +2,7 @@ patb@google.com zyy@google.com -branliu@google.com +jakmcbane@google.com +markpun@google.com -per-file FontScaleConverter*=fuego@google.com
\ No newline at end of file +per-file FontScaleConverter*=fuego@google.com diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index 2658efab0e44..92f8bb4e005e 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -2568,7 +2568,7 @@ public class Resources { impl.getValue(id, value, true); if (value.type == TypedValue.TYPE_STRING) { return loadXmlResourceParser(value.string.toString(), id, - value.assetCookie, type); + value.assetCookie, type, value.usesFeatureFlags); } throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id) + " type #0x" + Integer.toHexString(value.type) + " is not valid"); @@ -2591,7 +2591,26 @@ public class Resources { @UnsupportedAppUsage XmlResourceParser loadXmlResourceParser(String file, int id, int assetCookie, String type) throws NotFoundException { - return mResourcesImpl.loadXmlResourceParser(file, id, assetCookie, type); + return loadXmlResourceParser(file, id, assetCookie, type, true); + } + + /** + * Loads an XML parser for the specified file. + * + * @param file the path for the XML file to parse + * @param id the resource identifier for the file + * @param assetCookie the asset cookie for the file + * @param type the type of resource (used for logging) + * @param usesFeatureFlags whether the xml has read/write feature flags + * @return a parser for the specified XML file + * @throws NotFoundException if the file could not be loaded + * @hide + */ + @NonNull + @VisibleForTesting + public XmlResourceParser loadXmlResourceParser(String file, int id, int assetCookie, + String type, boolean usesFeatureFlags) throws NotFoundException { + return mResourcesImpl.loadXmlResourceParser(file, id, assetCookie, type, usesFeatureFlags); } /** diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java index 8c76fd70afd9..6cbad2f0909b 100644 --- a/core/java/android/content/res/ResourcesImpl.java +++ b/core/java/android/content/res/ResourcesImpl.java @@ -276,7 +276,8 @@ public class ResourcesImpl { } @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs) + @VisibleForTesting + public void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs) throws NotFoundException { boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs); if (found) { @@ -1057,8 +1058,8 @@ public class ResourcesImpl { int id, int density, String file) throws IOException, XmlPullParserException { try ( - XmlResourceParser rp = - loadXmlResourceParser(file, id, value.assetCookie, "drawable") + XmlResourceParser rp = loadXmlResourceParser( + file, id, value.assetCookie, "drawable", value.usesFeatureFlags) ) { return Drawable.createFromXmlForDensity(wrapper, rp, density, null); } @@ -1092,7 +1093,7 @@ public class ResourcesImpl { try { if (file.endsWith("xml")) { final XmlResourceParser rp = loadXmlResourceParser( - file, id, value.assetCookie, "font"); + file, id, value.assetCookie, "font", value.usesFeatureFlags); final FontResourcesParser.FamilyResourceEntry familyEntry = FontResourcesParser.parse(rp, wrapper); if (familyEntry == null) { @@ -1286,7 +1287,7 @@ public class ResourcesImpl { if (file.endsWith(".xml")) { try { final XmlResourceParser parser = loadXmlResourceParser( - file, id, value.assetCookie, "ComplexColor"); + file, id, value.assetCookie, "ComplexColor", value.usesFeatureFlags); final AttributeSet attrs = Xml.asAttributeSet(parser); int type; @@ -1331,12 +1332,13 @@ public class ResourcesImpl { * @param id the resource identifier for the file * @param assetCookie the asset cookie for the file * @param type the type of resource (used for logging) + * @param usesFeatureFlags whether the xml has read/write feature flags * @return a parser for the specified XML file * @throws NotFoundException if the file could not be loaded */ @NonNull XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie, - @NonNull String type) + @NonNull String type, boolean usesFeatureFlags) throws NotFoundException { if (id != 0) { try { @@ -1355,7 +1357,8 @@ public class ResourcesImpl { // Not in the cache, create a new block and put it at // the next slot in the cache. - final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file); + final XmlBlock block = + mAssets.openXmlBlockAsset(assetCookie, file, usesFeatureFlags); if (block != null) { final int pos = (mLastCachedXmlBlockIndex + 1) % num; mLastCachedXmlBlockIndex = pos; diff --git a/core/java/android/content/res/XmlBlock.java b/core/java/android/content/res/XmlBlock.java index 36fa05905814..b27150b7171f 100644 --- a/core/java/android/content/res/XmlBlock.java +++ b/core/java/android/content/res/XmlBlock.java @@ -59,12 +59,14 @@ public final class XmlBlock implements AutoCloseable { mAssets = null; mNative = nativeCreate(data, 0, data.length); mStrings = new StringBlock(nativeGetStringBlock(mNative), false); + mUsesFeatureFlags = true; } public XmlBlock(byte[] data, int offset, int size) { mAssets = null; mNative = nativeCreate(data, offset, size); mStrings = new StringBlock(nativeGetStringBlock(mNative), false); + mUsesFeatureFlags = true; } @Override @@ -346,7 +348,8 @@ public final class XmlBlock implements AutoCloseable { if (ev == ERROR_BAD_DOCUMENT) { throw new XmlPullParserException("Corrupt XML binary file"); } - if (useLayoutReadwrite() && ev == START_TAG) { + + if (useLayoutReadwrite() && mUsesFeatureFlags && ev == START_TAG) { AconfigFlags flags = ParsingPackageUtils.getAconfigFlags(); if (flags.skipCurrentElement(/* pkg= */ null, this)) { int depth = 1; @@ -678,10 +681,11 @@ public final class XmlBlock implements AutoCloseable { * are doing! The given native object must exist for the entire lifetime * of this newly creating XmlBlock. */ - XmlBlock(@Nullable AssetManager assets, long xmlBlock) { + XmlBlock(@Nullable AssetManager assets, long xmlBlock, boolean usesFeatureFlags) { mAssets = assets; mNative = xmlBlock; mStrings = new StringBlock(nativeGetStringBlock(xmlBlock), false); + mUsesFeatureFlags = usesFeatureFlags; } private @Nullable final AssetManager mAssets; @@ -690,6 +694,8 @@ public final class XmlBlock implements AutoCloseable { private boolean mOpen = true; private int mOpenCount = 1; + private final boolean mUsesFeatureFlags; + private static final native long nativeCreate(byte[] data, int offset, int size); diff --git a/core/java/android/content/theming/FieldColor.java b/core/java/android/content/theming/FieldColor.java new file mode 100644 index 000000000000..a06a54f362b5 --- /dev/null +++ b/core/java/android/content/theming/FieldColor.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.theming; + +import android.annotation.ColorInt; +import android.annotation.FlaggedApi; +import android.graphics.Color; + +import androidx.annotation.Nullable; + +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.regex.Pattern; + +/** @hide */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public class FieldColor extends ThemeSettingsField<Integer, String> { + private static final Pattern COLOR_PATTERN = Pattern.compile("[0-9a-fA-F]{6,8}"); + + public FieldColor( + String key, + BiConsumer<ThemeSettingsUpdater, Integer> setter, + Function<ThemeSettings, Integer> getter, + ThemeSettings defaults + ) { + super(key, setter, getter, defaults); + } + + @Override + @ColorInt + @Nullable + public Integer parse(String primitive) { + if (primitive == null) { + return null; + } + if (!COLOR_PATTERN.matcher(primitive).matches()) { + return null; + } + + try { + return Color.valueOf(Color.parseColor("#" + primitive)).toArgb(); + } catch (IllegalArgumentException e) { + return null; + } + } + + @Override + public String serialize(@ColorInt Integer value) { + return Integer.toHexString(value); + } + + @Override + public boolean validate(Integer value) { + return !value.equals(Color.TRANSPARENT); + } + + @Override + public Class<Integer> getFieldType() { + return Integer.class; + } + + @Override + public Class<String> getJsonType() { + return String.class; + } +} diff --git a/core/java/android/content/theming/FieldColorBoth.java b/core/java/android/content/theming/FieldColorBoth.java new file mode 100644 index 000000000000..e4a9f7f716d8 --- /dev/null +++ b/core/java/android/content/theming/FieldColorBoth.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.theming; + +import android.annotation.FlaggedApi; + +import androidx.annotation.Nullable; + +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** @hide */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public class FieldColorBoth extends ThemeSettingsField<Boolean, String> { + public FieldColorBoth( + String key, + BiConsumer<ThemeSettingsUpdater, Boolean> setter, + Function<ThemeSettings, Boolean> getter, + ThemeSettings defaults + ) { + super(key, setter, getter, defaults); + } + + @Override + @Nullable + public Boolean parse(String primitive) { + return switch (primitive) { + case "1" -> true; + case "0" -> false; + default -> null; + }; + } + + @Override + public String serialize(Boolean typedValue) { + if (typedValue) return "1"; + return "0"; + } + + @Override + public boolean validate(Boolean value) { + Objects.requireNonNull(value); + return true; + } + + @Override + public Class<Boolean> getFieldType() { + return Boolean.class; + } + + @Override + public Class<String> getJsonType() { + return String.class; + } +} diff --git a/core/java/android/content/theming/FieldColorIndex.java b/core/java/android/content/theming/FieldColorIndex.java new file mode 100644 index 000000000000..683568a42318 --- /dev/null +++ b/core/java/android/content/theming/FieldColorIndex.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.theming; + +import android.annotation.FlaggedApi; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** @hide */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public class FieldColorIndex extends ThemeSettingsField<Integer, String> { + public FieldColorIndex( + String key, + BiConsumer<ThemeSettingsUpdater, Integer> setter, + Function<ThemeSettings, Integer> getter, + ThemeSettings defaults + ) { + super(key, setter, getter, defaults); + } + + @Override + public Integer parse(String primitive) { + try { + return Integer.parseInt(primitive); + } catch (NumberFormatException e) { + return null; + } + } + + @Override + public String serialize(Integer typedValue) { + return typedValue.toString(); + } + + @Override + public boolean validate(Integer value) { + return value >= -1; + } + + @Override + public Class<Integer> getFieldType() { + return Integer.class; + } + + @Override + public Class<String> getJsonType() { + return String.class; + } +} diff --git a/core/java/android/content/theming/FieldColorSource.java b/core/java/android/content/theming/FieldColorSource.java new file mode 100644 index 000000000000..1ff3aa64fda5 --- /dev/null +++ b/core/java/android/content/theming/FieldColorSource.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.theming; + +import android.annotation.FlaggedApi; +import android.annotation.StringDef; + +import androidx.annotation.Nullable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** @hide */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public class FieldColorSource extends ThemeSettingsField<String, String> { + public FieldColorSource( + String key, + BiConsumer<ThemeSettingsUpdater, String> setter, + Function<ThemeSettings, String> getter, + ThemeSettings defaults + ) { + super(key, setter, getter, defaults); + } + + @Override + @Nullable + @Type + public String parse(String primitive) { + return primitive; + } + + @Override + public String serialize(@Type String typedValue) { + return typedValue; + } + + @Override + public boolean validate(String value) { + return switch (value) { + case "preset", "home_wallpaper", "lock_wallpaper" -> true; + default -> false; + }; + } + + @Override + public Class<String> getFieldType() { + return String.class; + } + + @Override + public Class<String> getJsonType() { + return String.class; + } + + + @StringDef({"preset", "home_wallpaper", "lock_wallpaper"}) + @Retention(RetentionPolicy.SOURCE) + @interface Type { + } +} diff --git a/core/java/android/content/theming/FieldThemeStyle.java b/core/java/android/content/theming/FieldThemeStyle.java new file mode 100644 index 000000000000..b433e5b96ec3 --- /dev/null +++ b/core/java/android/content/theming/FieldThemeStyle.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.theming; + +import android.annotation.FlaggedApi; +import android.annotation.Nullable; + +import java.util.Arrays; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** @hide */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public class FieldThemeStyle extends ThemeSettingsField<Integer, String> { + public FieldThemeStyle( + String key, + BiConsumer<ThemeSettingsUpdater, Integer> setter, + Function<ThemeSettings, Integer> getter, + ThemeSettings defaults + ) { + super(key, setter, getter, defaults); + } + + private static final @ThemeStyle.Type List<Integer> sValidStyles = Arrays.asList( + ThemeStyle.EXPRESSIVE, + ThemeStyle.SPRITZ, + ThemeStyle.TONAL_SPOT, ThemeStyle.FRUIT_SALAD, ThemeStyle.RAINBOW, + ThemeStyle.VIBRANT, + ThemeStyle.MONOCHROMATIC); + + @Override + public String serialize(@ThemeStyle.Type Integer typedValue) { + return ThemeStyle.toString(typedValue); + } + + @Override + public boolean validate(Integer value) { + return sValidStyles.contains(value); + } + + @Override + @Nullable + @ThemeStyle.Type + public Integer parse(String primitive) { + try { + return ThemeStyle.valueOf(primitive); + } catch (Exception e) { + return null; + } + } + + @Override + public Class<Integer> getFieldType() { + return Integer.class; + } + + @Override + public Class<String> getJsonType() { + return String.class; + } +} diff --git a/core/java/android/content/theming/ThemeSettings.java b/core/java/android/content/theming/ThemeSettings.java new file mode 100644 index 000000000000..e94c1fef5382 --- /dev/null +++ b/core/java/android/content/theming/ThemeSettings.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.theming; + +import android.annotation.ColorInt; +import android.annotation.FlaggedApi; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import java.util.Objects; + +/** + * Represents the theme settings for the system. + * This class holds various properties related to theming, such as color indices, palettes, + * accent colors, color sources, theme styles, and color combinations. + * + * @hide + */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public final class ThemeSettings implements Parcelable { + private final int mColorIndex; + private final int mSystemPalette; + private final int mAccentColor; + @NonNull + private final String mColorSource; + private final int mThemeStyle; + private final boolean mColorBoth; + + /** + * Constructs a new ThemeSettings object. + * + * @param colorIndex The color index. + * @param systemPalette The system palette color. + * @param accentColor The accent color. + * @param colorSource The color source. + * @param themeStyle The theme style. + * @param colorBoth The color combination. + */ + + public ThemeSettings(int colorIndex, @ColorInt int systemPalette, + @ColorInt int accentColor, @NonNull String colorSource, int themeStyle, + boolean colorBoth) { + + this.mAccentColor = accentColor; + this.mColorBoth = colorBoth; + this.mColorIndex = colorIndex; + this.mColorSource = colorSource; + this.mSystemPalette = systemPalette; + this.mThemeStyle = themeStyle; + } + + /** + * Constructs a ThemeSettings object from a Parcel. + * + * @param in The Parcel to read from. + */ + ThemeSettings(Parcel in) { + this.mAccentColor = in.readInt(); + this.mColorBoth = in.readBoolean(); + this.mColorIndex = in.readInt(); + this.mColorSource = Objects.requireNonNullElse(in.readString8(), "s"); + this.mSystemPalette = in.readInt(); + this.mThemeStyle = in.readInt(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mAccentColor); + dest.writeBoolean(mColorBoth); + dest.writeInt(mColorIndex); + dest.writeString8(mColorSource); + dest.writeInt(mSystemPalette); + dest.writeInt(mThemeStyle); + } + + /** + * Gets the color index. + * + * @return The color index. + */ + public Integer colorIndex() { + return mColorIndex; + } + + /** + * Gets the system palette color. + * + * @return The system palette color. + */ + @ColorInt + public Integer systemPalette() { + return mSystemPalette; + } + + /** + * Gets the accent color. + * + * @return The accent color. + */ + @ColorInt + public Integer accentColor() { + return mAccentColor; + } + + /** + * Gets the color source. + * + * @return The color source. + */ + @FieldColorSource.Type + public String colorSource() { + return mColorSource; + } + + /** + * Gets the theme style. + * + * @return The theme style. + */ + @ThemeStyle.Type + public Integer themeStyle() { + return mThemeStyle; + } + + /** + * Gets the color combination. + * + * @return The color combination. + */ + public Boolean colorBoth() { + return mColorBoth; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + return obj instanceof ThemeSettings other + && mColorIndex == other.mColorIndex + && mSystemPalette == other.mSystemPalette + && mAccentColor == other.mAccentColor + && mColorSource.equals(other.mColorSource) + && mThemeStyle == other.mThemeStyle + && mColorBoth == other.mColorBoth; + } + + @Override + public int hashCode() { + return Objects.hash(mColorIndex, mSystemPalette, mAccentColor, mColorSource, mThemeStyle, + mColorBoth); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Creator for Parcelable interface. + */ + public static final Creator<ThemeSettings> CREATOR = new Creator<>() { + @Override + public ThemeSettings createFromParcel(Parcel in) { + return new ThemeSettings(in); + } + + @Override + public ThemeSettings[] newArray(int size) { + return new ThemeSettings[size]; + } + }; + + /** + * Creates a new {@link ThemeSettingsUpdater} instance for updating the {@link ThemeSettings} + * through the API. + * + * @return A new {@link ThemeSettingsUpdater} instance. + */ + public static ThemeSettingsUpdater updater() { + return new ThemeSettingsUpdater(); + } +} diff --git a/core/java/android/content/theming/ThemeSettingsField.java b/core/java/android/content/theming/ThemeSettingsField.java new file mode 100644 index 000000000000..1696df4ad0f6 --- /dev/null +++ b/core/java/android/content/theming/ThemeSettingsField.java @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.theming; + + +import android.annotation.FlaggedApi; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.internal.util.Preconditions; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * Represents a field within {@link ThemeSettings}, providing methods for parsing, serializing, + * managing default values, and validating the field's value. + * <p> + * This class is designed to be extended by concrete classes that represent specific fields within + * {@link ThemeSettings}. Each subclass should define the following methods, where T is the type of + * the field's value and J is the type of the field's value stored in JSON: + * <ul> + * <li>{@link #parse(Object)} to parse a JSON representation into the field's value type.</li> + * <li>{@link #serialize(Object)} to serialize the field's value into a JSON representation.</li> + * <li>{@link #validate(Object)} to validate the field's value.</li> + * <li>{@link #getFieldType()} to return the type of the field's value.</li> + * <li>{@link #getJsonType()} to return the type of the field's value stored in JSON.</li> + * </ul> + * <p> + * The {@link #fromJSON(JSONObject, ThemeSettingsUpdater)} and + * {@link #toJSON(ThemeSettings, JSONObject)} + * methods handle the extraction and serialization of the field's value to and from JSON objects + * respectively. The {@link #fallbackParse(Object, Object)} method is used to parse a string + * representation of the field's value, falling back to a default value if parsing fails. + * + * @param <T> The type of the field's value. + * @param <J> The type of the JSON property. + * @hide + */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public abstract class ThemeSettingsField<T, J> { + private static final String TAG = ThemeSettingsField.class.getSimpleName(); + + private static final String KEY_PREFIX = "android.theme.customization."; + public static final String OVERLAY_CATEGORY_ACCENT_COLOR = KEY_PREFIX + "accent_color"; + public static final String OVERLAY_CATEGORY_SYSTEM_PALETTE = KEY_PREFIX + "system_palette"; + public static final String OVERLAY_CATEGORY_THEME_STYLE = KEY_PREFIX + "theme_style"; + public static final String OVERLAY_COLOR_SOURCE = KEY_PREFIX + "color_source"; + public static final String OVERLAY_COLOR_INDEX = KEY_PREFIX + "color_index"; + public static final String OVERLAY_COLOR_BOTH = KEY_PREFIX + "color_both"; + + + /** + * Returns an array of all available {@link ThemeSettingsField} instances. + * + * @param defaults The default {@link ThemeSettings} object to use for default values. + * @return An array of {@link ThemeSettingsField} instances. + */ + public static ThemeSettingsField<?, ?>[] getFields(ThemeSettings defaults) { + return new ThemeSettingsField[]{ + new FieldColorIndex( + OVERLAY_COLOR_INDEX, + ThemeSettingsUpdater::colorIndex, + ThemeSettings::colorIndex, + defaults), + new FieldColor( + OVERLAY_CATEGORY_SYSTEM_PALETTE, + ThemeSettingsUpdater::systemPalette, + ThemeSettings::systemPalette, + defaults), + new FieldColor( + OVERLAY_CATEGORY_ACCENT_COLOR, + ThemeSettingsUpdater::accentColor, + ThemeSettings::accentColor, + defaults), + new FieldColorSource( + OVERLAY_COLOR_SOURCE, + ThemeSettingsUpdater::colorSource, + ThemeSettings::colorSource, + defaults), + new FieldThemeStyle( + OVERLAY_CATEGORY_THEME_STYLE, + ThemeSettingsUpdater::themeStyle, + ThemeSettings::themeStyle, + defaults), + new FieldColorBoth( + OVERLAY_COLOR_BOTH, + ThemeSettingsUpdater::colorBoth, + ThemeSettings::colorBoth, + defaults) + }; + } + + public final String key; + private final BiConsumer<ThemeSettingsUpdater, T> mSetter; + private final Function<ThemeSettings, T> mGetter; + private final ThemeSettings mDefaults; + + /** + * Creates a new {@link ThemeSettingsField}. + * + * @param key The key to identify the field in JSON objects. + * @param setter The setter to update the field's value in a {@link ThemeSettingsUpdater}. + * @param getter The getter to retrieve the field's value from a {@link ThemeSettings} + * object. + * @param defaults The default {@link ThemeSettings} object to provide default values. + */ + + public ThemeSettingsField( + String key, + BiConsumer<ThemeSettingsUpdater, T> setter, + Function<ThemeSettings, T> getter, + ThemeSettings defaults + ) { + this.key = key; + mSetter = setter; + mGetter = getter; + mDefaults = defaults; + } + + /** + * Attempts to parse a JSON primitive representation of the field's value. If parsing fails, it + * defaults to the field's default value. + * + * @param primitive The string representation to parse. + */ + private T fallbackParse(Object primitive, T fallbackValue) { + if (primitive == null) { + Log.w(TAG, "Error, field `" + key + "` was not found, defaulting to " + fallbackValue); + return fallbackValue; + } + + if (!getJsonType().isInstance(primitive)) { + Log.w(TAG, "Error, field `" + key + "` expected to be of type `" + + getJsonType().getSimpleName() + + "`, got `" + primitive.getClass().getSimpleName() + "`, defaulting to " + + fallbackValue); + return fallbackValue; + } + + // skips parsing if destination json type is already the same as field type + T parsedValue = getFieldType() == getJsonType() ? (T) primitive : parse((J) primitive); + + if (parsedValue == null) { + Log.w(TAG, "Error parsing JSON field `" + key + "` , defaulting to " + fallbackValue); + return fallbackValue; + } + + if (!validate(parsedValue)) { + Log.w(TAG, + "Error validating JSON field `" + key + "` , defaulting to " + fallbackValue); + return fallbackValue; + } + + if (parsedValue.getClass() != getFieldType()) { + Log.w(TAG, "Error: JSON field `" + key + "` expected to be of type `" + + getFieldType().getSimpleName() + + "`, defaulting to " + fallbackValue); + return fallbackValue; + } + + return parsedValue; + } + + + /** + * Extracts the field's value from a JSON object and sets it in a + * {@link ThemeSettingsUpdater}. + * + * @param source The JSON object containing the field's value. + */ + public void fromJSON(JSONObject source, ThemeSettingsUpdater updater) { + Object primitiveStr = source.opt(key); + T typedValue = fallbackParse(primitiveStr, getDefaultValue()); + mSetter.accept(updater, typedValue); + } + + /** + * Serializes the field's value from a {@link ThemeSettings} object into a JSON object. + * + * @param source The {@link ThemeSettings} object from which to retrieve the field's + * value. + * @param destination The JSON object to which the field's value will be added. + */ + public void toJSON(ThemeSettings source, JSONObject destination) { + T value = mGetter.apply(source); + Preconditions.checkState(value.getClass() == getFieldType()); + + J serialized; + if (validate(value)) { + serialized = serialize(value); + } else { + T fallbackValue = getDefaultValue(); + serialized = serialize(fallbackValue); + Log.w(TAG, "Invalid value `" + value + "` for key `" + key + "`, defaulting to '" + + fallbackValue); + } + + try { + destination.put(key, serialized); + } catch (JSONException e) { + Log.d(TAG, + "Error writing JSON primitive, skipping field " + key + ", " + e.getMessage()); + } + } + + + /** + * Returns the default value of the field. + * + * @return The default value. + */ + @VisibleForTesting + @NonNull + public T getDefaultValue() { + return mGetter.apply(mDefaults); + } + + /** + * Parses a string representation into the field's value type. + * + * @param primitive The string representation to parse. + * @return The parsed value, or null if parsing fails. + */ + @VisibleForTesting + @Nullable + public abstract T parse(J primitive); + + /** + * Serializes the field's value into a primitive type suitable for JSON. + * + * @param value The value to serialize. + * @return The serialized value. + */ + @VisibleForTesting + public abstract J serialize(T value); + + /** + * Validates the field's value. + * This method can be overridden to perform custom validation logic and MUST NOT validate for + * nullity. + * + * @param value The value to validate. + * @return {@code true} if the value is valid, {@code false} otherwise. + */ + @VisibleForTesting + public abstract boolean validate(T value); + + /** + * Returns the type of the field's value. + * + * @return The type of the field's value. + */ + @VisibleForTesting + public abstract Class<T> getFieldType(); + + /** + * Returns the type of the field's value stored in JSON. + * + * <p>This method is used to determine the expected type of the field's value when it is + * stored in a JSON object. + * + * @return The type of the field's value stored in JSON. + */ + @VisibleForTesting + public abstract Class<J> getJsonType(); +} diff --git a/core/java/android/content/theming/ThemeSettingsUpdater.java b/core/java/android/content/theming/ThemeSettingsUpdater.java new file mode 100644 index 000000000000..acd7d356db69 --- /dev/null +++ b/core/java/android/content/theming/ThemeSettingsUpdater.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.theming; + + +import android.annotation.ColorInt; +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.VisibleForTesting; + +import java.util.Objects; + +/** + * Updater class for constructing {@link ThemeSettings} objects. + * This class provides a fluent interface for setting the various properties of the theme + * settings. + * + * @hide + */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public class ThemeSettingsUpdater implements Parcelable { + @ColorInt + private Integer mAccentColor; + private Boolean mColorBoth; + private Integer mColorIndex; + private String mColorSource; + @ColorInt + private Integer mSystemPalette; + private Integer mThemeStyle; + + ThemeSettingsUpdater(Integer colorIndex, @ColorInt Integer systemPalette, + @ColorInt Integer accentColor, @FieldColorSource.Type String colorSource, + @ThemeStyle.Type Integer themeStyle, Boolean colorBoth) { + this.mAccentColor = accentColor; + this.mColorBoth = colorBoth; + this.mColorIndex = colorIndex; + this.mColorSource = colorSource; + this.mSystemPalette = systemPalette; + this.mThemeStyle = themeStyle; + } + + ThemeSettingsUpdater() { + } + + // only reading basic JVM types for nullability + @SuppressLint("ParcelClassLoader") + protected ThemeSettingsUpdater(Parcel in) { + mAccentColor = (Integer) in.readValue(null); + mColorBoth = (Boolean) in.readValue(null); + mColorIndex = (Integer) in.readValue(null); + mColorSource = (String) in.readValue(null); + mSystemPalette = (Integer) in.readValue(null); + mThemeStyle = (Integer) in.readValue(null); + } + + // using read/writeValue for nullability support + @SuppressWarnings("AndroidFrameworkEfficientParcelable") + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeValue(mAccentColor); + dest.writeValue(mColorBoth); + dest.writeValue(mColorIndex); + dest.writeValue(mColorSource); + dest.writeValue(mSystemPalette); + dest.writeValue(mThemeStyle); + } + + /** + * Sets the color index. + * + * @param colorIndex The color index to set. + * @return This {@link ThemeSettingsUpdater} instance. + */ + public ThemeSettingsUpdater colorIndex(int colorIndex) { + this.mColorIndex = colorIndex; + return this; + } + + /** + * Returns the color index. + * + * @return The color index. + */ + @VisibleForTesting + public Integer getColorIndex() { + return mColorIndex; + } + + /** + * Sets the system palette color. + * + * @param systemPalette The system palette color to set. + * @return This {@link ThemeSettingsUpdater} instance. + */ + public ThemeSettingsUpdater systemPalette(@ColorInt int systemPalette) { + this.mSystemPalette = systemPalette; + return this; + } + + /** + * Returns the system palette color. + * + * @return The system palette color. + */ + @VisibleForTesting + public Integer getSystemPalette() { + return mSystemPalette; + } + + /** + * Sets the accent color. + * + * @param accentColor The accent color to set. + * @return This {@link ThemeSettingsUpdater} instance. + */ + public ThemeSettingsUpdater accentColor(@ColorInt int accentColor) { + this.mAccentColor = accentColor; + return this; + } + + /** + * Returns the accent color. + * + * @return The accent color. + */ + @VisibleForTesting + public Integer getAccentColor() { + return mAccentColor; + } + + /** + * Sets the color source. + * + * @param colorSource The color source to set. + * @return This {@link ThemeSettingsUpdater} instance. + */ + public ThemeSettingsUpdater colorSource(@NonNull @FieldColorSource.Type String colorSource) { + this.mColorSource = colorSource; + return this; + } + + /** + * Returns the theme style. + * + * @return The theme style. + */ + @VisibleForTesting + public Integer getThemeStyle() { + return mThemeStyle; + } + + /** + * Sets the theme style. + * + * @param themeStyle The theme style to set. + * @return This {@link ThemeSettingsUpdater} instance. + */ + public ThemeSettingsUpdater themeStyle(@ThemeStyle.Type int themeStyle) { + this.mThemeStyle = themeStyle; + return this; + } + + /** + * Returns the color source. + * + * @return The color source. + */ + @VisibleForTesting + public String getColorSource() { + return mColorSource; + } + + /** + * Sets the color combination. + * + * @param colorBoth The color combination to set. + * @return This {@link ThemeSettingsUpdater} instance. + */ + public ThemeSettingsUpdater colorBoth(boolean colorBoth) { + this.mColorBoth = colorBoth; + return this; + } + + /** + * Returns the color combination. + * + * @return The color combination. + */ + @VisibleForTesting + public Boolean getColorBoth() { + return mColorBoth; + } + + /** + * Constructs a new {@link ThemeSettings} object with the current builder settings. + * + * @return A new {@link ThemeSettings} object. + */ + public ThemeSettings toThemeSettings(@NonNull ThemeSettings defaults) { + return new ThemeSettings( + Objects.requireNonNullElse(mColorIndex, defaults.colorIndex()), + Objects.requireNonNullElse(mSystemPalette, defaults.systemPalette()), + Objects.requireNonNullElse(mAccentColor, defaults.accentColor()), + Objects.requireNonNullElse(mColorSource, defaults.colorSource()), + Objects.requireNonNullElse(mThemeStyle, defaults.themeStyle()), + Objects.requireNonNullElse(mColorBoth, defaults.colorBoth())); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<ThemeSettingsUpdater> CREATOR = + new Creator<>() { + @Override + public ThemeSettingsUpdater createFromParcel(Parcel in) { + return new ThemeSettingsUpdater(in); + } + + @Override + public ThemeSettingsUpdater[] newArray(int size) { + return new ThemeSettingsUpdater[size]; + } + }; +} diff --git a/core/java/android/content/theming/ThemeStyle.java b/core/java/android/content/theming/ThemeStyle.java new file mode 100644 index 000000000000..607896405020 --- /dev/null +++ b/core/java/android/content/theming/ThemeStyle.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.theming; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A class defining the different styles available for theming. + * This class replaces the previous enum implementation for improved performance and compatibility. + * + * @hide + */ +public final class ThemeStyle { + + private ThemeStyle() { + } + + /** + * @hide + */ + @IntDef({ + SPRITZ, + TONAL_SPOT, + VIBRANT, + EXPRESSIVE, + RAINBOW, + FRUIT_SALAD, + CONTENT, + MONOCHROMATIC, + CLOCK, + CLOCK_VIBRANT + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Type { + } + + /** + * Represents the SPRITZ style. + */ + public static final int SPRITZ = 0; + /** + * Represents the TONAL_SPOT style. + */ + public static final int TONAL_SPOT = 1; + /** + * Represents the VIBRANT style. + */ + public static final int VIBRANT = 2; + /** + * Represents the EXPRESSIVE style. + */ + public static final int EXPRESSIVE = 3; + /** + * Represents the RAINBOW style. + */ + public static final int RAINBOW = 4; + /** + * Represents the FRUIT_SALAD style. + */ + public static final int FRUIT_SALAD = 5; + /** + * Represents the CONTENT style. + */ + public static final int CONTENT = 6; + /** + * Represents the MONOCHROMATIC style. + */ + public static final int MONOCHROMATIC = 7; + /** + * Represents the CLOCK style. + */ + public static final int CLOCK = 8; + /** + * Represents the CLOCK_VIBRANT style. + */ + public static final int CLOCK_VIBRANT = 9; + + + /** + * Returns the string representation of the given style. + * + * @param style The style value. + * @return The string representation of the style. + * @throws IllegalArgumentException if the style value is invalid. + */ + @NonNull + public static String toString(@Nullable @Type Integer style) { + // Throw an exception if style is null + if (style == null) { + throw new IllegalArgumentException("Invalid style value: null"); + } + + return switch (style) { + case SPRITZ -> "SPRITZ"; + case TONAL_SPOT -> "TONAL_SPOT"; + case VIBRANT -> "VIBRANT"; + case EXPRESSIVE -> "EXPRESSIVE"; + case RAINBOW -> "RAINBOW"; + case FRUIT_SALAD -> "FRUIT_SALAD"; + case CONTENT -> "CONTENT"; + case MONOCHROMATIC -> "MONOCHROMATIC"; + case CLOCK -> "CLOCK"; + case CLOCK_VIBRANT -> "CLOCK_VIBRANT"; + default -> throw new IllegalArgumentException("Invalid style value: " + style); + }; + } + + /** + * Returns the style value corresponding to the given style name. + * + * @param styleName The name of the style. + * @return The style value. + * @throws IllegalArgumentException if the style name is invalid. + */ + public static @Type int valueOf(@Nullable String styleName) { + return switch (styleName) { + case "SPRITZ" -> SPRITZ; + case "TONAL_SPOT" -> TONAL_SPOT; + case "VIBRANT" -> VIBRANT; + case "EXPRESSIVE" -> EXPRESSIVE; + case "RAINBOW" -> RAINBOW; + case "FRUIT_SALAD" -> FRUIT_SALAD; + case "CONTENT" -> CONTENT; + case "MONOCHROMATIC" -> MONOCHROMATIC; + case "CLOCK" -> CLOCK; + case "CLOCK_VIBRANT" -> CLOCK_VIBRANT; + default -> throw new IllegalArgumentException("Invalid style name: " + styleName); + }; + } + + /** + * Returns the name of the given style. This method is equivalent to {@link #toString(int)}. + * + * @param style The style value. + * @return The name of the style. + */ + @NonNull + public static String name(@Type int style) { + return toString(style); + } + + /** + * Returns an array containing all the style values. + * + * @return An array of all style values. + */ + public static int[] values() { + return new int[]{ + SPRITZ, + TONAL_SPOT, + VIBRANT, + EXPRESSIVE, + RAINBOW, + FRUIT_SALAD, + CONTENT, + MONOCHROMATIC, + CLOCK, + CLOCK_VIBRANT + }; + } +} diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java index bebca57125b6..42df43e4d436 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/android/hardware/display/DisplayManagerGlobal.java @@ -475,6 +475,7 @@ public final class DisplayManagerGlobal { synchronized (mLock) { if (!mShouldImplicitlyRegisterRrChanges) { mShouldImplicitlyRegisterRrChanges = true; + Slog.i(TAG, "Implicitly registering for refresh rate"); updateCallbackIfNeededLocked(); } } @@ -1759,6 +1760,9 @@ public final class DisplayManagerGlobal { synchronized (mLock) { mDispatchNativeCallbacks = true; if (Flags.delayImplicitRrRegistrationUntilRrAccessed()) { + if (!mShouldImplicitlyRegisterRrChanges) { + Slog.i(TAG, "Choreographer implicitly registered for the refresh rate."); + } mShouldImplicitlyRegisterRrChanges = true; } registerCallbackIfNeededLocked(); diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java index 9dd1fed4a85a..1249af7cc595 100644 --- a/core/java/android/hardware/input/KeyGestureEvent.java +++ b/core/java/android/hardware/input/KeyGestureEvent.java @@ -72,7 +72,8 @@ public final class KeyGestureEvent { public static final int KEY_GESTURE_TYPE_ALL_APPS = 21; public static final int KEY_GESTURE_TYPE_LAUNCH_SEARCH = 22; public static final int KEY_GESTURE_TYPE_LANGUAGE_SWITCH = 23; - public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS = 24; + @Deprecated + public static final int DEPRECATED_KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS = 24; public static final int KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK = 25; public static final int KEY_GESTURE_TYPE_SYSTEM_MUTE = 26; public static final int KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT = 27; @@ -167,7 +168,6 @@ public final class KeyGestureEvent { KEY_GESTURE_TYPE_ALL_APPS, KEY_GESTURE_TYPE_LAUNCH_SEARCH, KEY_GESTURE_TYPE_LANGUAGE_SWITCH, - KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS, KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, KEY_GESTURE_TYPE_SYSTEM_MUTE, KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT, @@ -525,8 +525,6 @@ public final class KeyGestureEvent { return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_SEARCH; case KEY_GESTURE_TYPE_LANGUAGE_SWITCH: return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LANGUAGE_SWITCH; - case KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS: - return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__ACCESSIBILITY_ALL_APPS; case KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK: return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_CAPS_LOCK; case KEY_GESTURE_TYPE_SYSTEM_MUTE: @@ -707,8 +705,6 @@ public final class KeyGestureEvent { return "KEY_GESTURE_TYPE_LAUNCH_SEARCH"; case KEY_GESTURE_TYPE_LANGUAGE_SWITCH: return "KEY_GESTURE_TYPE_LANGUAGE_SWITCH"; - case KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS: - return "KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS"; case KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK: return "KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK"; case KEY_GESTURE_TYPE_SYSTEM_MUTE: diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index c41a5ce02e61..3dfad5396634 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -214,6 +214,13 @@ flag { } flag { + name: "request_key_capture_api" + namespace: "input" + description: "Adds support for key capture APIs" + bug: "375435312" +} + +flag { name: "fix_search_modifier_fallbacks" namespace: "input" description: "Fixes a bug in which fallbacks from Search based key combinations were not activating." diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 84d96bd1e155..3d6da5452ad2 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -435,6 +435,11 @@ public class InputMethodService extends AbstractInputMethodService { } /** + * Cached value of {@link #canImeRenderGesturalNavButtons}, as it doesn't change at runtime. + */ + private final boolean mCanImeRenderGesturalNavButtons = canImeRenderGesturalNavButtons(); + + /** * Allows the system to optimize the back button affordance based on the presence of software * keyboard. * @@ -564,6 +569,9 @@ public class InputMethodService extends AbstractInputMethodService { private final NavigationBarController mNavigationBarController = new NavigationBarController(this); + /** Whether a custom IME Switcher button was requested to be visible. */ + private boolean mCustomImeSwitcherButtonRequestedVisible; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) int mTheme = 0; @@ -783,7 +791,7 @@ public class InputMethodService extends AbstractInputMethodService { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMS.initializeInternal"); mPrivOps.set(params.privilegedOperations); InputMethodPrivilegedOperationsRegistry.put(params.token, mPrivOps); - mNavigationBarController.onNavButtonFlagsChanged(params.navigationBarFlags); + onNavButtonFlagsChanged(params.navigationBarFlags); attachToken(params.token); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } @@ -893,7 +901,7 @@ public class InputMethodService extends AbstractInputMethodService { public final void dispatchStartInput(@Nullable InputConnection inputConnection, @NonNull IInputMethod.StartInputParams params) { mPrivOps.reportStartInputAsync(params.startInputToken); - mNavigationBarController.onNavButtonFlagsChanged(params.navigationBarFlags); + onNavButtonFlagsChanged(params.navigationBarFlags); if (params.restarting) { restartInput(inputConnection, params.editorInfo); } else { @@ -918,6 +926,20 @@ public class InputMethodService extends AbstractInputMethodService { @Override public void onNavButtonFlagsChanged(@InputMethodNavButtonFlags int navButtonFlags) { mNavigationBarController.onNavButtonFlagsChanged(navButtonFlags); + if (!mCanImeRenderGesturalNavButtons) { + final boolean showImeSwitcher = (navButtonFlags + & InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN) != 0; + // The IME cannot draw the IME nav bar, so this will never be visible. In this case + // the system nav bar hosts the IME buttons. + // The system nav bar will be hidden when the IME is shown and the config is set. + final boolean navBarNotVisible = getApplicationContext().getResources() + .getBoolean(com.android.internal.R.bool.config_hideNavBarForKeyboard); + final boolean visible = showImeSwitcher && navBarNotVisible; + if (visible != mCustomImeSwitcherButtonRequestedVisible) { + mCustomImeSwitcherButtonRequestedVisible = visible; + onCustomImeSwitcherButtonRequestedVisible(visible); + } + } } /** @@ -4473,28 +4495,27 @@ public class InputMethodService extends AbstractInputMethodService { /** * Called when the requested visibility of a custom IME Switcher button changes. * - * <p>When the system provides an IME navigation bar, it may decide to show an IME Switcher - * button inside this bar. However, the IME can request hiding the bar provided by the system - * with {@code getWindowInsetsController().hide(captionBar())} (the IME navigation bar provides - * {@link Type#captionBar() captionBar} insets to the IME window). If the request is successful, - * then it becomes the IME's responsibility to provide a custom IME Switcher button in its - * input view, with equivalent functionality.</p> + * <p>When this method is called with {@code true} by the system, the IME must show a button + * within its UI to switch IMEs. When it is called with {@code false}, it must hide this button. + * + * <p>Normally, the system provides a button for switching to a different IME when that is + * appropriate. Under certain circumstances, namely when the IME successfully asks to hide the + * system-provided navigation bar (with {@code getWindowInsetsController().hide(captionBar())}), + * providing this button is delegated to the IME through this callback. * - * <p>This custom button is only requested to be visible when the system provides the IME - * navigation bar, both the bar and the IME Switcher button inside it should be visible, - * but the IME successfully requested to hide the bar. This does not depend on the current - * visibility of the IME. It could be called with {@code true} while the IME is hidden, in - * which case the IME should prepare to show the button as soon as the IME itself is shown.</p> + * <p>This does not depend on the current visibility of the IME. It could be called with + * {@code true} while the IME is hidden, in which case the IME should prepare to show the button + * as soon as the IME itself is shown. * * <p>This is only called when the requested visibility changes. The default value is * {@code false} and as such, this will not be called initially if the resulting value is - * {@code false}.</p> + * {@code false}. * * <p>This can be called at any time after {@link #onCreate}, even if the IME is not currently - * visible. However, this is not guaranteed to be called before the IME is shown, as it depends - * on when the IME requested hiding the IME navigation bar. If the request is sent during - * the showing flow (e.g. during {@link #onStartInputView}), this will be called shortly after - * {@link #onWindowShown}, but before the first IME frame is drawn.</p> + * visible. However, this is not guaranteed to be called before the IME is shown, as it may + * depend on the IME requesting to hide the system-provided navigation bar. If the request is + * sent during the showing flow (e.g. during {@link #onStartInputView}), this will be called + * shortly after {@link #onWindowShown}, but before the first IME frame is drawn. * * @param visible whether the button is requested visible or not. */ @@ -4686,6 +4707,8 @@ public class InputMethodService extends AbstractInputMethodService { + " touchableRegion=" + mTmpInsets.touchableRegion); p.println(" mSettingsObserver=" + mSettingsObserver); p.println(" mNavigationBarController=" + mNavigationBarController.toDebugString()); + p.println(" mCustomImeSwitcherButtonRequestedVisible=" + + mCustomImeSwitcherButtonRequestedVisible); } private final ImeTracing.ServiceDumper mDumper = new ImeTracing.ServiceDumper() { diff --git a/core/java/android/inputmethodservice/NavigationBarController.java b/core/java/android/inputmethodservice/NavigationBarController.java index 7da053d0010e..f1dee89b0b1d 100644 --- a/core/java/android/inputmethodservice/NavigationBarController.java +++ b/core/java/android/inputmethodservice/NavigationBarController.java @@ -170,6 +170,9 @@ final class NavigationBarController { private boolean mShouldShowImeSwitcherWhenImeIsShown; + /** Whether a custom IME Switcher button should be visible. */ + private boolean mCustomImeSwitcherButtonRequestedVisible; + @Appearance private int mAppearance; @@ -181,9 +184,6 @@ final class NavigationBarController { private boolean mDrawLegacyNavigationBarBackground; - /** Whether a custom IME Switcher button should be visible. */ - private boolean mCustomImeSwitcherVisible; - private final Rect mTempRect = new Rect(); private final int[] mTempPos = new int[2]; @@ -275,7 +275,9 @@ final class NavigationBarController { // IME navigation bar. boolean visible = insets.isVisible(captionBar()); mNavigationBarFrame.setVisibility(visible ? View.VISIBLE : View.GONE); - checkCustomImeSwitcherVisibility(); + checkCustomImeSwitcherButtonRequestedVisible( + mShouldShowImeSwitcherWhenImeIsShown, mImeDrawsImeNavBar, + !visible /* imeNavBarNotVisible */); } return view.onApplyWindowInsets(insets); }); @@ -502,33 +504,31 @@ final class NavigationBarController { mShouldShowImeSwitcherWhenImeIsShown; mShouldShowImeSwitcherWhenImeIsShown = shouldShowImeSwitcherWhenImeIsShown; - checkCustomImeSwitcherVisibility(); - mService.mWindow.getWindow().getDecorView().getWindowInsetsController() .setImeCaptionBarInsetsHeight(getImeCaptionBarHeight(imeDrawsImeNavBar)); if (imeDrawsImeNavBar) { installNavigationBarFrameIfNecessary(); - if (mNavigationBarFrame == null) { - return; - } - if (mShouldShowImeSwitcherWhenImeIsShown - == prevShouldShowImeSwitcherWhenImeIsShown) { - return; - } - final NavigationBarView navigationBarView = mNavigationBarFrame.findViewByPredicate( - NavigationBarView.class::isInstance); - if (navigationBarView != null) { - // TODO(b/213337792): Support InputMethodService#setBackDisposition(). - // TODO(b/213337792): Set NAVBAR_IME_VISIBLE only when necessary. - final int flags = NAVBAR_BACK_DISMISS_IME | NAVBAR_IME_VISIBLE - | (mShouldShowImeSwitcherWhenImeIsShown - ? NAVBAR_IME_SWITCHER_BUTTON_VISIBLE : 0); - navigationBarView.setNavbarFlags(flags); + if (mNavigationBarFrame != null && mShouldShowImeSwitcherWhenImeIsShown + != prevShouldShowImeSwitcherWhenImeIsShown) { + final NavigationBarView navigationBarView = mNavigationBarFrame + .findViewByPredicate(NavigationBarView.class::isInstance); + if (navigationBarView != null) { + // TODO(b/213337792): Support InputMethodService#setBackDisposition(). + // TODO(b/213337792): Set NAVBAR_IME_VISIBLE only when necessary. + final int flags = NAVBAR_BACK_DISMISS_IME | NAVBAR_IME_VISIBLE + | (mShouldShowImeSwitcherWhenImeIsShown + ? NAVBAR_IME_SWITCHER_BUTTON_VISIBLE : 0); + navigationBarView.setNavbarFlags(flags); + } } } else { uninstallNavigationBarFrameIfNecessary(); } + + // Check custom IME Switcher button visibility after (un)installing nav bar frame. + checkCustomImeSwitcherButtonRequestedVisible(shouldShowImeSwitcherWhenImeIsShown, + imeDrawsImeNavBar, !isShown() /* imeNavBarNotVisible */); } @Override @@ -631,22 +631,29 @@ final class NavigationBarController { } /** - * Checks if a custom IME Switcher button should be visible, and notifies the IME when this - * state changes. This can only be {@code true} if three conditions are met: + * Checks if a custom IME Switcher button should be requested visible, and notifies the IME + * when this state changes. This is only {@code true} when the IME Switcher button is + * requested visible, and the navigation bar is not requested visible. * - * <li>The IME should draw the IME navigation bar.</li> - * <li>The IME Switcher button should be visible when the IME is visible.</li> - * <li>The IME navigation bar should be visible, but was requested hidden by the IME.</li> + * @param buttonVisible whether the IME Switcher button is requested visible. + * @param shouldDrawImeNavBar whether the IME navigation bar should be drawn. + * @param imeNavBarNotVisible whether the IME navigation bar is not requested visible. This + * will be {@code true} if it is requested hidden or not + * installed. */ - private void checkCustomImeSwitcherVisibility() { + private void checkCustomImeSwitcherButtonRequestedVisible(boolean buttonVisible, + boolean shouldDrawImeNavBar, boolean imeNavBarNotVisible) { if (!Flags.imeSwitcherRevampApi()) { return; } - final boolean visible = mImeDrawsImeNavBar && mShouldShowImeSwitcherWhenImeIsShown - && mNavigationBarFrame != null && !isShown(); - if (visible != mCustomImeSwitcherVisible) { - mCustomImeSwitcherVisible = visible; - mService.onCustomImeSwitcherButtonRequestedVisible(mCustomImeSwitcherVisible); + // The system nav bar will be hidden when the IME is shown and the config is set. + final boolean navBarNotVisible = shouldDrawImeNavBar ? imeNavBarNotVisible + : mService.getResources().getBoolean( + com.android.internal.R.bool.config_hideNavBarForKeyboard); + final boolean visible = buttonVisible && navBarNotVisible; + if (visible != mCustomImeSwitcherButtonRequestedVisible) { + mCustomImeSwitcherButtonRequestedVisible = visible; + mService.onCustomImeSwitcherButtonRequestedVisible(visible); } } @@ -656,7 +663,8 @@ final class NavigationBarController { + " mNavigationBarFrame=" + mNavigationBarFrame + " mShouldShowImeSwitcherWhenImeIsShown=" + mShouldShowImeSwitcherWhenImeIsShown - + " mCustomImeSwitcherVisible=" + mCustomImeSwitcherVisible + + " mCustomImeSwitcherButtonRequestedVisible=" + + mCustomImeSwitcherButtonRequestedVisible + " mAppearance=0x" + Integer.toHexString(mAppearance) + " mDarkIntensity=" + mDarkIntensity + " mDrawLegacyNavigationBarBackground=" + mDrawLegacyNavigationBarBackground diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index 71d79bb32807..b2f25212729e 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -1780,7 +1780,7 @@ public abstract class BatteryStats { out.writeInt(statIrqTime); out.writeInt(statSoftIrqTime); out.writeInt(statIdlTime); - out.writeString(statSubsystemPowerState); + out.writeString8(statSubsystemPowerState); } public void readFromParcel(Parcel in) { @@ -1801,7 +1801,15 @@ public abstract class BatteryStats { statIrqTime = in.readInt(); statSoftIrqTime = in.readInt(); statIdlTime = in.readInt(); - statSubsystemPowerState = in.readString(); + statSubsystemPowerState = in.readString8(); + } + + + public boolean isEmpty() { + return userTime == 0 && systemTime == 0 && appCpuUid1 == Process.INVALID_UID + && appCpuUid2 == Process.INVALID_UID && appCpuUid3 == Process.INVALID_UID + && statSystemTime == 0 && statIOWaitTime == 0 && statIrqTime == 0 + && statSoftIrqTime == 0 && statIdlTime == 0 && statSubsystemPowerState == null; } } @@ -2238,6 +2246,7 @@ public abstract class BatteryStats { tagsFirstOccurrence = false; powerStats = null; processStateChange = null; + stepDetails = null; } @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) @@ -2289,6 +2298,7 @@ public abstract class BatteryStats { currentTime = o.currentTime; powerStats = o.powerStats; processStateChange = o.processStateChange; + stepDetails = o.stepDetails; } public boolean sameNonEvent(HistoryItem o) { @@ -6938,8 +6948,9 @@ public abstract class BatteryStats { // This constant MUST be incremented whenever the history dump format changes. private static final int FORMAT_VERSION = 2; - private final SimpleDateFormat mHistoryItemTimestampFormat = - new SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US); + private final boolean mPerformanceBaseline; + private final HistoryLogTimeFormatter mHistoryLogTimeFormatter; + private final SimpleDateFormat mHistoryItemTimestampFormat; private final SimpleDateFormat mCurrentTimeEventTimeFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US); @@ -6950,6 +6961,7 @@ public abstract class BatteryStats { private final int mFormatVersion; + private final StringBuilder mStringBuilder = new StringBuilder(); int oldState = 0; int oldState2 = 0; int oldLevel = -1; @@ -6964,18 +6976,30 @@ public abstract class BatteryStats { long lastTime = -1; public HistoryPrinter() { - this(TimeZone.getDefault()); + this(0); } - public HistoryPrinter(TimeZone timeZone) { + public HistoryPrinter(int flags) { + this(TimeZone.getDefault(), flags); + } + + public HistoryPrinter(TimeZone timeZone, int flags) { this(com.android.server.power.optimization.Flags .extendedBatteryHistoryContinuousCollectionEnabled() - ? FORMAT_VERSION : FORMAT_LEGACY, timeZone); + ? FORMAT_VERSION : FORMAT_LEGACY, timeZone, flags); } - private HistoryPrinter(int formatVersion, TimeZone timeZone) { + private HistoryPrinter(int formatVersion, TimeZone timeZone, int flags) { mFormatVersion = formatVersion; - mHistoryItemTimestampFormat.getCalendar().setTimeZone(timeZone); + mPerformanceBaseline = (flags & DUMP_DEBUG_PERF_BASELINE) != 0; + if (mPerformanceBaseline) { + mHistoryItemTimestampFormat = new SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US); + mHistoryItemTimestampFormat.getCalendar().setTimeZone(timeZone); + mHistoryLogTimeFormatter = null; + } else { + mHistoryItemTimestampFormat = null; + mHistoryLogTimeFormatter = new HistoryLogTimeFormatter(timeZone); + } mCurrentTimeEventTimeFormat.getCalendar().setTimeZone(timeZone); } @@ -7009,7 +7033,8 @@ public abstract class BatteryStats { @SuppressWarnings("JavaUtilDate") private String printNextItem(HistoryItem rec, long baseTime, boolean checkin, boolean verbose) { - StringBuilder item = new StringBuilder(); + StringBuilder item = mStringBuilder; + item.setLength(0); if (!checkin) { item.append(" "); if (mFormatVersion == FORMAT_LEGACY) { @@ -7019,8 +7044,13 @@ public abstract class BatteryStats { item.append(rec.numReadInts); item.append(") "); } else { - mDate.setTime(rec.currentTime); - item.append(mHistoryItemTimestampFormat.format(mDate)).append(' '); + if (mPerformanceBaseline) { + mDate.setTime(rec.currentTime); + item.append(mHistoryItemTimestampFormat.format(mDate)).append(' '); + } else { + mHistoryLogTimeFormatter.append(item, rec.currentTime); + item.append(' '); + } } } else { item.append(BATTERY_STATS_CHECKIN_VERSION); item.append(','); @@ -7318,7 +7348,8 @@ public abstract class BatteryStats { } item.append(", SubsystemPowerState "); - item.append(rec.stepDetails.statSubsystemPowerState); + item.append(rec.stepDetails.statSubsystemPowerState != null + ? rec.stepDetails.statSubsystemPowerState : "Empty"); item.append("\n"); } else { item.append(BATTERY_STATS_CHECKIN_VERSION); item.append(','); @@ -7388,6 +7419,64 @@ public abstract class BatteryStats { sb.append(":"); sb.append(stime); } + + /** + * In essence, this is a wrapper over SimpleDateFormat that takes advantage + * of the fact that events in battery history are closely spaced, which allows it + * to reuse the results of the most expensive formatting work. + */ + private static class HistoryLogTimeFormatter { + private static final long HOUR_MILLIS = 3600000L; + private static final long MINUTE_MILLIS = 60000L; + private final SimpleDateFormat mDateFormat = + new SimpleDateFormat("MM-dd HH:", Locale.US); + private final Date mDate = new Date(); + + private final long mTimeZoneOffset; + private long mCachedHour; + private String mCachedHourFormatted; + + private HistoryLogTimeFormatter(TimeZone timeZone) { + mTimeZoneOffset = timeZone.getRawOffset(); + mDateFormat.getCalendar().setTimeZone(timeZone); + } + + /* Appends timestampMs formatted as "MM-dd HH:mm:ss.SSS" */ + void append(StringBuilder sb, long timestampMs) { + long localTime = timestampMs + mTimeZoneOffset; + long hour = localTime / HOUR_MILLIS; + if (hour != mCachedHour) { + mDate.setTime(timestampMs); + mCachedHourFormatted = mDateFormat.format(mDate); + mCachedHour = hour; + } + sb.append(mCachedHourFormatted); + + long remainder = localTime % HOUR_MILLIS; + + long minutes = remainder / MINUTE_MILLIS; + if (minutes < 10) { + sb.append('0'); + } + sb.append(minutes).append(':'); + + remainder = remainder % MINUTE_MILLIS; + long seconds = remainder / 1000; + if (seconds < 10) { + sb.append('0'); + } + sb.append(seconds).append('.'); + + long millis = remainder % 1000; + if (millis < 100) { + sb.append('0'); + if (millis < 10) { + sb.append('0'); + } + } + sb.append(millis); + } + } } private void printSizeValue(PrintWriter pw, long size) { @@ -7573,9 +7662,10 @@ public abstract class BatteryStats { public static final int DUMP_INCLUDE_HISTORY = 1<<4; public static final int DUMP_VERBOSE = 1<<5; public static final int DUMP_DEVICE_WIFI_ONLY = 1<<6; + public static final int DUMP_DEBUG_PERF_BASELINE = 1 << 7; private void dumpHistory(PrintWriter pw, int flags, long histStart, boolean checkin) { - final HistoryPrinter hprinter = new HistoryPrinter(); + final HistoryPrinter hprinter = new HistoryPrinter(flags); synchronized (this) { if (!checkin) { final long historyTotalSize = getHistoryTotalSize(); @@ -8511,7 +8601,7 @@ public abstract class BatteryStats { } // History data (HISTORY_DATA) - final HistoryPrinter hprinter = new HistoryPrinter(); + final HistoryPrinter hprinter = new HistoryPrinter(flags); long lastTime = -1; long baseTime = -1; boolean printed = false; diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java index 739908ef0dfc..b4ca217539a3 100644 --- a/core/java/android/os/BatteryUsageStats.java +++ b/core/java/android/os/BatteryUsageStats.java @@ -129,12 +129,6 @@ public final class BatteryUsageStats implements Parcelable, Closeable { // Max window size. CursorWindow uses only as much memory as needed. private static final long BATTERY_CONSUMER_CURSOR_WINDOW_SIZE = 20_000_000; // bytes - /** - * Used by tests to ensure all BatteryUsageStats instances are closed. - */ - @VisibleForTesting - public static boolean DEBUG_INSTANCE_COUNT; - private static final int STATSD_PULL_ATOM_MAX_BYTES = 45000; private static final int[] UID_USAGE_TIME_PROCESS_STATES = { @@ -1267,11 +1261,16 @@ public final class BatteryUsageStats implements Parcelable, Closeable { } } + /* + * Used by tests to ensure all BatteryUsageStats instances are closed. + */ + private static volatile boolean sInstanceLeakDetectionEnabled; + @GuardedBy("BatteryUsageStats.class") private static Map<CursorWindow, Exception> sInstances; private static void onCursorWindowAllocated(CursorWindow window) { - if (!DEBUG_INSTANCE_COUNT) { + if (!sInstanceLeakDetectionEnabled) { return; } @@ -1284,7 +1283,7 @@ public final class BatteryUsageStats implements Parcelable, Closeable { } private static void onCursorWindowReleased(CursorWindow window) { - if (!DEBUG_INSTANCE_COUNT) { + if (!sInstanceLeakDetectionEnabled) { return; } @@ -1294,12 +1293,26 @@ public final class BatteryUsageStats implements Parcelable, Closeable { } /** + * Enables detection of leaked BatteryUsageStats instances, meaning instances that are created + * but not closed during the test execution. + */ + @VisibleForTesting + public static void enableInstanceLeakDetection() { + sInstanceLeakDetectionEnabled = true; + synchronized (BatteryUsageStats.class) { + if (sInstances != null) { + sInstances.clear(); + } + } + } + + /** * Used by tests to ensure all BatteryUsageStats instances are closed. */ @VisibleForTesting public static void assertAllInstancesClosed() { - if (!DEBUG_INSTANCE_COUNT) { - throw new IllegalStateException("DEBUG_INSTANCE_COUNT is false"); + if (!sInstanceLeakDetectionEnabled) { + throw new IllegalStateException("Instance leak detection is not enabled"); } synchronized (BatteryUsageStats.class) { diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index 4a9928532b93..8a64dd67ace9 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -4702,11 +4702,9 @@ public final class Parcel { object = readValue(type, loader, clazz, itemTypes); int actual = dataPosition() - start; if (actual != length) { - String error = "Unparcelling of " + object + " of type " - + Parcel.valueTypeToString(type) + " consumed " + actual - + " bytes, but " + length + " expected."; - Slog.wtfStack(TAG, error); - throw new BadParcelableException(error); + Slog.wtfStack(TAG, + "Unparcelling of " + object + " of type " + Parcel.valueTypeToString(type) + + " consumed " + actual + " bytes, but " + length + " expected."); } } else { object = readValue(type, loader, clazz, itemTypes); diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig index b52a454ea956..14c154d2e3a9 100644 --- a/core/java/android/os/flags.aconfig +++ b/core/java/android/os/flags.aconfig @@ -13,6 +13,16 @@ flag { } flag { + name: "adpf_25q2_metrics" + namespace: "game" + description: "Add missing metrics for ADPF 25Q2 features." + bug: "367803904" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "adpf_gpu_report_actual_work_duration" is_exported: true namespace: "game" diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index 34272b17cf54..ef6f37ac6f9c 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -554,3 +554,12 @@ flag { description: "This flag is used to add role protection to READ_BLOCKED_NUMBERS for SYSTEM_UI_INTELLIGENCE" bug: "354758615" } + +flag { + name: "enable_system_supervision_role_behavior" + is_fixed_read_only: true + is_exported: true + namespace: "supervision" + description: "This flag is used to enable the role behavior for the system supervision role" + bug: "378102594" +} diff --git a/core/java/android/security/advancedprotection/AdvancedProtectionManager.java b/core/java/android/security/advancedprotection/AdvancedProtectionManager.java index cb2b13d1e120..ecc5fd468ee8 100644 --- a/core/java/android/security/advancedprotection/AdvancedProtectionManager.java +++ b/core/java/android/security/advancedprotection/AdvancedProtectionManager.java @@ -308,7 +308,7 @@ public final class AdvancedProtectionManager { } /** - * Enables or disables advanced protection on the device. + * Enables or disables advanced protection on the device. Can only be called by an admin user. * * @param enabled {@code true} to enable advanced protection, {@code false} to disable it. * @hide diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig index 0a922d61a786..a2403826fe32 100644 --- a/core/java/android/security/flags.aconfig +++ b/core/java/android/security/flags.aconfig @@ -52,21 +52,6 @@ flag { } flag { - name: "extend_vb_chain_to_updated_apk" - namespace: "hardware_backed_security" - description: "Use v4 signature and fs-verity to chain verification of allowlisted APKs to Verified Boot" - bug: "277916185" - is_fixed_read_only: true -} - -flag { - name: "binary_transparency_sepolicy_hash" - namespace: "hardware_backed_security" - description: "Collect sepolicy hash from sysfs" - bug: "308471499" -} - -flag { name: "frp_enforcement" is_exported: true namespace: "hardware_backed_security" @@ -83,13 +68,6 @@ flag { } flag { - name: "dump_attestation_verifications" - namespace: "hardware_backed_security" - description: "Add a dump capability for attestation_verification service" - bug: "335498868" -} - -flag { name: "should_trust_manager_listen_for_primary_auth" namespace: "biometrics" description: "Causes TrustManagerService to listen for credential attempts and ignore reports from upstream" diff --git a/core/java/android/service/dreams/flags.aconfig b/core/java/android/service/dreams/flags.aconfig index d3a230d1335d..82b035c8ebfd 100644 --- a/core/java/android/service/dreams/flags.aconfig +++ b/core/java/android/service/dreams/flags.aconfig @@ -84,3 +84,11 @@ flag { description: "Allow dreaming when device is stationary and upright" bug: "383208131" } + +flag { + name: "dreams_v2" + namespace: "systemui" + description: "Enables various improvements to the dream experience " + "such as new triggers and various bug fixes" + bug: "403579494" +} diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index 0152c52a6753..ebd6efac3d96 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -1074,15 +1074,15 @@ public abstract class Layout { public void onCharacterBounds(int index, int lineNum, float left, float top, float right, float bottom) { - var newBackground = determineContrastingBackgroundColor(index); - var hasBgColorChanged = newBackground != bgPaint.getColor(); - // Skip processing if the character is a space or a tap to avoid // rendering an abrupt, empty rectangle. if (TextLine.isLineEndSpace(mText.charAt(index))) { return; } + var newBackground = determineContrastingBackgroundColor(index); + var hasBgColorChanged = newBackground != bgPaint.getColor(); + // To avoid highlighting emoji sequences, we use Extended_Pictgraphs as a // heuristic. Highlighting is skipped based on code points, not glyph type // (text vs. color), so emojis with default text presentation are diff --git a/core/java/android/util/TypedValue.java b/core/java/android/util/TypedValue.java index 26ab5885c9ea..11f3f8f68dd6 100644 --- a/core/java/android/util/TypedValue.java +++ b/core/java/android/util/TypedValue.java @@ -247,6 +247,12 @@ public class TypedValue { */ public int sourceResourceId; + /** + * Whether the value uses feature flags that need to be evaluated at runtime. + * @hide + */ + public boolean usesFeatureFlags = false; + /* ------------------------------------------------------------ */ /** Return the data for this value as a float. Only use for values diff --git a/core/java/android/util/apk/ApkSignatureSchemeV4Verifier.java b/core/java/android/util/apk/ApkSignatureSchemeV4Verifier.java index 52102714eb5f..ac9ebe58e73e 100644 --- a/core/java/android/util/apk/ApkSignatureSchemeV4Verifier.java +++ b/core/java/android/util/apk/ApkSignatureSchemeV4Verifier.java @@ -88,7 +88,7 @@ public class ApkSignatureSchemeV4Verifier { if (signatureBytes != null && signatureBytes.length > 0) { needsConsistencyCheck = false; signature = V4Signature.readFrom(signatureBytes); - } else if (android.security.Flags.extendVbChainToUpdatedApk()) { + } else { // 2. Try fs-verity next. fs-verity checks against the Merkle tree, but the // v4 signature file (including a raw root hash) is managed separately. We need to // ensure the signed data from the file is consistent with the actual file. @@ -101,9 +101,6 @@ public class ApkSignatureSchemeV4Verifier { throw new SignatureNotFoundException( "Failed to obtain signature bytes from .idsig"); } - } else { - throw new SignatureNotFoundException( - "Failed to obtain signature bytes from IncFS."); } if (!signature.isVersionSupported()) { throw new SecurityException( diff --git a/core/java/android/view/IDisplayWindowListener.aidl b/core/java/android/view/IDisplayWindowListener.aidl index 67ae7430e0b7..79f61f3f0d77 100644 --- a/core/java/android/view/IDisplayWindowListener.aidl +++ b/core/java/android/view/IDisplayWindowListener.aidl @@ -64,4 +64,9 @@ oneway interface IDisplayWindowListener { * Called when the keep clear ares on a display have changed. */ void onKeepClearAreasChanged(int displayId, in List<Rect> restricted, in List<Rect> unrestricted); + + /** + * Called when the eligibility of the desktop mode for a display have changed. + */ + void onDesktopModeEligibleChanged(int displayId); } diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 237d8f96496f..4bf64954a380 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -135,8 +135,29 @@ interface IWindowManager int getDisplayIdByUniqueId(String uniqueId); @EnforcePermission("WRITE_SECURE_SETTINGS") void setForcedDisplayDensityForUser(int displayId, int density, int userId); + /** + * Clears forced density and forced density ratio in DisplayWindowSettings for the given + * displayId. + * + * @param displayId Id of the display. + * @param userId Id of the user. + */ @EnforcePermission("WRITE_SECURE_SETTINGS") void clearForcedDisplayDensityForUser(int displayId, int userId); + /** + * Sets display forced density ratio and forced density in DisplayWindowSettings for + * the given displayId. Ratio is used to update forced density to persist display size when + * resolution change happens. Use {@link #setForcedDisplayDensityForUser} when there is no need + * to handle resolution changes for the display. If setForcedDisplayDensityForUser is used after, + * this the ratio will be updated to use the last set forced density. Use + * {@link #clearForcedDisplayDensityForUser} to reset. + * + * @param displayId Id of the display. + * @param ratio The ratio of forced density to the default density. + * @param userId Id of the user. + */ + @EnforcePermission("WRITE_SECURE_SETTINGS") + void setForcedDisplayDensityRatio(int displayId, float ratio, int userId); /** * Sets settings for a specific user in a batch to minimize configuration updates. diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java index 6decd6d3a603..0558858895b8 100644 --- a/core/java/android/view/InsetsAnimationControlImpl.java +++ b/core/java/android/view/InsetsAnimationControlImpl.java @@ -257,6 +257,11 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro } @Override + public boolean willUpdateSurface() { + return !mFinished && !mCancelled; + } + + @Override public @AnimationType int getAnimationType() { return mAnimationType; } diff --git a/core/java/android/view/InsetsAnimationControlRunner.java b/core/java/android/view/InsetsAnimationControlRunner.java index 4f102da4692a..968181b1723d 100644 --- a/core/java/android/view/InsetsAnimationControlRunner.java +++ b/core/java/android/view/InsetsAnimationControlRunner.java @@ -55,6 +55,12 @@ public interface InsetsAnimationControlRunner { void updateSurfacePosition(SparseArray<InsetsSourceControl> controls); /** + * Returns {@code true} if this runner will keep playing the animation and updating the surface. + * {@code false} otherwise. + */ + boolean willUpdateSurface(); + + /** * Cancels the animation. */ void cancel(); diff --git a/core/java/android/view/InsetsAnimationThreadControlRunner.java b/core/java/android/view/InsetsAnimationThreadControlRunner.java index 8c2c4951a9f7..8acb46dcc0a4 100644 --- a/core/java/android/view/InsetsAnimationThreadControlRunner.java +++ b/core/java/android/view/InsetsAnimationThreadControlRunner.java @@ -89,7 +89,7 @@ public class InsetsAnimationThreadControlRunner implements InsetsAnimationContro } }; - private SurfaceParamsApplier mSurfaceParamsApplier = new SurfaceParamsApplier() { + private final SurfaceParamsApplier mSurfaceParamsApplier = new SurfaceParamsApplier() { private final float[] mTmpFloat9 = new float[9]; @@ -170,6 +170,17 @@ public class InsetsAnimationThreadControlRunner implements InsetsAnimationContro @Override @UiThread + public boolean willUpdateSurface() { + synchronized (mControl) { + // This is called from the UI thread, however, applyChangeInsets would be called on the + // animation thread, so we need this critical section to ensure this is not called + // during applyChangeInsets. See: scheduleApplyChangeInsets. + return mControl.willUpdateSurface(); + } + } + + @Override + @UiThread public void cancel() { InsetsAnimationThread.getHandler().post(mControl::cancel); } diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 4c578fb93600..f7ffc1e1a103 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -1747,9 +1747,9 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation mTypesBeingCancelled |= types; try { for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { - InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner; - if ((control.getTypes() & types) != 0) { - cancelAnimation(control, true /* invokeCallback */); + final InsetsAnimationControlRunner runner = mRunningAnimations.get(i).runner; + if ((runner.getTypes() & types) != 0) { + cancelAnimation(runner, true /* invokeCallback */); } } if ((types & ime()) != 0) { @@ -1788,11 +1788,11 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation ImeTracker.PHASE_CLIENT_ANIMATION_FINISHED_SHOW); ImeTracker.forLogging().onShown(statsToken); } else { - ImeTracker.forLogging().onProgress(statsToken, - ImeTracker.PHASE_CLIENT_ANIMATION_FINISHED_HIDE); // The requestedVisibleTypes are only send at the end of the hide animation. // Therefore, the requested is not finished at this point. if (!Flags.refactorInsetsController()) { + ImeTracker.forLogging().onProgress(statsToken, + ImeTracker.PHASE_CLIENT_ANIMATION_FINISHED_HIDE); ImeTracker.forLogging().onHidden(statsToken); } } @@ -1807,10 +1807,10 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation void notifyControlRevoked(InsetsSourceConsumer consumer) { final @InsetsType int type = consumer.getType(); for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { - InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner; - control.notifyControlRevoked(type); - if (control.getControllingTypes() == 0) { - cancelAnimation(control, true /* invokeCallback */); + final InsetsAnimationControlRunner runner = mRunningAnimations.get(i).runner; + runner.notifyControlRevoked(type); + if (runner.getControllingTypes() == 0) { + cancelAnimation(runner, true /* invokeCallback */); } } if (type == ime()) { @@ -1823,38 +1823,38 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } } - private void cancelAnimation(InsetsAnimationControlRunner control, boolean invokeCallback) { + private void cancelAnimation(InsetsAnimationControlRunner runner, boolean invokeCallback) { if (invokeCallback) { - ImeTracker.forLogging().onCancelled(control.getStatsToken(), + ImeTracker.forLogging().onCancelled(runner.getStatsToken(), ImeTracker.PHASE_CLIENT_ANIMATION_CANCEL); - control.cancel(); + runner.cancel(); } else { // Succeeds if invokeCallback is false (i.e. when called from notifyFinished). - ImeTracker.forLogging().onProgress(control.getStatsToken(), + ImeTracker.forLogging().onProgress(runner.getStatsToken(), ImeTracker.PHASE_CLIENT_ANIMATION_CANCEL); } if (DEBUG) { Log.d(TAG, TextUtils.formatSimple( "cancelAnimation of types: %d, animType: %d, host: %s", - control.getTypes(), control.getAnimationType(), mHost.getRootViewTitle())); + runner.getTypes(), runner.getAnimationType(), mHost.getRootViewTitle())); } @InsetsType int removedTypes = 0; for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { RunningAnimation runningAnimation = mRunningAnimations.get(i); - if (runningAnimation.runner == control) { + if (runningAnimation.runner == runner) { mRunningAnimations.remove(i); - removedTypes = control.getTypes(); + removedTypes = runner.getTypes(); if (invokeCallback) { dispatchAnimationEnd(runningAnimation.runner.getAnimation()); } else { if (Flags.refactorInsetsController()) { if ((removedTypes & ime()) != 0 - && control.getAnimationType() == ANIMATION_TYPE_HIDE) { + && runner.getAnimationType() == ANIMATION_TYPE_HIDE) { if (mHost != null) { // if the (hide) animation is cancelled, the // requestedVisibleTypes should be reported at this point. reportRequestedVisibleTypes(!Flags.reportAnimatingInsetsTypes() - ? control.getStatsToken() : null); + ? runner.getStatsToken() : null); mHost.getInputMethodManager().removeImeSurface( mHost.getWindowToken()); } @@ -1869,9 +1869,9 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation if (mHost != null) { final boolean dispatchStatsToken = Flags.reportAnimatingInsetsTypes() && (removedTypes & ime()) != 0 - && control.getAnimationType() == ANIMATION_TYPE_HIDE; + && runner.getAnimationType() == ANIMATION_TYPE_HIDE; mHost.updateAnimatingTypes(mAnimatingTypes, - dispatchStatsToken ? control.getStatsToken() : null); + dispatchStatsToken ? runner.getStatsToken() : null); } } @@ -1959,14 +1959,30 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation @VisibleForTesting(visibility = PACKAGE) public @AnimationType int getAnimationType(@InsetsType int type) { for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { - InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner; - if (control.controlsType(type)) { + final InsetsAnimationControlRunner runner = mRunningAnimations.get(i).runner; + if (runner.controlsType(type)) { return mRunningAnimations.get(i).type; } } return ANIMATION_TYPE_NONE; } + /** + * Returns {@code true} if there is an animation which controls the given {@link InsetsType} and + * the runner is still playing the surface animation. + * + * @see InsetsAnimationControlRunner#willUpdateSurface() + */ + boolean hasSurfaceAnimation(@InsetsType int type) { + for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { + final InsetsAnimationControlRunner runner = mRunningAnimations.get(i).runner; + if (runner.controlsType(type) && runner.willUpdateSurface()) { + return true; + } + } + return false; + } + @VisibleForTesting(visibility = PACKAGE) public void setRequestedVisibleTypes(@InsetsType int visibleTypes, @InsetsType int mask) { final @InsetsType int requestedVisibleTypes = diff --git a/core/java/android/view/InsetsResizeAnimationRunner.java b/core/java/android/view/InsetsResizeAnimationRunner.java index 5262751cc6ed..6356be262cc4 100644 --- a/core/java/android/view/InsetsResizeAnimationRunner.java +++ b/core/java/android/view/InsetsResizeAnimationRunner.java @@ -233,6 +233,11 @@ public class InsetsResizeAnimationRunner implements InsetsAnimationControlRunner } @Override + public boolean willUpdateSurface() { + return false; + } + + @Override public boolean hasZeroInsetsIme() { return false; } diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java index 945975a88cd5..1a750a3f89c4 100644 --- a/core/java/android/view/InsetsSourceConsumer.java +++ b/core/java/android/view/InsetsSourceConsumer.java @@ -17,7 +17,6 @@ package android.view; import static android.view.InsetsController.ANIMATION_TYPE_NONE; -import static android.view.InsetsController.ANIMATION_TYPE_RESIZE; import static android.view.InsetsController.AnimationType; import static android.view.InsetsController.DEBUG; import static android.view.InsetsSourceConsumerProto.ANIMATION_STATE; @@ -201,9 +200,8 @@ public class InsetsSourceConsumer { } // If there is no animation controlling the leash, make sure the visibility and the - // position is up-to-date. Note: ANIMATION_TYPE_RESIZE doesn't control the leash. - final int animType = mController.getAnimationType(mType); - if (animType == ANIMATION_TYPE_NONE || animType == ANIMATION_TYPE_RESIZE) { + // position is up-to-date. + if (!mController.hasSurfaceAnimation(mType)) { applyRequestedVisibilityAndPositionToControl(); } diff --git a/core/java/android/view/translation/ListenerGroup.java b/core/java/android/view/ListenerGroup.java index 5c70805042fa..889caec3f5d1 100644 --- a/core/java/android/view/translation/ListenerGroup.java +++ b/core/java/android/view/ListenerGroup.java @@ -14,10 +14,9 @@ * limitations under the License. */ -package android.view.translation; +package android.view; import android.annotation.NonNull; -import android.view.ListenerWrapper; import java.util.ArrayList; import java.util.List; @@ -32,26 +31,39 @@ import java.util.function.Consumer; */ public class ListenerGroup<T> { private final List<ListenerWrapper<T>> mListeners = new ArrayList<>(); + @NonNull + private T mLastValue; + + /** + * Constructs a {@link ListenerGroup} that will replay the last reported value whenever a new + * listener is registered. + * @param value the initial value + */ + public ListenerGroup(@NonNull T value) { + mLastValue = value; + } /** * Relays the value to all the registered {@link java.util.function.Consumer} */ public void accept(@NonNull T value) { - Objects.requireNonNull(value); + mLastValue = Objects.requireNonNull(value); for (int i = 0; i < mListeners.size(); i++) { mListeners.get(i).accept(value); } } /** - * Adds a {@link Consumer} to the group. If the {@link Consumer} is already present then this - * is a no op. + * Adds a {@link Consumer} to the group and replays the last reported value. If the + * {@link Consumer} is already present then this is a no op. */ public void addListener(@NonNull Executor executor, @NonNull Consumer<T> consumer) { if (isConsumerPresent(consumer)) { return; } - mListeners.add(new ListenerWrapper<>(executor, consumer)); + final ListenerWrapper<T> listenerWrapper = new ListenerWrapper<>(executor, consumer); + mListeners.add(listenerWrapper); + listenerWrapper.accept(mLastValue); } /** diff --git a/core/java/android/view/RoundScrollbarRenderer.java b/core/java/android/view/RoundScrollbarRenderer.java index 331e34526ae8..a592e1f0a874 100644 --- a/core/java/android/view/RoundScrollbarRenderer.java +++ b/core/java/android/view/RoundScrollbarRenderer.java @@ -45,8 +45,8 @@ public class RoundScrollbarRenderer { private static final float MIN_SCROLLBAR_ANGLE_SWIPE = 0.3f * SCROLLBAR_ANGLE_RANGE; private static final float GAP_BETWEEN_TRACK_AND_THUMB_DP = 3f; private static final float OUTER_PADDING_DP = 2f; - private static final int DEFAULT_THUMB_COLOR = 0xFFFFFFFF; - private static final int DEFAULT_TRACK_COLOR = 0x4CFFFFFF; + private static final int DEFAULT_THUMB_COLOR = 0xFFC6C6C7; + private static final int DEFAULT_TRACK_COLOR = 0xFF2F3131; // Rate at which the scrollbar will resize itself when the size of the view changes private static final float RESIZING_RATE = 0.8f; diff --git a/core/java/android/view/ScrollCaptureConnection.java b/core/java/android/view/ScrollCaptureConnection.java index f0c7909647ce..0abb8b6c9a5a 100644 --- a/core/java/android/view/ScrollCaptureConnection.java +++ b/core/java/android/view/ScrollCaptureConnection.java @@ -20,8 +20,9 @@ import static android.os.Trace.TRACE_TAG_GRAPHICS; import static java.util.Objects.requireNonNull; -import android.annotation.BinderThread; +import android.annotation.AnyThread; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.UiThread; import android.graphics.Point; import android.graphics.Rect; @@ -64,9 +65,13 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub imple private final Executor mUiThread; private final CloseGuard mCloseGuard = new CloseGuard(); + @Nullable private ScrollCaptureCallback mLocal; + @Nullable private IScrollCaptureCallbacks mRemote; + @Nullable private ScrollCaptureSession mSession; + @Nullable private CancellationSignal mCancellation; private volatile boolean mActive; @@ -92,7 +97,7 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub imple mPositionInWindow = new Point(selectedTarget.getPositionInWindow()); } - @BinderThread + @AnyThread @Override public ICancellationSignal startCapture(@NonNull Surface surface, @NonNull IScrollCaptureCallbacks remote) throws RemoteException { @@ -115,7 +120,11 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub imple Runnable listener = SafeCallback.create(mCancellation, mUiThread, this::onStartCaptureCompleted); // -> UiThread - mUiThread.execute(() -> mLocal.onScrollCaptureStart(mSession, mCancellation, listener)); + mUiThread.execute(() -> { + if (mLocal != null && mCancellation != null) { + mLocal.onScrollCaptureStart(mSession, mCancellation, listener); + } + }); return cancellation; } @@ -123,7 +132,11 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub imple private void onStartCaptureCompleted() { mActive = true; try { - mRemote.onCaptureStarted(); + if (mRemote != null) { + mRemote.onCaptureStarted(); + } else { + close(); + } } catch (RemoteException e) { Log.w(TAG, "Shutting down due to error: ", e); close(); @@ -132,7 +145,7 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub imple Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, mTraceId); } - @BinderThread + @AnyThread @Override public ICancellationSignal requestImage(Rect requestRect) throws RemoteException { Trace.asyncTraceForTrackBegin(TRACE_TAG_GRAPHICS, TRACE_TRACK, REQUEST_IMAGE, mTraceId); @@ -145,7 +158,7 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub imple SafeCallback.create(mCancellation, mUiThread, this::onImageRequestCompleted); // -> UiThread mUiThread.execute(() -> { - if (mLocal != null) { + if (mLocal != null && mSession != null && mCancellation != null) { mLocal.onScrollCaptureImageRequest( mSession, mCancellation, new Rect(requestRect), listener); } @@ -157,7 +170,11 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub imple @UiThread void onImageRequestCompleted(Rect capturedArea) { try { - mRemote.onImageRequestCompleted(0, capturedArea); + if (mRemote != null) { + mRemote.onImageRequestCompleted(0, capturedArea); + } else { + close(); + } } catch (RemoteException e) { Log.w(TAG, "Shutting down due to error: ", e); close(); @@ -167,7 +184,7 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub imple Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, mTraceId); } - @BinderThread + @AnyThread @Override public ICancellationSignal endCapture() throws RemoteException { Trace.asyncTraceForTrackBegin(TRACE_TAG_GRAPHICS, TRACE_TRACK, END_CAPTURE, mTraceId); @@ -212,7 +229,7 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub imple } - @BinderThread + @AnyThread @Override public synchronized void close() { Trace.instantForTrack(TRACE_TAG_GRAPHICS, TRACE_TRACK, "close"); @@ -220,7 +237,11 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub imple Log.w(TAG, "close(): capture session still active! Ending now."); cancelPendingAction(); final ScrollCaptureCallback callback = mLocal; - mUiThread.execute(() -> callback.onScrollCaptureEnd(() -> { /* ignore */ })); + mUiThread.execute(() -> { + if (callback != null) { + callback.onScrollCaptureEnd(() -> { /* ignore */ }); + } + }); mActive = false; } if (mRemote != null) { @@ -297,10 +318,13 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub imple protected final void maybeAccept(Consumer<T> consumer) { T value = mValue.getAndSet(null); if (mSignal.isCanceled()) { + Log.w(TAG, "callback ignored, operation already cancelled"); return; } if (value != null) { mExecutor.execute(() -> consumer.accept(value)); + } else { + Log.w(TAG, "callback ignored, value already delivered"); } } diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index c7ae3283c46c..f9d7a672f43a 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -2592,7 +2592,7 @@ public final class SurfaceControl implements Parcelable { int[] dataspaces = nativeGetCompositionDataspaces(); ColorSpace srgb = ColorSpace.get(ColorSpace.Named.SRGB); ColorSpace[] colorSpaces = { srgb, srgb }; - if (dataspaces.length == 2) { + if (dataspaces != null && dataspaces.length == 2) { for (int i = 0; i < 2; ++i) { ColorSpace cs = ColorSpace.getFromDataSpace(dataspaces[i]); if (cs != null) { diff --git a/core/java/android/view/SyncRtSurfaceTransactionApplier.java b/core/java/android/view/SyncRtSurfaceTransactionApplier.java index e9c937cc0f9b..b2f0bd931174 100644 --- a/core/java/android/view/SyncRtSurfaceTransactionApplier.java +++ b/core/java/android/view/SyncRtSurfaceTransactionApplier.java @@ -16,6 +16,7 @@ package android.view; +import android.annotation.SuppressLint; import android.graphics.Matrix; import android.graphics.Rect; import android.view.SurfaceControl.Transaction; @@ -39,6 +40,9 @@ public class SyncRtSurfaceTransactionApplier { public static final int FLAG_BACKGROUND_BLUR_RADIUS = 1 << 5; public static final int FLAG_VISIBILITY = 1 << 6; public static final int FLAG_TRANSACTION = 1 << 7; + public static final int FLAG_EARLY_WAKEUP_START = 1 << 8; + public static final int FLAG_EARLY_WAKEUP_END = 1 << 9; + public static final int FLAG_OPAQUE = 1 << 10; private SurfaceControl mTargetSc; private final ViewRootImpl mTargetViewRootImpl; @@ -99,6 +103,7 @@ public class SyncRtSurfaceTransactionApplier { } } + @SuppressLint("MissingPermission") public static void applyParams(Transaction t, SurfaceParams params, float[] tmpFloat9) { if ((params.flags & FLAG_TRANSACTION) != 0) { t.merge(params.mergeTransaction); @@ -129,6 +134,15 @@ public class SyncRtSurfaceTransactionApplier { t.hide(params.surface); } } + if ((params.flags & FLAG_EARLY_WAKEUP_START) != 0) { + t.setEarlyWakeupStart(); + } + if ((params.flags & FLAG_EARLY_WAKEUP_END) != 0) { + t.setEarlyWakeupEnd(); + } + if ((params.flags & FLAG_OPAQUE) != 0) { + t.setOpaque(params.surface, params.opaque); + } } /** @@ -172,6 +186,7 @@ public class SyncRtSurfaceTransactionApplier { Rect windowCrop; int layer; boolean visible; + boolean opaque; Transaction mergeTransaction; /** @@ -263,17 +278,48 @@ public class SyncRtSurfaceTransactionApplier { } /** + * Provides a hint to SurfaceFlinger to change its offset so that SurfaceFlinger + * wakes up earlier to compose surfaces. + * @return this Builder + */ + public Builder withEarlyWakeupStart() { + flags |= FLAG_EARLY_WAKEUP_START; + return this; + } + + /** + * Removes the early wake up hint set by earlyWakeupStart. + * @return this Builder + */ + public Builder withEarlyWakeupEnd() { + flags |= FLAG_EARLY_WAKEUP_END; + return this; + } + + /** + * @param opaque Indicates weather the surface must be considered opaque. + * @return this Builder + */ + public Builder withOpaque(boolean opaque) { + this.opaque = opaque; + flags |= FLAG_OPAQUE; + return this; + } + + /** * @return a new SurfaceParams instance */ public SurfaceParams build() { return new SurfaceParams(surface, flags, alpha, matrix, windowCrop, layer, - cornerRadius, backgroundBlurRadius, visible, mergeTransaction); + cornerRadius, backgroundBlurRadius, visible, mergeTransaction, + opaque); } } private SurfaceParams(SurfaceControl surface, int params, float alpha, Matrix matrix, Rect windowCrop, int layer, float cornerRadius, - int backgroundBlurRadius, boolean visible, Transaction mergeTransaction) { + int backgroundBlurRadius, boolean visible, + Transaction mergeTransaction, boolean opaque) { this.flags = params; this.surface = surface; this.alpha = alpha; @@ -284,6 +330,7 @@ public class SyncRtSurfaceTransactionApplier { this.backgroundBlurRadius = backgroundBlurRadius; this.visible = visible; this.mergeTransaction = mergeTransaction; + this.opaque = opaque; } private final int flags; @@ -312,5 +359,6 @@ public class SyncRtSurfaceTransactionApplier { public final boolean visible; public final Transaction mergeTransaction; + public final boolean opaque; } } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index f32ce6f1d6e4..1213d173ab3b 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -5179,9 +5179,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * This lives here since it's only valid for interactive views. This list is null * until its first use. */ - private List<Rect> mSystemGestureExclusionRects = null; - private List<Rect> mKeepClearRects = null; - private List<Rect> mUnrestrictedKeepClearRects = null; + private ArrayList<Rect> mSystemGestureExclusionRects = null; + private ArrayList<Rect> mKeepClearRects = null; + private ArrayList<Rect> mUnrestrictedKeepClearRects = null; private boolean mPreferKeepClear = false; private Rect mHandwritingArea = null; @@ -12891,21 +12891,34 @@ public class View implements Drawable.Callback, KeyEvent.Callback, final ListenerInfo info = getListenerInfo(); final boolean rectsChanged = !reduceChangedExclusionRectsMsgs() - || !Objects.equals(info.mSystemGestureExclusionRects, rects); - if (info.mSystemGestureExclusionRects != null) { - if (rectsChanged) { - info.mSystemGestureExclusionRects.clear(); - info.mSystemGestureExclusionRects.addAll(rects); - } - } else { - info.mSystemGestureExclusionRects = new ArrayList<>(rects); + || !Objects.deepEquals(info.mSystemGestureExclusionRects, rects); + if (info.mSystemGestureExclusionRects == null) { + info.mSystemGestureExclusionRects = new ArrayList<>(); } if (rectsChanged) { + deepCopyRectsObjectRecycling(info.mSystemGestureExclusionRects, rects); updatePositionUpdateListener(); postUpdate(this::updateSystemGestureExclusionRects); } } + private void deepCopyRectsObjectRecycling(@NonNull ArrayList<Rect> dest, List<Rect> src) { + dest.ensureCapacity(src.size()); + for (int i = 0; i < src.size(); i++) { + if (i < dest.size()) { + // Replace if there is an old rect to refresh + dest.get(i).set(src.get(i)); + } else { + // Add a rect if the list enlarged + dest.add(Rect.copyOrNull(src.get(i))); + } + } + while (dest.size() > src.size()) { + // Remove elements if the list shrank + dest.removeLast(); + } + } + private void updatePositionUpdateListener() { final ListenerInfo info = getListenerInfo(); if (getSystemGestureExclusionRects().isEmpty() @@ -13031,14 +13044,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public final void setPreferKeepClearRects(@NonNull List<Rect> rects) { final ListenerInfo info = getListenerInfo(); - if (info.mKeepClearRects != null) { - info.mKeepClearRects.clear(); - info.mKeepClearRects.addAll(rects); - } else { - info.mKeepClearRects = new ArrayList<>(rects); + final boolean rectsChanged = !reduceChangedExclusionRectsMsgs() + || !Objects.deepEquals(info.mKeepClearRects, rects); + if (info.mKeepClearRects == null) { + info.mKeepClearRects = new ArrayList<>(); + } + if (rectsChanged) { + deepCopyRectsObjectRecycling(info.mKeepClearRects, rects); + updatePositionUpdateListener(); + postUpdate(this::updateKeepClearRects); } - updatePositionUpdateListener(); - postUpdate(this::updateKeepClearRects); } /** @@ -13076,14 +13091,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @RequiresPermission(android.Manifest.permission.SET_UNRESTRICTED_KEEP_CLEAR_AREAS) public final void setUnrestrictedPreferKeepClearRects(@NonNull List<Rect> rects) { final ListenerInfo info = getListenerInfo(); - if (info.mUnrestrictedKeepClearRects != null) { - info.mUnrestrictedKeepClearRects.clear(); - info.mUnrestrictedKeepClearRects.addAll(rects); - } else { - info.mUnrestrictedKeepClearRects = new ArrayList<>(rects); + final boolean rectsChanged = !reduceChangedExclusionRectsMsgs() + || !Objects.deepEquals(info.mUnrestrictedKeepClearRects, rects); + if (info.mUnrestrictedKeepClearRects == null) { + info.mUnrestrictedKeepClearRects = new ArrayList<>(); + } + if (rectsChanged) { + deepCopyRectsObjectRecycling(info.mUnrestrictedKeepClearRects, rects); + updatePositionUpdateListener(); + postUpdate(this::updateKeepClearRects); } - updatePositionUpdateListener(); - postUpdate(this::updateKeepClearRects); } /** diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 4e3ff9063179..b1676dde3b70 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -138,6 +138,7 @@ import static com.android.window.flags.Flags.enableWindowContextResourcesUpdateO import static com.android.window.flags.Flags.predictiveBackSwipeEdgeNoneApi; import static com.android.window.flags.Flags.reduceChangedExclusionRectsMsgs; import static com.android.window.flags.Flags.setScPropertiesInClient; +import static com.android.window.flags.Flags.fixViewRootCallTrace; import android.Manifest; import android.accessibilityservice.AccessibilityService; @@ -189,6 +190,7 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; import android.graphics.RenderNode; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.hardware.SyncFence; @@ -292,6 +294,7 @@ import com.android.internal.os.SomeArgs; import com.android.internal.policy.DecorView; import com.android.internal.policy.PhoneFallbackEventHandler; import com.android.internal.protolog.ProtoLog; +import com.android.internal.util.ContrastColorUtil; import com.android.internal.util.FastPrintWriter; import com.android.internal.view.BaseSurfaceHolder; import com.android.internal.view.RootViewSurfaceTaker; @@ -1588,7 +1591,9 @@ public final class ViewRootImpl implements ViewParent, mAttachInfo.mPanelParentWindowToken = panelParentView.getApplicationWindowToken(); } - mAdded = true; + if (!fixViewRootCallTrace()) { + mAdded = true; + } int res; /* = WindowManagerImpl.ADD_OKAY; */ // Schedule the first layout -before- adding to the window @@ -1639,7 +1644,9 @@ public final class ViewRootImpl implements ViewParent, mTmpFrames.compatScale = compatScale[0]; mInvCompatScale = 1f / compatScale[0]; } catch (RemoteException | RuntimeException e) { - mAdded = false; + if (!fixViewRootCallTrace()) { + mAdded = false; + } mView = null; mAttachInfo.mRootView = null; mFallbackEventHandler.setView(null); @@ -1670,7 +1677,9 @@ public final class ViewRootImpl implements ViewParent, if (DEBUG_LAYOUT) Log.v(mTag, "Added window " + mWindow); if (res < WindowManagerGlobal.ADD_OKAY) { mAttachInfo.mRootView = null; - mAdded = false; + if (!fixViewRootCallTrace()) { + mAdded = false; + } mFallbackEventHandler.setView(null); unscheduleTraversals(); setAccessibilityFocus(null, null); @@ -1779,6 +1788,9 @@ public final class ViewRootImpl implements ViewParent, mFirstInputStage = nativePreImeStage; mFirstPostImeInputStage = earlyPostImeStage; mPendingInputEventQueueLengthCounterName = "aq:pending:" + counterSuffix; + if (fixViewRootCallTrace()) { + mAdded = true; + } if (!mRemoved || !mAppVisible) { AnimationHandler.requestAnimatorsEnabled(mAppVisible, this); @@ -2078,12 +2090,21 @@ public final class ViewRootImpl implements ViewParent, // preference for dark mode in configuration.uiMode. Instead, we assume that both // force invert and the system's dark theme are enabled. if (shouldApplyForceInvertDark()) { - final boolean isLightTheme = - a.getBoolean(R.styleable.Theme_isLightTheme, false); - // TODO: b/372558459 - Also check the background ColorDrawable color lightness // TODO: b/368725782 - Use hwui color area detection instead of / in // addition to these heuristics. - if (isLightTheme) { + final boolean isLightTheme = + a.getBoolean(R.styleable.Theme_isLightTheme, false); + final boolean isBackgroundColorLight; + if (mView != null && mView.getBackground() + instanceof ColorDrawable colorDrawable) { + isBackgroundColorLight = + !ContrastColorUtil.isColorDarkLab(colorDrawable.getColor()); + } else { + // Treat unknown as light, so that only isLightTheme is used to determine + // force dark treatment. + isBackgroundColorLight = true; + } + if (isLightTheme && isBackgroundColorLight) { return ForceDarkType.FORCE_INVERT_COLOR_DARK; } else { return ForceDarkType.NONE; @@ -10270,6 +10291,8 @@ public final class ViewRootImpl implements ViewParent, try { mWindowSession.notifyImeWindowVisibilityChangedFromClient(mWindow, visible, statsToken); } catch (RemoteException e) { + ImeTracker.forLogging().onFailed(statsToken, + ImeTracker.PHASE_CLIENT_NOTIFY_IME_VISIBILITY_CHANGED); e.rethrowFromSystemServer(); } } @@ -11505,12 +11528,24 @@ public final class ViewRootImpl implements ViewParent, // Search through View-tree View rootView = getView(); - if (rootView != null) { - Point point = new Point(); - Rect rect = new Rect(0, 0, rootView.getWidth(), rootView.getHeight()); - getChildVisibleRect(rootView, rect, point); - rootView.dispatchScrollCaptureSearch(rect, point, results::addTarget); + if (rootView == null) { + ScrollCaptureResponse.Builder response = new ScrollCaptureResponse.Builder(); + response.setWindowTitle(getTitle().toString()); + response.setPackageName(mContext.getPackageName()); + response.setDescription("The root view was null"); + try { + listener.onScrollCaptureResponse(response.build()); + } catch (RemoteException e) { + Log.e(TAG, "Failed to send scroll capture search result", e); + } + return; } + + Point point = new Point(); + Rect rect = new Rect(0, 0, rootView.getWidth(), rootView.getHeight()); + getChildVisibleRect(rootView, rect, point); + rootView.dispatchScrollCaptureSearch(rect, point, results::addTarget); + Runnable onComplete = () -> dispatchScrollCaptureSearchResponse(listener, results); results.setOnCompleteListener(onComplete); if (!results.isComplete()) { @@ -11535,6 +11570,16 @@ public final class ViewRootImpl implements ViewParent, pw.flush(); response.addMessage(writer.toString()); + if (mView == null) { + response.setDescription("The root view disappeared!"); + try { + listener.onScrollCaptureResponse(response.build()); + } catch (RemoteException e) { + Log.e(TAG, "Failed to send scroll capture search result", e); + } + return; + } + if (selectedTarget == null) { response.setDescription("No scrollable targets found in window"); try { @@ -11561,6 +11606,7 @@ public final class ViewRootImpl implements ViewParent, boundsOnScreen.set(0, 0, mView.getWidth(), mView.getHeight()); boundsOnScreen.offset(mAttachInfo.mTmpLocation[0], mAttachInfo.mTmpLocation[1]); response.setWindowBounds(boundsOnScreen); + Log.d(TAG, "ScrollCaptureSearchResponse: " + response); // Create a connection and return it to the caller ScrollCaptureConnection connection = new ScrollCaptureConnection( diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 9d21f1aff0c3..1ba3a74b8b2b 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -80,9 +80,6 @@ import static android.view.WindowLayoutParamsProto.WINDOW_ANIMATIONS; import static android.view.WindowLayoutParamsProto.X; import static android.view.WindowLayoutParamsProto.Y; -import static com.android.hardware.input.Flags.FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW; -import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow; - import android.Manifest.permission; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; @@ -4549,29 +4546,6 @@ public interface WindowManager extends ViewManager { public static final int INPUT_FEATURE_SENSITIVE_FOR_PRIVACY = 1 << 3; /** - * Input feature used to indicate that the system should send power key events to this - * window when it's in the foreground. The window can override the double press power key - * gesture behavior. - * - * A double press gesture is defined as two - * {@link KeyEvent.Callback#onKeyDown(int, KeyEvent)} events within a time span defined by - * {@link ViewConfiguration#getMultiPressTimeout()}. - * - * Note: While the window may receive all power key {@link KeyEvent}s, it can only - * override the double press gesture behavior. The system will perform default behavior for - * single, long-press and other multi-press gestures, regardless of if the app handles the - * key or not. - * - * To override the default behavior for double press, the app must return true for the - * second {@link KeyEvent.Callback#onKeyDown(int, KeyEvent)}. If the app returns false, the - * system behavior will be performed for double press. - * @hide - */ - @RequiresPermission(permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public static final int - INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS = 1 << 4; - - /** * An internal annotation for flags that can be specified to {@link #inputFeatures}. * * NOTE: These are not the same as {@link android.os.InputConfig} flags. @@ -4583,8 +4557,7 @@ public interface WindowManager extends ViewManager { INPUT_FEATURE_NO_INPUT_CHANNEL, INPUT_FEATURE_DISABLE_USER_ACTIVITY, INPUT_FEATURE_SPY, - INPUT_FEATURE_SENSITIVE_FOR_PRIVACY, - INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS + INPUT_FEATURE_SENSITIVE_FOR_PRIVACY }) public @interface InputFeatureFlags { } @@ -4874,44 +4847,6 @@ public interface WindowManager extends ViewManager { } /** - * Specifies if the system should send power key events to this window when it's in the - * foreground, with only the double tap gesture behavior being overrideable. - * - * @param enabled if true, the system should send power key events to this window when it's - * in the foreground, with only the power key double tap gesture being - * overrideable. - * @hide - */ - @SystemApi - @RequiresPermission(permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - @FlaggedApi(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public void setReceivePowerKeyDoublePressEnabled(boolean enabled) { - if (enabled) { - inputFeatures - |= INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS; - } else { - inputFeatures - &= ~INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS; - } - } - - /** - * Returns whether or not the system should send power key events to this window when it's - * in the foreground, with only the double tap gesture being overrideable. - * - * @return if the system should send power key events to this window when it's in the - * foreground, with only the double tap gesture being overrideable. - * @hide - */ - @SystemApi - @RequiresPermission(permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - @FlaggedApi(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public boolean isReceivePowerKeyDoublePressEnabled() { - return (inputFeatures - & INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS) != 0; - } - - /** * Specifies that the window should be considered a trusted system overlay. Trusted system * overlays are ignored when considering whether windows are obscured during input * dispatch. Requires the {@link android.Manifest.permission#INTERNAL_SYSTEM_WINDOW} @@ -6312,16 +6247,6 @@ public interface WindowManager extends ViewManager { inputFeatures &= ~INPUT_FEATURE_SPY; features.add("INPUT_FEATURE_SPY"); } - if (overridePowerKeyBehaviorInFocusedWindow()) { - if ((inputFeatures - & INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS) - != 0) { - inputFeatures - &= - ~INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS; - features.add("INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS"); - } - } if (inputFeatures != 0) { features.add(Integer.toHexString(inputFeatures)); } diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java index b97f28da7559..5fdb387c0f73 100644 --- a/core/java/android/view/WindowManagerGlobal.java +++ b/core/java/android/view/WindowManagerGlobal.java @@ -44,7 +44,6 @@ import android.util.Log; import android.util.Pair; import android.util.SparseArray; import android.view.inputmethod.InputMethodManager; -import android.view.translation.ListenerGroup; import android.window.ITrustedPresentationListener; import android.window.InputTransferToken; import android.window.TrustedPresentationThresholds; @@ -154,7 +153,8 @@ public final class WindowManagerGlobal { * @hide */ @GuardedBy("mLock") - private final ListenerGroup<List<View>> mWindowViewsListenerGroup = new ListenerGroup<>(); + private final ListenerGroup<List<View>> mWindowViewsListenerGroup = + new ListenerGroup<>(new ArrayList<>()); @UnsupportedAppUsage private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>(); @UnsupportedAppUsage @@ -339,7 +339,6 @@ public final class WindowManagerGlobal { return; } mWindowViewsListenerGroup.addListener(executor, consumer); - executor.execute(() -> consumer.accept(getWindowViews())); } } diff --git a/core/java/android/view/XrWindowProperties.java b/core/java/android/view/XrWindowProperties.java index 23021a563393..c02d7a9bb8c5 100644 --- a/core/java/android/view/XrWindowProperties.java +++ b/core/java/android/view/XrWindowProperties.java @@ -27,7 +27,7 @@ public final class XrWindowProperties { private XrWindowProperties() {} /** - * Both Application and activity level + * Application and Activity level * {@link android.content.pm.PackageManager.Property PackageManager.Property} for an app to * inform the system of the activity launch mode in XR. When it is declared at the application * level, all activities are set to the defined value, unless it is overridden at the activity @@ -105,7 +105,7 @@ public final class XrWindowProperties { "XR_ACTIVITY_START_MODE_HOME_SPACE"; /** - * Both Application and activity level + * Application and Activity level * {@link android.content.pm.PackageManager.Property PackageManager.Property} for an app to * inform the system of the type of safety boundary recommended for the activity. When it is * declared at the application level, all activities are set to the defined value, unless it is @@ -156,4 +156,30 @@ public final class XrWindowProperties { */ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) public static final String XR_BOUNDARY_TYPE_LARGE = "XR_BOUNDARY_TYPE_LARGE"; + + /** + * Application and Activity level + * {@link android.content.pm.PackageManager.Property PackageManager.Property} to inform the + * system if it should play a system provided default animation when the app requests to enter + * or exit <a + * href="https://developer.android.com/develop/xr/jetpack-xr-sdk/transition-home-space-to-full-space">managed + * full space mode</a> in XR. When set to {@code true}, the system provided default animation is + * not played and the app is responsible for playing a custom enter or exit animation. When it + * is declared at the application level, all activities are set to the defined value, unless it + * is overridden at the activity level. + * + * <p>The default value is {@code false}. + * + * <p><b>Syntax:</b> + * <pre> + * <application> + * <property + * android:name="android.window.PROPERTY_XR_USES_CUSTOM_FULL_SPACE_MANAGED_ANIMATION" + * android:value="false|true"/> + * </application> + * </pre> + */ + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) + public static final String PROPERTY_XR_USES_CUSTOM_FULL_SPACE_MANAGED_ANIMATION = + "android.window.PROPERTY_XR_USES_CUSTOM_FULL_SPACE_MANAGED_ANIMATION"; } diff --git a/core/java/android/view/autofill/AutofillFeatureFlags.java b/core/java/android/view/autofill/AutofillFeatureFlags.java index 0814e23eea87..816d2debf87e 100644 --- a/core/java/android/view/autofill/AutofillFeatureFlags.java +++ b/core/java/android/view/autofill/AutofillFeatureFlags.java @@ -422,7 +422,7 @@ public class AutofillFeatureFlags { * * @hide */ - public static final boolean DEFAULT_SESSION_FILL_EVENT_HISTORY_ENABLED = false; + public static final boolean DEFAULT_SESSION_FILL_EVENT_HISTORY_ENABLED = true; /** * @hide diff --git a/core/java/android/view/inputmethod/ImeTracker.java b/core/java/android/view/inputmethod/ImeTracker.java index b1ba8b32d2f4..64f41c7a2987 100644 --- a/core/java/android/view/inputmethod/ImeTracker.java +++ b/core/java/android/view/inputmethod/ImeTracker.java @@ -232,6 +232,8 @@ public interface ImeTracker { PHASE_WM_NOTIFY_HIDE_ANIMATION_FINISHED, PHASE_WM_UPDATE_DISPLAY_WINDOW_ANIMATING_TYPES, PHASE_CLIENT_ON_CONTROLS_CHANGED, + PHASE_SERVER_IME_INVOKER, + PHASE_SERVER_CLIENT_INVOKER, }) @Retention(RetentionPolicy.SOURCE) @interface Phase {} @@ -473,6 +475,10 @@ public interface ImeTracker { /** InsetsController received a control for the IME. */ int PHASE_CLIENT_ON_CONTROLS_CHANGED = ImeProtoEnums.PHASE_CLIENT_ON_CONTROLS_CHANGED; + /** Reached the IME invoker on the server. */ + int PHASE_SERVER_IME_INVOKER = ImeProtoEnums.PHASE_SERVER_IME_INVOKER; + /** Reached the IME client invoker on the server. */ + int PHASE_SERVER_CLIENT_INVOKER = ImeProtoEnums.PHASE_SERVER_CLIENT_INVOKER; /** * Called when an IME request is started. diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index b3bd89c2a87d..7d7570d48970 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -620,6 +620,12 @@ public final class InputMethodManager { @GuardedBy("mH") private CompletionInfo[] mCompletions; + /** + * Tracks last pending {@link #startInputInner(int, IBinder, int, int, int)} sequenceId. + */ + @GuardedBy("mH") + private int mLastPendingStartSeqId = INVALID_SEQ_ID; + // Cursor position on the screen. @GuardedBy("mH") @UnsupportedAppUsage @@ -652,6 +658,8 @@ public final class InputMethodManager { private static final String CACHE_KEY_CONNECTIONLESS_STYLUS_HANDWRITING_PROPERTY = "cache_key.system_server.connectionless_stylus_handwriting"; + static final int INVALID_SEQ_ID = -1; + @GuardedBy("mH") private int mCursorSelStart; @GuardedBy("mH") @@ -1193,12 +1201,18 @@ public final class InputMethodManager { case MSG_START_INPUT_RESULT: { final InputBindResult res = (InputBindResult) msg.obj; final int startInputSeq = msg.arg1; - if (res == null) { - // IMMS logs .wtf already. - return; - } - if (DEBUG) Log.v(TAG, "Starting input: Bind result=" + res); synchronized (mH) { + if (mLastPendingStartSeqId == startInputSeq) { + // last pending startInput has been completed. reset. + mLastPendingStartSeqId = INVALID_SEQ_ID; + } + + if (res == null) { + // IMMS logs .wtf already. + return; + } + + if (DEBUG) Log.v(TAG, "Starting input: Bind result=" + res); if (res.id != null) { updateInputChannelLocked(res.channel); mCurMethod = res.method; // for @UnsupportedAppUsage @@ -2220,6 +2234,7 @@ public final class InputMethodManager { } mCompletions = null; mServedConnecting = false; + mLastPendingStartSeqId = INVALID_SEQ_ID; clearConnectionLocked(); } mReportInputConnectionOpenedRunner = null; @@ -3274,6 +3289,9 @@ public final class InputMethodManager { * @param view The view whose text has changed. */ public void restartInput(View view) { + if (DEBUG) { + Log.d(TAG, "restartInput()"); + } // Re-dispatch if there is a context mismatch. final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view); if (fallbackImm != null) { @@ -3351,6 +3369,9 @@ public final class InputMethodManager { */ public void invalidateInput(@NonNull View view) { Objects.requireNonNull(view); + if (DEBUG) { + Log.d(TAG, "IMM#invaldateInput()"); + } // Re-dispatch if there is a context mismatch. final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view); @@ -3363,7 +3384,8 @@ public final class InputMethodManager { if (mServedInputConnection == null || getServedViewLocked() != view) { return; } - mServedInputConnection.scheduleInvalidateInput(); + mServedInputConnection.scheduleInvalidateInput( + mLastPendingStartSeqId != INVALID_SEQ_ID); } } @@ -3532,7 +3554,7 @@ public final class InputMethodManager { ? editorInfo.targetInputMethodUser.getIdentifier() : UserHandle.myUserId(); Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMM.startInputOrWindowGainedFocus"); - int startInputSeq = -1; + int startInputSeq = INVALID_SEQ_ID; if (Flags.useZeroJankProxy()) { // async result delivered via MSG_START_INPUT_RESULT. startInputSeq = IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocusAsync( @@ -3557,6 +3579,9 @@ public final class InputMethodManager { // initialized and ready for use. if (ic != null) { final int seqId = startInputSeq; + if (Flags.invalidateInputCallsRestart()) { + mLastPendingStartSeqId = seqId; + } mReportInputConnectionOpenedRunner = new ReportInputConnectionOpenedRunner(startInputSeq) { @Override @@ -5047,6 +5072,7 @@ public final class InputMethodManager { } p.println(" mServedInputConnection=" + mServedInputConnection); p.println(" mServedInputConnectionHandler=" + mServedInputConnectionHandler); + p.println(" mLastPendingStartSeqId=" + mLastPendingStartSeqId); p.println(" mCompletions=" + Arrays.toString(mCompletions)); p.println(" mCursorRect=" + mCursorRect); p.println(" mCursorSelStart=" + mCursorSelStart diff --git a/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java b/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java index ced27d6d4886..3e8575324352 100644 --- a/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java +++ b/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java @@ -16,6 +16,8 @@ package android.view.inputmethod; +import static android.view.inputmethod.InputMethodManager.INVALID_SEQ_ID; + import static com.android.internal.inputmethod.InputConnectionProtoDumper.buildGetCursorCapsModeProto; import static com.android.internal.inputmethod.InputConnectionProtoDumper.buildGetExtractedTextProto; import static com.android.internal.inputmethod.InputConnectionProtoDumper.buildGetSelectedTextProto; @@ -276,8 +278,19 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub { * make sure that application code is not modifying text context in a reentrant manner.</p> */ public void scheduleInvalidateInput() { + scheduleInvalidateInput(false /* isRestarting */); + } + + /** + * @see #scheduleInvalidateInput() + * @param isRestarting when {@code true}, there is an in-progress restartInput that could race + * with {@link InputMethodManager#invalidateInput(View)}. To prevent race, + * fallback to calling {@link InputMethodManager#restartInput(View)}. + */ + void scheduleInvalidateInput(boolean isRestarting) { if (mHasPendingInvalidation.compareAndSet(false, true)) { - final int nextSessionId = mCurrentSessionId.incrementAndGet(); + final int nextSessionId = + isRestarting ? INVALID_SEQ_ID : mCurrentSessionId.incrementAndGet(); // By calling InputConnection#takeSnapshot() directly from the message loop, we can make // sure that application code is not modifying text context in a reentrant manner. // e.g. We may see methods like EditText#setText() in the callstack here. @@ -330,6 +343,14 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub { } } } + if (isRestarting) { + if (DEBUG) { + Log.d(TAG, "scheduleInvalidateInput called with ongoing restartInput." + + " Fallback to calling restartInput()."); + } + mParentInputMethodManager.restartInput(view); + return; + } if (!alwaysTrueEndBatchEditDetected) { final TextSnapshot textSnapshot = ic.takeSnapshot(); diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 3dfbc2517986..1e0c4906e6ed 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -548,6 +548,25 @@ public class RemoteViews implements Parcelable, Filter { } /** + * Set a view tag associating a View with an ID to be used for widget interaction usage events + * ({@link android.app.usage.UsageEvents.Event}). When this RemoteViews is applied to a bound + * widget, any clicks or scrolls on the tagged view will be reported to + * {@link android.app.usage.UsageStatsManager} using this tag. + * + * @param viewId ID of the View whose tag will be set + * @param tag The integer tag to use for the event + * + * @see android.appwidget.AppWidgetManager#EVENT_TYPE_WIDGET_INTERACTION + * @see android.appwidget.AppWidgetManager#EXTRA_EVENT_CLICKED_VIEWS + * @see android.appwidget.AppWidgetManager#EXTRA_EVENT_SCROLLED_VIEWS + * @see android.app.usage.UsageStatsManager#queryEventsForSelf + */ + @FlaggedApi(Flags.FLAG_ENGAGEMENT_METRICS) + public void setUsageEventTag(@IdRes int viewId, int tag) { + addAction(new SetIntTagAction(viewId, com.android.internal.R.id.remoteViewsMetricsId, tag)); + } + + /** * Set that it is disallowed to reapply another remoteview with the same layout as this view. * This should be done if an action is destroying the view tree of the base layout. * @@ -666,6 +685,14 @@ public class RemoteViews implements Parcelable, Filter { View view, PendingIntent pendingIntent, RemoteResponse response); + + /** + * Invoked when an AbsListView is scrolled. + * @param view view that was scrolled + * + * @hide + */ + default void onScroll(@NonNull AbsListView view) {} } /** @@ -1313,6 +1340,21 @@ public class RemoteViews implements Parcelable, Filter { // a type error. throw new ActionException(throwable); } + if (adapterView instanceof AbsListView listView) { + listView.setOnScrollListener(new AbsListView.OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + if (scrollState != SCROLL_STATE_IDLE) { + params.handler.onScroll(view); + } + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, + int visibleItemCount, int totalItemCount) { + } + }); + } } @Override @@ -1804,6 +1846,19 @@ public class RemoteViews implements Parcelable, Filter { AbsListView v = (AbsListView) target; v.setRemoteViewsAdapter(mIntent, mIsAsync); v.setRemoteViewsInteractionHandler(params.handler); + v.setOnScrollListener(new AbsListView.OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + if (scrollState != SCROLL_STATE_IDLE) { + params.handler.onScroll(view); + } + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, + int visibleItemCount, int totalItemCount) { + } + }); } else if (target instanceof AdapterViewAnimator) { AdapterViewAnimator v = (AdapterViewAnimator) target; v.setRemoteViewsAdapter(mIntent, mIsAsync); @@ -1894,7 +1949,8 @@ public class RemoteViews implements Parcelable, Filter { target.setTagInternal(com.android.internal.R.id.fillInIntent, null); return; } - target.setOnClickListener(v -> mResponse.handleViewInteraction(v, params.handler)); + target.setOnClickListener(v -> + mResponse.handleViewInteraction(v, params.handler)); } @Override diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index d5120ffb4de6..1ea4ce10c60e 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -55,27 +55,27 @@ public enum DesktopModeFlags { ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS( Flags::enableCaptionCompatInsetForceConsumptionAlways, true), ENABLE_CASCADING_WINDOWS(Flags::enableCascadingWindows, true), - ENABLE_DESKTOP_APP_HANDLE_ANIMATION(Flags::enableDesktopAppHandleAnimation, false), + ENABLE_DESKTOP_APP_HANDLE_ANIMATION(Flags::enableDesktopAppHandleAnimation, true), ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS_BUGFIX( Flags::enableDesktopAppLaunchAlttabTransitionsBugfix, true), ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX(Flags::enableDesktopAppLaunchTransitionsBugfix, true), ENABLE_DESKTOP_CLOSE_SHORTCUT_BUGFIX(Flags::enableDesktopCloseShortcutBugfix, false), ENABLE_DESKTOP_COMPAT_UI_VISIBILITY_STATUS(Flags::enableCompatUiVisibilityStatus, true), - ENABLE_DESKTOP_IMMERSIVE_DRAG_BUGFIX(Flags::enableDesktopImmersiveDragBugfix, false), + ENABLE_DESKTOP_IMMERSIVE_DRAG_BUGFIX(Flags::enableDesktopImmersiveDragBugfix, true), ENABLE_DESKTOP_INDICATOR_IN_SEPARATE_THREAD_BUGFIX( - Flags::enableDesktopIndicatorInSeparateThreadBugfix, false), + Flags::enableDesktopIndicatorInSeparateThreadBugfix, true), ENABLE_DESKTOP_OPENING_DEEPLINK_MINIMIZE_ANIMATION_BUGFIX( - Flags::enableDesktopOpeningDeeplinkMinimizeAnimationBugfix, false), + Flags::enableDesktopOpeningDeeplinkMinimizeAnimationBugfix, true), ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX( - Flags::enableDesktopRecentsTransitionsCornersBugfix, false), + Flags::enableDesktopRecentsTransitionsCornersBugfix, true), ENABLE_DESKTOP_SKIP_COMPAT_UI_EDUCATION_IN_DESKTOP_MODE_BUGFIX( Flags::skipCompatUiEducationInDesktopMode, true), ENABLE_DESKTOP_SYSTEM_DIALOGS_TRANSITIONS(Flags::enableDesktopSystemDialogsTransitions, true), ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX( - Flags::enableDesktopTabTearingMinimizeAnimationBugfix, false), + Flags::enableDesktopTabTearingMinimizeAnimationBugfix, true), ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX( - Flags::enableDesktopTrampolineCloseAnimationBugfix, false), + Flags::enableDesktopTrampolineCloseAnimationBugfix, true), ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER( Flags::enableDesktopWallpaperActivityForSystemUser, true), ENABLE_DESKTOP_WINDOWING_APP_TO_WEB(Flags::enableDesktopWindowingAppToWeb, true), @@ -107,17 +107,17 @@ public enum DesktopModeFlags { true), ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD(Flags::enableDragResizeSetUpInBgThread, true), ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX( - Flags::enableDragToDesktopIncomingTransitionsBugfix, false), + Flags::enableDragToDesktopIncomingTransitionsBugfix, true), ENABLE_FULLY_IMMERSIVE_IN_DESKTOP(Flags::enableFullyImmersiveInDesktop, true), ENABLE_HANDLE_INPUT_FIX(Flags::enableHandleInputFix, true), ENABLE_HOLD_TO_DRAG_APP_HANDLE(Flags::enableHoldToDragAppHandle, true), - ENABLE_INPUT_LAYER_TRANSITION_FIX(Flags::enableInputLayerTransitionFix, false), + ENABLE_INPUT_LAYER_TRANSITION_FIX(Flags::enableInputLayerTransitionFix, true), ENABLE_MINIMIZE_BUTTON(Flags::enableMinimizeButton, true), ENABLE_MODALS_FULLSCREEN_WITH_PERMISSIONS(Flags::enableModalsFullscreenWithPermission, true), ENABLE_OPAQUE_BACKGROUND_FOR_TRANSPARENT_WINDOWS( Flags::enableOpaqueBackgroundForTransparentWindows, true), ENABLE_QUICKSWITCH_DESKTOP_SPLIT_BUGFIX(Flags::enableQuickswitchDesktopSplitBugfix, true), - ENABLE_REQUEST_FULLSCREEN_BUGFIX(Flags::enableRequestFullscreenBugfix, false), + ENABLE_REQUEST_FULLSCREEN_BUGFIX(Flags::enableRequestFullscreenBugfix, true), ENABLE_RESIZING_METRICS(Flags::enableResizingMetrics, true), ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE( Flags::enableRestoreToPreviousSizeFromDesktopImmersive, true), @@ -134,7 +134,7 @@ public enum DesktopModeFlags { ENABLE_TOP_VISIBLE_ROOT_TASK_PER_USER_TRACKING(Flags::enableTopVisibleRootTaskPerUserTracking, true), ENABLE_VISUAL_INDICATOR_IN_TRANSITION_BUGFIX( - Flags::enableVisualIndicatorInTransitionBugfix, false), + Flags::enableVisualIndicatorInTransitionBugfix, true), ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS(Flags::enableWindowingDynamicInitialBounds, true), ENABLE_WINDOWING_EDGE_DRAG_RESIZE(Flags::enableWindowingEdgeDragResize, true), ENABLE_WINDOWING_SCALED_RESIZING(Flags::enableWindowingScaledResizing, true), @@ -148,9 +148,9 @@ public enum DesktopModeFlags { INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC( Flags::includeTopTransparentFullscreenTaskInDesktopHeuristic, true), INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES( - Flags::inheritTaskBoundsForTrampolineTaskLaunches, false), + Flags::inheritTaskBoundsForTrampolineTaskLaunches, true), SKIP_DECOR_VIEW_RELAYOUT_WHEN_CLOSING_BUGFIX( - Flags::skipDecorViewRelayoutWhenClosingBugfix, false), + Flags::skipDecorViewRelayoutWhenClosingBugfix, true), // go/keep-sorted end ; diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 0d87b73a5e03..2393edacd116 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -1006,3 +1006,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "enable_desktop_swipe_back_minimize_animation_bugfix" + namespace: "lse_desktop_experience" + description: "Enabling a minimize animation when a window is minimized via a swipe-back navigation gesture in Desktop Windowing mode." + bug: "359343764" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 59dd32258d8c..99f9929e1071 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -388,6 +388,17 @@ flag { } flag { + name: "remove_depart_target_from_motion" + namespace: "windowing_frontend" + description: "Remove DepartingAnimationTarget from BackMotionEvent" + bug: "395035430" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "predictive_back_default_enable_sdk_36" namespace: "systemui" description: "Enable Predictive Back by default with targetSdk>=36" diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig index 94e87c7a034b..f162b1f40d8e 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -125,7 +125,7 @@ flag { } flag { - namespace: "windowing_sdk" + namespace: "car_framework" name: "safe_region_letterboxing" description: "Enables letterboxing for a safe region" bug: "380132497" @@ -152,3 +152,36 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + namespace: "windowing_sdk" + name: "support_widget_intents_on_connected_display" + description: "Launch widget intents on originating display" + bug: "358368849" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + namespace: "windowing_sdk" + name: "exclude_task_from_recents" + description: "Enables WCT to set whether the task should be excluded from the Recents list" + bug: "404726350" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + namespace: "windowing_sdk" + name: "fix_view_root_call_trace" + description: "Do not set mAdded=true unless #setView finished successfully" + bug: "385705687" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/com/android/internal/app/MediaRouteChooserContentManager.java b/core/java/com/android/internal/app/MediaRouteChooserContentManager.java index 09c6f5e6caaa..64538fdbdac1 100644 --- a/core/java/com/android/internal/app/MediaRouteChooserContentManager.java +++ b/core/java/com/android/internal/app/MediaRouteChooserContentManager.java @@ -17,21 +17,57 @@ package com.android.internal.app; import android.content.Context; +import android.media.MediaRouter; +import android.text.TextUtils; import android.view.Gravity; +import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; import android.widget.LinearLayout; import android.widget.ListView; +import android.widget.TextView; import com.android.internal.R; +import java.util.Comparator; + public class MediaRouteChooserContentManager { + /** + * A delegate interface that a MediaRouteChooser UI should implement. It allows the content + * manager to inform the UI of any UI changes that need to be made in response to content + * updates. + */ + public interface Delegate { + /** + * Dismiss the UI to transition to a different workflow. + */ + void dismissView(); + + /** + * Returns true if the progress bar should be shown when the list view is empty. + */ + boolean showProgressBarWhenEmpty(); + } + Context mContext; + Delegate mDelegate; - private final boolean mShowProgressBarWhenEmpty; + private final MediaRouter mRouter; + private final MediaRouterCallback mCallback; - public MediaRouteChooserContentManager(Context context, boolean showProgressBarWhenEmpty) { + private int mRouteTypes; + private RouteAdapter mAdapter; + private boolean mAttachedToWindow; + + public MediaRouteChooserContentManager(Context context, Delegate delegate) { mContext = context; - mShowProgressBarWhenEmpty = showProgressBarWhenEmpty; + mDelegate = delegate; + + mRouter = context.getSystemService(MediaRouter.class); + mCallback = new MediaRouterCallback(); + mAdapter = new RouteAdapter(mContext); } /** @@ -41,9 +77,11 @@ public class MediaRouteChooserContentManager { public void bindViews(View containerView) { View emptyView = containerView.findViewById(android.R.id.empty); ListView listView = containerView.findViewById(R.id.media_route_list); + listView.setAdapter(mAdapter); + listView.setOnItemClickListener(mAdapter); listView.setEmptyView(emptyView); - if (!mShowProgressBarWhenEmpty) { + if (!mDelegate.showProgressBarWhenEmpty()) { containerView.findViewById(R.id.media_route_progress_bar).setVisibility(View.GONE); // Center the empty view when the progress bar is not shown. @@ -53,4 +91,170 @@ public class MediaRouteChooserContentManager { emptyView.setLayoutParams(params); } } + + /** + * Called when this UI is attached to a window.. + */ + public void onAttachedToWindow() { + mAttachedToWindow = true; + mRouter.addCallback(mRouteTypes, mCallback, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); + refreshRoutes(); + } + + /** + * Called when this UI is detached from a window.. + */ + public void onDetachedFromWindow() { + mAttachedToWindow = false; + mRouter.removeCallback(mCallback); + } + + /** + * Gets the media route types for filtering the routes that the user can + * select using the media route chooser dialog. + * + * @return The route types. + */ + public int getRouteTypes() { + return mRouteTypes; + } + + /** + * Sets the types of routes that will be shown in the media route chooser dialog + * launched by this button. + * + * @param types The route types to match. + */ + public void setRouteTypes(int types) { + if (mRouteTypes != types) { + mRouteTypes = types; + + if (mAttachedToWindow) { + mRouter.removeCallback(mCallback); + mRouter.addCallback(types, mCallback, + MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); + } + + refreshRoutes(); + } + } + + /** + * Refreshes the list of routes that are shown in the chooser dialog. + */ + public void refreshRoutes() { + if (mAttachedToWindow) { + mAdapter.update(); + } + } + + /** + * Returns true if the route should be included in the list. + * <p> + * The default implementation returns true for enabled non-default routes that + * match the route types. Subclasses can override this method to filter routes + * differently. + * </p> + * + * @param route The route to consider, never null. + * @return True if the route should be included in the chooser dialog. + */ + public boolean onFilterRoute(MediaRouter.RouteInfo route) { + return !route.isDefault() && route.isEnabled() && route.matchesTypes(mRouteTypes); + } + + private final class RouteAdapter extends ArrayAdapter<MediaRouter.RouteInfo> + implements AdapterView.OnItemClickListener { + private final LayoutInflater mInflater; + + RouteAdapter(Context context) { + super(context, 0); + mInflater = LayoutInflater.from(context); + } + + public void update() { + clear(); + final int count = mRouter.getRouteCount(); + for (int i = 0; i < count; i++) { + MediaRouter.RouteInfo route = mRouter.getRouteAt(i); + if (onFilterRoute(route)) { + add(route); + } + } + sort(RouteComparator.sInstance); + notifyDataSetChanged(); + } + + @Override + public boolean areAllItemsEnabled() { + return false; + } + + @Override + public boolean isEnabled(int position) { + return getItem(position).isEnabled(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = convertView; + if (view == null) { + view = mInflater.inflate(R.layout.media_route_list_item, parent, false); + } + MediaRouter.RouteInfo route = getItem(position); + TextView text1 = view.findViewById(android.R.id.text1); + TextView text2 = view.findViewById(android.R.id.text2); + text1.setText(route.getName()); + CharSequence description = route.getDescription(); + if (TextUtils.isEmpty(description)) { + text2.setVisibility(View.GONE); + text2.setText(""); + } else { + text2.setVisibility(View.VISIBLE); + text2.setText(description); + } + view.setEnabled(route.isEnabled()); + return view; + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + MediaRouter.RouteInfo route = getItem(position); + if (route.isEnabled()) { + route.select(); + mDelegate.dismissView(); + } + } + } + + private static final class RouteComparator implements Comparator<MediaRouter.RouteInfo> { + public static final RouteComparator sInstance = new RouteComparator(); + + @Override + public int compare(MediaRouter.RouteInfo lhs, MediaRouter.RouteInfo rhs) { + return lhs.getName().toString().compareTo(rhs.getName().toString()); + } + } + + private final class MediaRouterCallback extends MediaRouter.SimpleCallback { + @Override + public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) { + refreshRoutes(); + } + + @Override + public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) { + refreshRoutes(); + } + + @Override + public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) { + refreshRoutes(); + } + + @Override + public void onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo info) { + mDelegate.dismissView(); + } + } } diff --git a/core/java/com/android/internal/app/MediaRouteChooserDialog.java b/core/java/com/android/internal/app/MediaRouteChooserDialog.java index 5030a143ea94..fc7ed89f395c 100644 --- a/core/java/com/android/internal/app/MediaRouteChooserDialog.java +++ b/core/java/com/android/internal/app/MediaRouteChooserDialog.java @@ -19,23 +19,14 @@ package com.android.internal.app; import android.app.AlertDialog; import android.content.Context; import android.media.MediaRouter; -import android.media.MediaRouter.RouteInfo; import android.os.Bundle; -import android.text.TextUtils; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; import android.widget.Button; -import android.widget.ListView; -import android.widget.TextView; import com.android.internal.R; -import java.util.Comparator; - /** * This class implements the route chooser dialog for {@link MediaRouter}. * <p> @@ -47,15 +38,11 @@ import java.util.Comparator; * * TODO: Move this back into the API, as in the support library media router. */ -public class MediaRouteChooserDialog extends AlertDialog { - private final MediaRouter mRouter; - private final MediaRouterCallback mCallback; - - private int mRouteTypes; +public class MediaRouteChooserDialog extends AlertDialog implements + MediaRouteChooserContentManager.Delegate { private View.OnClickListener mExtendedSettingsClickListener; - private RouteAdapter mAdapter; private Button mExtendedSettingsButton; - private boolean mAttachedToWindow; + private final boolean mShowProgressBarWhenEmpty; private final MediaRouteChooserContentManager mContentManager; @@ -66,19 +53,8 @@ public class MediaRouteChooserDialog extends AlertDialog { public MediaRouteChooserDialog(Context context, int theme, boolean showProgressBarWhenEmpty) { super(context, theme); - mRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE); - mCallback = new MediaRouterCallback(); - mContentManager = new MediaRouteChooserContentManager(context, showProgressBarWhenEmpty); - } - - /** - * Gets the media route types for filtering the routes that the user can - * select using the media route chooser dialog. - * - * @return The route types. - */ - public int getRouteTypes() { - return mRouteTypes; + mShowProgressBarWhenEmpty = showProgressBarWhenEmpty; + mContentManager = new MediaRouteChooserContentManager(context, this); } /** @@ -88,17 +64,7 @@ public class MediaRouteChooserDialog extends AlertDialog { * @param types The route types to match. */ public void setRouteTypes(int types) { - if (mRouteTypes != types) { - mRouteTypes = types; - - if (mAttachedToWindow) { - mRouter.removeCallback(mCallback); - mRouter.addCallback(types, mCallback, - MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); - } - - refreshRoutes(); - } + mContentManager.setRouteTypes(types); } public void setExtendedSettingsClickListener(View.OnClickListener listener) { @@ -108,21 +74,6 @@ public class MediaRouteChooserDialog extends AlertDialog { } } - /** - * Returns true if the route should be included in the list. - * <p> - * The default implementation returns true for enabled non-default routes that - * match the route types. Subclasses can override this method to filter routes - * differently. - * </p> - * - * @param route The route to consider, never null. - * @return True if the route should be included in the chooser dialog. - */ - public boolean onFilterRoute(MediaRouter.RouteInfo route) { - return !route.isDefault() && route.isEnabled() && route.matchesTypes(mRouteTypes); - } - @Override protected void onCreate(Bundle savedInstanceState) { // Note: setView must be called before super.onCreate(). @@ -130,7 +81,7 @@ public class MediaRouteChooserDialog extends AlertDialog { R.layout.media_route_chooser_dialog, null); setView(containerView); - setTitle(mRouteTypes == MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY + setTitle(mContentManager.getRouteTypes() == MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY ? R.string.media_route_chooser_title_for_remote_display : R.string.media_route_chooser_title); @@ -139,11 +90,6 @@ public class MediaRouteChooserDialog extends AlertDialog { super.onCreate(savedInstanceState); - mAdapter = new RouteAdapter(getContext()); - ListView listView = findViewById(R.id.media_route_list); - listView.setAdapter(mAdapter); - listView.setOnItemClickListener(mAdapter); - mExtendedSettingsButton = findViewById(R.id.media_route_extended_settings_button); updateExtendedSettingsButton(); @@ -161,27 +107,23 @@ public class MediaRouteChooserDialog extends AlertDialog { @Override public void onAttachedToWindow() { super.onAttachedToWindow(); - - mAttachedToWindow = true; - mRouter.addCallback(mRouteTypes, mCallback, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); - refreshRoutes(); + mContentManager.onAttachedToWindow(); } @Override public void onDetachedFromWindow() { - mAttachedToWindow = false; - mRouter.removeCallback(mCallback); - + mContentManager.onDetachedFromWindow(); super.onDetachedFromWindow(); } - /** - * Refreshes the list of routes that are shown in the chooser dialog. - */ - public void refreshRoutes() { - if (mAttachedToWindow) { - mAdapter.update(); - } + @Override + public void dismissView() { + dismiss(); + } + + @Override + public boolean showProgressBarWhenEmpty() { + return mShowProgressBarWhenEmpty; } static boolean isLightTheme(Context context) { @@ -189,99 +131,4 @@ public class MediaRouteChooserDialog extends AlertDialog { return context.getTheme().resolveAttribute(R.attr.isLightTheme, value, true) && value.data != 0; } - - private final class RouteAdapter extends ArrayAdapter<MediaRouter.RouteInfo> - implements ListView.OnItemClickListener { - private final LayoutInflater mInflater; - - public RouteAdapter(Context context) { - super(context, 0); - mInflater = LayoutInflater.from(context); - } - - public void update() { - clear(); - final int count = mRouter.getRouteCount(); - for (int i = 0; i < count; i++) { - MediaRouter.RouteInfo route = mRouter.getRouteAt(i); - if (onFilterRoute(route)) { - add(route); - } - } - sort(RouteComparator.sInstance); - notifyDataSetChanged(); - } - - @Override - public boolean areAllItemsEnabled() { - return false; - } - - @Override - public boolean isEnabled(int position) { - return getItem(position).isEnabled(); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View view = convertView; - if (view == null) { - view = mInflater.inflate(R.layout.media_route_list_item, parent, false); - } - MediaRouter.RouteInfo route = getItem(position); - TextView text1 = view.findViewById(android.R.id.text1); - TextView text2 = view.findViewById(android.R.id.text2); - text1.setText(route.getName()); - CharSequence description = route.getDescription(); - if (TextUtils.isEmpty(description)) { - text2.setVisibility(View.GONE); - text2.setText(""); - } else { - text2.setVisibility(View.VISIBLE); - text2.setText(description); - } - view.setEnabled(route.isEnabled()); - return view; - } - - @Override - public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - MediaRouter.RouteInfo route = getItem(position); - if (route.isEnabled()) { - route.select(); - dismiss(); - } - } - } - - private final class MediaRouterCallback extends MediaRouter.SimpleCallback { - @Override - public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) { - refreshRoutes(); - } - - @Override - public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) { - refreshRoutes(); - } - - @Override - public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) { - refreshRoutes(); - } - - @Override - public void onRouteSelected(MediaRouter router, int type, RouteInfo info) { - dismiss(); - } - } - - private static final class RouteComparator implements Comparator<MediaRouter.RouteInfo> { - public static final RouteComparator sInstance = new RouteComparator(); - - @Override - public int compare(MediaRouter.RouteInfo lhs, MediaRouter.RouteInfo rhs) { - return lhs.getName().toString().compareTo(rhs.getName().toString()); - } - } } diff --git a/core/java/com/android/internal/content/FileSystemProvider.java b/core/java/com/android/internal/content/FileSystemProvider.java index 0801dd8c0bd8..fc74a179f66e 100644 --- a/core/java/com/android/internal/content/FileSystemProvider.java +++ b/core/java/com/android/internal/content/FileSystemProvider.java @@ -119,7 +119,7 @@ public abstract class FileSystemProvider extends DocumentsProvider { * Callback indicating that the given document has been deleted or moved. This gives * the provider a hook to revoke the uri permissions. */ - protected void onDocIdDeleted(String docId) { + protected void onDocIdDeleted(String docId, boolean shouldRevokeUriPermission) { // Default is no-op } @@ -292,7 +292,6 @@ public abstract class FileSystemProvider extends DocumentsProvider { final String afterDocId = getDocIdForFile(after); onDocIdChanged(docId); - onDocIdDeleted(docId); onDocIdChanged(afterDocId); final File afterVisibleFile = getFileForDocId(afterDocId, true); @@ -301,6 +300,10 @@ public abstract class FileSystemProvider extends DocumentsProvider { updateMediaStore(getContext(), afterVisibleFile); if (!TextUtils.equals(docId, afterDocId)) { + // DocumentsProvider handles the revoking / granting uri permission for the docId and + // the afterDocId in the renameDocument case. Don't need to call revokeUriPermission + // for the docId here. + onDocIdDeleted(docId, /* shouldRevokeUriPermission */ false); return afterDocId; } else { return null; @@ -324,7 +327,7 @@ public abstract class FileSystemProvider extends DocumentsProvider { final String docId = getDocIdForFile(after); onDocIdChanged(sourceDocumentId); - onDocIdDeleted(sourceDocumentId); + onDocIdDeleted(sourceDocumentId, /* shouldRevokeUriPermission */ true); onDocIdChanged(docId); // update the database updateMediaStore(getContext(), visibleFileBefore); @@ -362,7 +365,7 @@ public abstract class FileSystemProvider extends DocumentsProvider { } onDocIdChanged(docId); - onDocIdDeleted(docId); + onDocIdDeleted(docId, /* shouldRevokeUriPermission */ true); updateMediaStore(getContext(), visibleFile); } diff --git a/core/java/com/android/internal/inputmethod/InputMethodDebug.java b/core/java/com/android/internal/inputmethod/InputMethodDebug.java index 4d5e67ab8fde..9cdfc02e2e28 100644 --- a/core/java/com/android/internal/inputmethod/InputMethodDebug.java +++ b/core/java/com/android/internal/inputmethod/InputMethodDebug.java @@ -305,6 +305,8 @@ public final class InputMethodDebug { return "HIDE_INPUT_TARGET_CHANGED"; case SoftInputShowHideReason.HIDE_WINDOW_LOST_FOCUS: return "HIDE_WINDOW_LOST_FOCUS"; + case SoftInputShowHideReason.IME_REQUESTED_CHANGED_LISTENER: + return "IME_REQUESTED_CHANGED_LISTENER"; default: return "Unknown=" + reason; } diff --git a/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java b/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java index cf0580c2f021..8b4371ea8478 100644 --- a/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java +++ b/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java @@ -92,6 +92,7 @@ import java.lang.annotation.Retention; SoftInputShowHideReason.SHOW_INPUT_TARGET_CHANGED, SoftInputShowHideReason.HIDE_INPUT_TARGET_CHANGED, SoftInputShowHideReason.HIDE_WINDOW_LOST_FOCUS, + SoftInputShowHideReason.IME_REQUESTED_CHANGED_LISTENER, }) public @interface SoftInputShowHideReason { /** Default, undefined reason. */ @@ -422,4 +423,10 @@ public @interface SoftInputShowHideReason { /** Hide soft input when the window lost focus. */ int HIDE_WINDOW_LOST_FOCUS = ImeProtoEnums.REASON_HIDE_WINDOW_LOST_FOCUS; + + /** + * Show / Hide soft input by + * {@link com.android.server.wm.WindowManagerInternal.OnImeRequestedChangedListener} + */ + int IME_REQUESTED_CHANGED_LISTENER = ImeProtoEnums.REASON_IME_REQUESTED_CHANGED_LISTENER; } diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java index f1c47a7a023b..3fc74c9f1f54 100644 --- a/core/java/com/android/internal/os/BatteryStatsHistory.java +++ b/core/java/com/android/internal/os/BatteryStatsHistory.java @@ -79,7 +79,7 @@ public class BatteryStatsHistory { private static final String TAG = "BatteryStatsHistory"; // Current on-disk Parcel version. Must be updated when the format of the parcelable changes - private static final int VERSION = 213; + private static final int VERSION = 214; // Part of initial delta int that specifies the time delta. static final int DELTA_TIME_MASK = 0x7ffff; @@ -316,7 +316,6 @@ public class BatteryStatsHistory { } private final Parcel mHistoryBuffer; - private final HistoryStepDetailsCalculator mStepDetailsCalculator; private final Clock mClock; private int mMaxHistoryBufferSize; @@ -337,25 +336,6 @@ public class BatteryStatsHistory { */ private List<Parcel> mHistoryParcels = null; - /** - * When iterating history files, the current file index. - */ - private BatteryHistoryFragment mCurrentFragment; - - /** - * When iterating history files, the current file parcel. - */ - private Parcel mCurrentParcel; - /** - * When iterating history file, the current parcel's Parcel.dataSize(). - */ - private int mCurrentParcelEnd; - /** - * Used when BatteryStatsImpl object is created from deserialization of a parcel, - * such as Settings app or checkin file, to iterate over history parcels. - */ - private int mParcelIndex = 0; - private final ReentrantLock mWriteLock = new ReentrantLock(); private final HistoryItem mHistoryCur = new HistoryItem(); @@ -384,28 +364,11 @@ public class BatteryStatsHistory { // Monotonically increasing size of written history private long mMonotonicHistorySize; private final ArraySet<PowerStats.Descriptor> mWrittenPowerStatsDescriptors = new ArraySet<>(); - private byte mLastHistoryStepLevel = 0; private boolean mMutable = true; private int mIteratorCookie; private final BatteryStatsHistory mWritableHistory; /** - * A delegate responsible for computing additional details for a step in battery history. - */ - public interface HistoryStepDetailsCalculator { - /** - * Returns additional details for the current history step or null. - */ - @Nullable - HistoryStepDetails getHistoryStepDetails(); - - /** - * Resets the calculator to get ready for a new battery session - */ - void clear(); - } - - /** * A delegate for android.os.Trace to allow testing static calls. Due to * limitations in Android Tracing (b/153319140), the delegate also records * counter values in system properties which allows reading the value at the @@ -472,21 +435,17 @@ public class BatteryStatsHistory { * @param maxHistoryBufferSize the most amount of RAM to used for buffering of history steps */ public BatteryStatsHistory(Parcel historyBuffer, int maxHistoryBufferSize, - @Nullable BatteryHistoryStore store, HistoryStepDetailsCalculator stepDetailsCalculator, - Clock clock, MonotonicClock monotonicClock, TraceDelegate tracer, - EventLogger eventLogger) { - this(historyBuffer, maxHistoryBufferSize, store, - stepDetailsCalculator, - clock, monotonicClock, tracer, eventLogger, null); + @Nullable BatteryHistoryStore store, Clock clock, MonotonicClock monotonicClock, + TraceDelegate tracer, EventLogger eventLogger) { + this(historyBuffer, maxHistoryBufferSize, store, clock, monotonicClock, tracer, eventLogger, + null); } private BatteryStatsHistory(@Nullable Parcel historyBuffer, int maxHistoryBufferSize, - @Nullable BatteryHistoryStore store, - @NonNull HistoryStepDetailsCalculator stepDetailsCalculator, @NonNull Clock clock, + @Nullable BatteryHistoryStore store, @NonNull Clock clock, @NonNull MonotonicClock monotonicClock, @NonNull TraceDelegate tracer, @NonNull EventLogger eventLogger, @Nullable BatteryStatsHistory writableHistory) { mMaxHistoryBufferSize = maxHistoryBufferSize; - mStepDetailsCalculator = stepDetailsCalculator; mTracer = tracer; mClock = clock; mMonotonicClock = monotonicClock; @@ -527,7 +486,6 @@ public class BatteryStatsHistory { mClock = Clock.SYSTEM_CLOCK; mTracer = null; mStore = null; - mStepDetailsCalculator = null; mEventLogger = new EventLogger(); mWritableHistory = null; mMutable = false; @@ -556,9 +514,6 @@ public class BatteryStatsHistory { mNextHistoryTagIdx = 0; mNumHistoryTagChars = 0; mHistoryBufferLastPos = -1; - if (mStepDetailsCalculator != null) { - mStepDetailsCalculator.clear(); - } } /** @@ -596,7 +551,7 @@ public class BatteryStatsHistory { Parcel historyBufferCopy = Parcel.obtain(); historyBufferCopy.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize()); - return new BatteryStatsHistory(historyBufferCopy, 0, mStore, null, + return new BatteryStatsHistory(historyBufferCopy, 0, mStore, null, null, null, mEventLogger, this); } } finally { @@ -712,10 +667,6 @@ public class BatteryStatsHistory { if (mStore != null) { mStore.lock(); } - mCurrentFragment = null; - mCurrentParcel = null; - mCurrentParcelEnd = 0; - mParcelIndex = 0; BatteryStatsHistoryIterator iterator = new BatteryStatsHistoryIterator( this, startTimeMs, endTimeMs); mIteratorCookie = System.identityHashCode(iterator); @@ -1614,6 +1565,21 @@ public class BatteryStatsHistory { } /** + * Records an update containing HistoryStepDetails, except if the details are empty. + */ + public void recordHistoryStepDetails(HistoryStepDetails details, long elapsedRealtimeMs, + long uptimeMs) { + if (details.isEmpty()) { + return; + } + synchronized (this) { + mHistoryCur.stepDetails = details; + writeHistoryItem(elapsedRealtimeMs, uptimeMs); + mHistoryCur.stepDetails = null; + } + } + + /** * Writes the current history item to history. */ public void writeHistoryItem(long elapsedRealtimeMs, long uptimeMs) { @@ -1632,8 +1598,8 @@ public class BatteryStatsHistory { mHistoryAddTmp.processStateChange = null; mHistoryAddTmp.eventCode = HistoryItem.EVENT_NONE; mHistoryAddTmp.states &= ~HistoryItem.STATE_CPU_RUNNING_FLAG; + mHistoryAddTmp.stepDetails = null; writeHistoryItem(wakeElapsedTimeMs, uptimeMs, mHistoryAddTmp); - } } mHistoryCur.states |= HistoryItem.STATE_CPU_RUNNING_FLAG; @@ -1952,15 +1918,8 @@ public class BatteryStatsHistory { } int firstToken = deltaTimeToken | (cur.states & BatteryStatsHistory.DELTA_STATE_MASK); - if (cur.batteryLevel < mLastHistoryStepLevel || mLastHistoryStepLevel == 0) { - cur.stepDetails = mStepDetailsCalculator.getHistoryStepDetails(); - if (cur.stepDetails != null) { - batteryLevelInt |= BatteryStatsHistory.BATTERY_LEVEL_DETAILS_FLAG; - mLastHistoryStepLevel = cur.batteryLevel; - } - } else { - cur.stepDetails = null; - mLastHistoryStepLevel = cur.batteryLevel; + if (cur.stepDetails != null) { + batteryLevelInt |= BatteryStatsHistory.BATTERY_LEVEL_DETAILS_FLAG; } final boolean batteryLevelIntChanged = batteryLevelInt != 0; @@ -2055,6 +2014,7 @@ public class BatteryStatsHistory { + Integer.toHexString(cur.states2)); } } + cur.tagsFirstOccurrence = false; if (cur.wakelockTag != null || cur.wakeReasonTag != null) { int wakeLockIndex; int wakeReasonIndex; diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java index 3d81e4fc7acd..e20a52b24485 100644 --- a/core/java/com/android/internal/policy/DecorView.java +++ b/core/java/com/android/internal/policy/DecorView.java @@ -120,7 +120,6 @@ import com.android.internal.view.menu.MenuHelper; import com.android.internal.widget.ActionBarContextView; import com.android.internal.widget.BackgroundFallback; import com.android.internal.widget.floatingtoolbar.FloatingToolbar; -import com.android.window.flags.Flags; import java.util.List; import java.util.concurrent.Executor; @@ -1004,8 +1003,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind public void onWindowSystemUiVisibilityChanged(int visible) { updateColorViews(null /* insets */, true /* animate */); - if (!Flags.actionModeEdgeToEdge() - && mStatusGuard != null && mStatusGuard.getVisibility() == VISIBLE) { + if (mStatusGuard != null && mStatusGuard.getVisibility() == VISIBLE) { updateStatusGuardColor(); } } @@ -1042,7 +1040,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind } mFrameOffsets.set(insets.getSystemWindowInsetsAsRect()); insets = updateColorViews(insets, true /* animate */); - insets = updateActionModeInsets(insets); + insets = updateStatusGuard(insets); if (getForeground() != null) { drawableChanged(); } @@ -1594,7 +1592,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind } } - private WindowInsets updateActionModeInsets(WindowInsets insets) { + private WindowInsets updateStatusGuard(WindowInsets insets) { boolean showStatusGuard = false; // Show the status guard when the non-overlay contextual action bar is showing if (mPrimaryActionModeView != null) { @@ -1610,78 +1608,54 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind final Rect rect = mTempRect; // Apply the insets that have not been applied by the contentParent yet. - final WindowInsets innerInsets = + WindowInsets innerInsets = mWindow.mContentParent.computeSystemWindowInsets(insets, rect); - final boolean consumesSystemWindowInsetsTop; - if (Flags.actionModeEdgeToEdge()) { - final Insets newPadding = innerInsets.getSystemWindowInsets(); - final Insets newMargin = innerInsets.getInsets( - WindowInsets.Type.navigationBars()); - - // Don't extend into navigation bar area so the width can align with status - // bar color view. - if (mlp.leftMargin != newMargin.left - || mlp.rightMargin != newMargin.right) { - mlpChanged = true; - mlp.leftMargin = newMargin.left; - mlp.rightMargin = newMargin.right; - } - - mPrimaryActionModeView.setPadding( - newPadding.left - newMargin.left, - newPadding.top, - newPadding.right - newMargin.right, - 0); - consumesSystemWindowInsetsTop = newPadding.top > 0; - } else { - int newTopMargin = innerInsets.getSystemWindowInsetTop(); - int newLeftMargin = innerInsets.getSystemWindowInsetLeft(); - int newRightMargin = innerInsets.getSystemWindowInsetRight(); - - // Must use root window insets for the guard, because the color views - // consume the navigation bar inset if the window does not request - // LAYOUT_HIDE_NAV - but the status guard is attached at the root. - WindowInsets rootInsets = getRootWindowInsets(); - int newGuardLeftMargin = rootInsets.getSystemWindowInsetLeft(); - int newGuardRightMargin = rootInsets.getSystemWindowInsetRight(); - - if (mlp.topMargin != newTopMargin || mlp.leftMargin != newLeftMargin - || mlp.rightMargin != newRightMargin) { - mlpChanged = true; - mlp.topMargin = newTopMargin; - mlp.leftMargin = newLeftMargin; - mlp.rightMargin = newRightMargin; - } + int newTopMargin = innerInsets.getSystemWindowInsetTop(); + int newLeftMargin = innerInsets.getSystemWindowInsetLeft(); + int newRightMargin = innerInsets.getSystemWindowInsetRight(); + + // Must use root window insets for the guard, because the color views consume + // the navigation bar inset if the window does not request LAYOUT_HIDE_NAV - but + // the status guard is attached at the root. + WindowInsets rootInsets = getRootWindowInsets(); + int newGuardLeftMargin = rootInsets.getSystemWindowInsetLeft(); + int newGuardRightMargin = rootInsets.getSystemWindowInsetRight(); + + if (mlp.topMargin != newTopMargin || mlp.leftMargin != newLeftMargin + || mlp.rightMargin != newRightMargin) { + mlpChanged = true; + mlp.topMargin = newTopMargin; + mlp.leftMargin = newLeftMargin; + mlp.rightMargin = newRightMargin; + } - if (newTopMargin > 0 && mStatusGuard == null) { - mStatusGuard = new View(mContext); - mStatusGuard.setVisibility(GONE); - final LayoutParams lp = new LayoutParams(MATCH_PARENT, - mlp.topMargin, Gravity.LEFT | Gravity.TOP); + if (newTopMargin > 0 && mStatusGuard == null) { + mStatusGuard = new View(mContext); + mStatusGuard.setVisibility(GONE); + final LayoutParams lp = new LayoutParams(MATCH_PARENT, + mlp.topMargin, Gravity.LEFT | Gravity.TOP); + lp.leftMargin = newGuardLeftMargin; + lp.rightMargin = newGuardRightMargin; + addView(mStatusGuard, indexOfChild(mStatusColorViewState.view), lp); + } else if (mStatusGuard != null) { + final LayoutParams lp = (LayoutParams) + mStatusGuard.getLayoutParams(); + if (lp.height != mlp.topMargin || lp.leftMargin != newGuardLeftMargin + || lp.rightMargin != newGuardRightMargin) { + lp.height = mlp.topMargin; lp.leftMargin = newGuardLeftMargin; lp.rightMargin = newGuardRightMargin; - addView(mStatusGuard, indexOfChild(mStatusColorViewState.view), lp); - } else if (mStatusGuard != null) { - final LayoutParams lp = (LayoutParams) - mStatusGuard.getLayoutParams(); - if (lp.height != mlp.topMargin || lp.leftMargin != newGuardLeftMargin - || lp.rightMargin != newGuardRightMargin) { - lp.height = mlp.topMargin; - lp.leftMargin = newGuardLeftMargin; - lp.rightMargin = newGuardRightMargin; - mStatusGuard.setLayoutParams(lp); - } + mStatusGuard.setLayoutParams(lp); } + } - // The action mode's theme may differ from the app, so - // always show the status guard above it if we have one. - showStatusGuard = mStatusGuard != null; + // The action mode's theme may differ from the app, so + // always show the status guard above it if we have one. + showStatusGuard = mStatusGuard != null; - if (showStatusGuard && mStatusGuard.getVisibility() != VISIBLE) { - // If it wasn't previously shown, the color may be stale - updateStatusGuardColor(); - } - consumesSystemWindowInsetsTop = showStatusGuard; + if (showStatusGuard && mStatusGuard.getVisibility() != VISIBLE) { + // If it wasn't previously shown, the color may be stale + updateStatusGuardColor(); } // We only need to consume the insets if the action @@ -1690,16 +1664,14 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind // screen_simple_overlay_action_mode.xml). final boolean nonOverlay = (mWindow.getLocalFeaturesPrivate() & (1 << Window.FEATURE_ACTION_MODE_OVERLAY)) == 0; - if (nonOverlay && consumesSystemWindowInsetsTop) { + if (nonOverlay && showStatusGuard) { insets = insets.inset(0, insets.getSystemWindowInsetTop(), 0, 0); } } else { - if (!Flags.actionModeEdgeToEdge()) { - // reset top margin - if (mlp.topMargin != 0 || mlp.leftMargin != 0 || mlp.rightMargin != 0) { - mlpChanged = true; - mlp.topMargin = 0; - } + // reset top margin + if (mlp.topMargin != 0 || mlp.leftMargin != 0 || mlp.rightMargin != 0) { + mlpChanged = true; + mlp.topMargin = 0; } } if (mlpChanged) { @@ -1707,7 +1679,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind } } } - if (!Flags.actionModeEdgeToEdge() && mStatusGuard != null) { + if (mStatusGuard != null) { mStatusGuard.setVisibility(showStatusGuard ? VISIBLE : GONE); } return insets; @@ -2211,7 +2183,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind for (int i = getChildCount() - 1; i >= 0; i--) { View v = getChildAt(i); if (v != mStatusColorViewState.view && v != mNavigationColorViewState.view - && (Flags.actionModeEdgeToEdge() || v != mStatusGuard)) { + && v != mStatusGuard) { removeViewAt(i); } } diff --git a/core/java/com/android/internal/policy/KeyInterceptionInfo.java b/core/java/com/android/internal/policy/KeyInterceptionInfo.java index fed8fe3b4cc0..b20f6d225b69 100644 --- a/core/java/com/android/internal/policy/KeyInterceptionInfo.java +++ b/core/java/com/android/internal/policy/KeyInterceptionInfo.java @@ -27,13 +27,11 @@ public class KeyInterceptionInfo { // Debug friendly name to help identify the window public final String windowTitle; public final int windowOwnerUid; - public final int inputFeaturesFlags; - public KeyInterceptionInfo(int type, int flags, String title, int uid, int inputFeaturesFlags) { + public KeyInterceptionInfo(int type, int flags, String title, int uid) { layoutParamsType = type; layoutParamsPrivateFlags = flags; windowTitle = title; windowOwnerUid = uid; - this.inputFeaturesFlags = inputFeaturesFlags; } } diff --git a/core/java/com/android/internal/policy/SystemBarUtils.java b/core/java/com/android/internal/policy/SystemBarUtils.java index 783c68695fb3..e5badc87fb13 100644 --- a/core/java/com/android/internal/policy/SystemBarUtils.java +++ b/core/java/com/android/internal/policy/SystemBarUtils.java @@ -66,12 +66,14 @@ public final class SystemBarUtils { display.getDisplayInfo(info); Insets insets; Insets waterfallInsets; + final int localWidth = context.getResources().getDisplayMetrics().widthPixels; + final int localHeight = context.getResources().getDisplayMetrics().heightPixels; if (cutout == null) { insets = Insets.NONE; waterfallInsets = Insets.NONE; } else { DisplayCutout rotated = - cutout.getRotated(info.logicalWidth, info.logicalHeight, rotation, targetRot); + cutout.getRotated(localWidth, localHeight, rotation, targetRot); insets = Insets.of(rotated.getSafeInsets()); waterfallInsets = rotated.getWaterfallInsets(); } diff --git a/core/java/com/android/internal/protolog/LogcatOnlyProtoLogImpl.java b/core/java/com/android/internal/protolog/LogcatOnlyProtoLogImpl.java index 34e04181388d..256a3ffd6456 100644 --- a/core/java/com/android/internal/protolog/LogcatOnlyProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/LogcatOnlyProtoLogImpl.java @@ -19,6 +19,7 @@ package com.android.internal.protolog; import static com.android.internal.protolog.ProtoLog.REQUIRE_PROTOLOGTOOL; import android.annotation.NonNull; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; import android.text.TextUtils; import android.util.Log; @@ -31,14 +32,15 @@ import java.util.Collections; import java.util.List; /** - * Class only create and used to server temporarily for when there is source code pre-processing by + * Class only created and used to serve temporarily for when there is source code pre-processing by * the ProtoLog tool, when the tracing to Perfetto flag is off, and the static REQUIRE_PROTOLOGTOOL * boolean is false. In which case we simply want to log protolog message to logcat. Note, that this * means that in such cases there is no real advantage of using protolog over logcat. * - * @deprecated Should not be used. This is just a temporary class to support a legacy behavior. + * NOTE: Should not be used in real products as this mostly removes the benefits of protolog. This + * is just a temporary class to support a legacy behavior and tests running on the host-side. */ -@Deprecated +@RavenwoodKeepWholeClass public class LogcatOnlyProtoLogImpl implements IProtoLog { private static final String LOG_TAG = LogcatOnlyProtoLogImpl.class.getName(); diff --git a/core/java/com/android/internal/protolog/ProtoLog.java b/core/java/com/android/internal/protolog/ProtoLog.java index c81af959f36c..915b981a7e7a 100644 --- a/core/java/com/android/internal/protolog/ProtoLog.java +++ b/core/java/com/android/internal/protolog/ProtoLog.java @@ -17,6 +17,8 @@ package com.android.internal.protolog; import android.os.ServiceManager; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; +import android.ravenwood.annotation.RavenwoodReplace; import android.tracing.perfetto.DataSourceParams; import android.tracing.perfetto.InitArguments; import android.tracing.perfetto.Producer; @@ -47,6 +49,7 @@ import java.util.HashSet; * Methods in this class are stubs, that are replaced by optimised versions by the ProtoLogTool * during build. */ +@RavenwoodKeepWholeClass // LINT.IfChange public class ProtoLog { // LINT.ThenChange(frameworks/base/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt) @@ -73,13 +76,25 @@ public class ProtoLog { // These tracing instances are only used when we cannot or do not preprocess the source // files to extract out the log strings. Otherwise, the trace calls are replaced with calls // directly to the generated tracing implementations. - if (android.tracing.Flags.perfettoProtologTracing()) { - initializePerfettoProtoLog(groups); - } else { + if (logOnlyToLogcat()) { sProtoLogInstance = new LogcatOnlyProtoLogImpl(); + } else { + initializePerfettoProtoLog(groups); } } + @RavenwoodReplace(reason = "Always use the Log backend on ravenwood, not Perfetto") + private static boolean logOnlyToLogcat() { + return !android.tracing.Flags.perfettoProtologTracing(); + } + + private static boolean logOnlyToLogcat$ravenwood() { + // We don't want to initialize Perfetto data sources and have to deal with Perfetto + // when running tests on the host side, instead just log everything to logcat which has + // already been made compatible with ravenwood. + return true; + } + private static void initializePerfettoProtoLog(IProtoLogGroup... groups) { var datasource = getSharedSingleInstanceDataSource(); diff --git a/core/java/com/android/internal/protolog/common/LogLevel.java b/core/java/com/android/internal/protolog/common/LogLevel.java index b5541ae81c2d..ea62a665c980 100644 --- a/core/java/com/android/internal/protolog/common/LogLevel.java +++ b/core/java/com/android/internal/protolog/common/LogLevel.java @@ -16,6 +16,9 @@ package com.android.internal.protolog.common; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; + +@RavenwoodKeepWholeClass public enum LogLevel { DEBUG("d", 1), VERBOSE("v", 2), diff --git a/core/java/com/android/internal/util/ContrastColorUtil.java b/core/java/com/android/internal/util/ContrastColorUtil.java index 0fd139188665..c68f107951ac 100644 --- a/core/java/com/android/internal/util/ContrastColorUtil.java +++ b/core/java/com/android/internal/util/ContrastColorUtil.java @@ -41,8 +41,6 @@ import android.text.style.TextAppearanceSpan; import android.util.Log; import android.util.Pair; -import com.android.internal.annotations.VisibleForTesting; - import java.util.Arrays; import java.util.WeakHashMap; @@ -381,6 +379,13 @@ public class ContrastColorUtil { return calculateLuminance(color) <= 0.17912878474; } + /** Like {@link #isColorDark(int)} but converts to LAB before checking the L component. */ + public static boolean isColorDarkLab(int color) { + final double[] result = ColorUtilsFromCompat.getTempDouble3Array(); + ColorUtilsFromCompat.colorToLAB(color, result); + return result[0] < 50; + } + private int processColor(int color) { return Color.argb(Color.alpha(color), 255 - Color.red(color), diff --git a/core/java/com/android/internal/widget/ActionBarContextView.java b/core/java/com/android/internal/widget/ActionBarContextView.java index d5bb51187ba4..80fc218839d5 100644 --- a/core/java/com/android/internal/widget/ActionBarContextView.java +++ b/core/java/com/android/internal/widget/ActionBarContextView.java @@ -34,7 +34,6 @@ import android.widget.TextView; import com.android.internal.R; import com.android.internal.view.menu.MenuBuilder; -import com.android.window.flags.Flags; /** * @hide @@ -316,14 +315,12 @@ public class ActionBarContextView extends AbsActionBarView { final int contentWidth = MeasureSpec.getSize(widthMeasureSpec); - final int maxHeight = !Flags.actionModeEdgeToEdge() && mContentHeight > 0 - ? mContentHeight : MeasureSpec.getSize(heightMeasureSpec); + int maxHeight = mContentHeight > 0 ? + mContentHeight : MeasureSpec.getSize(heightMeasureSpec); final int verticalPadding = getPaddingTop() + getPaddingBottom(); int availableWidth = contentWidth - getPaddingLeft() - getPaddingRight(); - final int height = Flags.actionModeEdgeToEdge() - ? mContentHeight > 0 ? mContentHeight : maxHeight - : maxHeight - verticalPadding; + final int height = maxHeight - verticalPadding; final int childSpecHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST); if (mClose != null) { @@ -379,8 +376,7 @@ public class ActionBarContextView extends AbsActionBarView { } setMeasuredDimension(contentWidth, measuredHeight); } else { - setMeasuredDimension(contentWidth, Flags.actionModeEdgeToEdge() - ? mContentHeight + verticalPadding : maxHeight); + setMeasuredDimension(contentWidth, maxHeight); } } diff --git a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java index 362b79db4003..ff57fd4fe2ce 100644 --- a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java +++ b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java @@ -294,24 +294,54 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar } } - private boolean setMargin(View view, int left, int top, int right, int bottom) { + private boolean applyInsets(View view, Rect insets, boolean toPadding, + boolean left, boolean top, boolean right, boolean bottom) { + boolean changed; + if (toPadding) { + changed = setMargin(view, EMPTY_RECT, left, top, right, bottom); + changed |= setPadding(view, insets, left, top, right, bottom); + } else { + changed = setPadding(view, EMPTY_RECT, left, top, right, bottom); + changed |= setMargin(view, insets, left, top, right, bottom); + } + return changed; + } + + private boolean setPadding(View view, Rect insets, + boolean left, boolean top, boolean right, boolean bottom) { + if ((left && view.getPaddingLeft() != insets.left) + || (top && view.getPaddingTop() != insets.top) + || (right && view.getPaddingRight() != insets.right) + || (bottom && view.getPaddingBottom() != insets.bottom)) { + view.setPadding( + left ? insets.left : view.getPaddingLeft(), + top ? insets.top : view.getPaddingTop(), + right ? insets.right : view.getPaddingRight(), + bottom ? insets.bottom : view.getPaddingBottom()); + return true; + } + return false; + } + + private boolean setMargin(View view, Rect insets, + boolean left, boolean top, boolean right, boolean bottom) { final LayoutParams lp = (LayoutParams) view.getLayoutParams(); boolean changed = false; - if (lp.leftMargin != left) { + if (left && lp.leftMargin != insets.left) { changed = true; - lp.leftMargin = left; + lp.leftMargin = insets.left; } - if (lp.topMargin != top) { + if (top && lp.topMargin != insets.top) { changed = true; - lp.topMargin = top; + lp.topMargin = insets.top; } - if (lp.rightMargin != right) { + if (right && lp.rightMargin != insets.right) { changed = true; - lp.rightMargin = right; + lp.rightMargin = insets.right; } - if (lp.bottomMargin != bottom) { + if (bottom && lp.bottomMargin != insets.bottom) { changed = true; - lp.bottomMargin = bottom; + lp.bottomMargin = insets.bottom; } return changed; } @@ -337,30 +367,12 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar final Insets sysInsets = insets.getSystemWindowInsets(); mSystemInsets.set(sysInsets.left, sysInsets.top, sysInsets.right, sysInsets.bottom); - boolean changed = false; - if (mActionBarExtendsIntoSystemInsets) { - // Don't extend into navigation bar area so the width can align with status bar - // color view. - final Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars()); - final int paddingLeft = sysInsets.left - navBarInsets.left; - final int paddingRight = sysInsets.right - navBarInsets.right; - mActionBarTop.setPadding(paddingLeft, sysInsets.top, paddingRight, 0); - changed |= setMargin( - mActionBarTop, navBarInsets.left, 0, navBarInsets.right, 0); - if (mActionBarBottom != null) { - mActionBarBottom.setPadding(paddingLeft, 0, paddingRight, sysInsets.bottom); - changed |= setMargin( - mActionBarBottom, navBarInsets.left, 0, navBarInsets.right, 0); - } - } else { - mActionBarTop.setPadding(0, 0, 0, 0); - changed |= setMargin( - mActionBarTop, sysInsets.left, sysInsets.top, sysInsets.right, 0); - if (mActionBarBottom != null) { - mActionBarBottom.setPadding(0, 0, 0, 0); - changed |= setMargin( - mActionBarTop, sysInsets.left, 0, sysInsets.right, sysInsets.bottom); - } + // The top and bottom action bars are always within the content area. + boolean changed = applyInsets(mActionBarTop, mSystemInsets, + mActionBarExtendsIntoSystemInsets, true, true, true, false); + if (mActionBarBottom != null) { + changed |= applyInsets(mActionBarBottom, mSystemInsets, + mActionBarExtendsIntoSystemInsets, true, false, true, true); } // Cannot use the result of computeSystemWindowInsets, because that consumes the @@ -509,12 +521,7 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar ); } } - setMargin( - mContent, - mContentInsets.left, - mContentInsets.top, - mContentInsets.right, - mContentInsets.bottom); + setMargin(mContent, mContentInsets, true, true, true, true); if (!mLastInnerInsets.equals(mInnerInsets)) { // If the inner insets have changed, we need to dispatch this down to diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index d35072fc10c3..6f1d72944a55 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -41,7 +41,6 @@ import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; -import android.content.pm.PackageManager; import android.content.pm.UserInfo; import android.hardware.input.InputManagerGlobal; import android.os.Build; @@ -77,7 +76,6 @@ import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.HashMap; import java.util.List; /** @@ -240,8 +238,6 @@ public class LockPatternUtils { private final SparseLongArray mLockoutDeadlines = new SparseLongArray(); private Boolean mHasSecureLockScreen; - private HashMap<UserHandle, UserManager> mUserManagerCache = new HashMap<>(); - /** * Use {@link TrustManager#isTrustUsuallyManaged(int)}. * @@ -363,22 +359,6 @@ public class LockPatternUtils { return mUserManager; } - private UserManager getUserManager(int userId) { - UserHandle userHandle = UserHandle.of(userId); - if (mUserManagerCache.containsKey(userHandle)) { - return mUserManagerCache.get(userHandle); - } - - try { - Context userContext = mContext.createPackageContextAsUser("system", 0, userHandle); - UserManager userManager = userContext.getSystemService(UserManager.class); - mUserManagerCache.put(userHandle, userManager); - return userManager; - } catch (PackageManager.NameNotFoundException e) { - throw new RuntimeException("Failed to create context for user " + userHandle, e); - } - } - private TrustManager getTrustManager() { TrustManager trust = (TrustManager) mContext.getSystemService(Context.TRUST_SERVICE); if (trust == null) { @@ -966,7 +946,7 @@ public class LockPatternUtils { */ public void setSeparateProfileChallengeEnabled(int userHandle, boolean enabled, LockscreenCredential profilePassword) { - if (!isCredentialSharableWithParent(userHandle)) { + if (!isCredentialShareableWithParent(userHandle)) { return; } try { @@ -985,7 +965,7 @@ public class LockPatternUtils { * credential is not shareable with its parent, or a non-profile user. */ public boolean isSeparateProfileChallengeEnabled(int userHandle) { - return isCredentialSharableWithParent(userHandle) && hasSeparateChallenge(userHandle); + return isCredentialShareableWithParent(userHandle) && hasSeparateChallenge(userHandle); } /** @@ -995,7 +975,7 @@ public class LockPatternUtils { * credential is not shareable with its parent, or a non-profile user. */ public boolean isProfileWithUnifiedChallenge(int userHandle) { - return isCredentialSharableWithParent(userHandle) && !hasSeparateChallenge(userHandle); + return isCredentialShareableWithParent(userHandle) && !hasSeparateChallenge(userHandle); } /** @@ -1020,8 +1000,13 @@ public class LockPatternUtils { return info != null && info.isManagedProfile(); } - private boolean isCredentialSharableWithParent(int userHandle) { - return getUserManager(userHandle).isCredentialSharableWithParent(); + private boolean isCredentialShareableWithParent(int userHandle) { + try { + return getUserManager().getUserProperties(UserHandle.of(userHandle)) + .isCredentialShareableWithParent(); + } catch (IllegalArgumentException e) { + return false; + } } /** diff --git a/core/java/com/android/internal/widget/NotificationProgressBar.java b/core/java/com/android/internal/widget/NotificationProgressBar.java index f6e2a4df8cca..c484a8851dfd 100644 --- a/core/java/com/android/internal/widget/NotificationProgressBar.java +++ b/core/java/com/android/internal/widget/NotificationProgressBar.java @@ -26,6 +26,8 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Rect; +import android.graphics.drawable.Animatable2; +import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.graphics.drawable.LayerDrawable; @@ -66,6 +68,8 @@ public final class NotificationProgressBar extends ProgressBar implements private static final boolean DEBUG = false; private static final float FADED_OPACITY = 0.5f; + private Animatable2.AnimationCallback mIndeterminateAnimationCallback = null; + private NotificationProgressDrawable mNotificationProgressDrawable; private final Rect mProgressDrawableBounds = new Rect(); @@ -150,6 +154,38 @@ public final class NotificationProgressBar extends ProgressBar implements 0); } + @Override + public void setIndeterminateDrawable(Drawable d) { + final Drawable oldDrawable = getIndeterminateDrawable(); + if (oldDrawable != d) { + if (mIndeterminateAnimationCallback != null) { + ((AnimatedVectorDrawable) oldDrawable).unregisterAnimationCallback( + mIndeterminateAnimationCallback); + mIndeterminateAnimationCallback = null; + } + if (d instanceof AnimatedVectorDrawable) { + mIndeterminateAnimationCallback = new Animatable2.AnimationCallback() { + @Override + public void onAnimationEnd(Drawable drawable) { + super.onAnimationEnd(drawable); + + if (shouldLoopIndeterminateAnimation()) { + ((AnimatedVectorDrawable) drawable).start(); + } + } + }; + ((AnimatedVectorDrawable) d).registerAnimationCallback( + mIndeterminateAnimationCallback); + } + } + + super.setIndeterminateDrawable(d); + } + + private boolean shouldLoopIndeterminateAnimation() { + return isIndeterminate() && isAttachedToWindow() && isAggregatedVisible(); + } + /** * Setter for the notification progress model. * diff --git a/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java b/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java index e8aeb8653d06..d8093c4b1f1c 100644 --- a/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java +++ b/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java @@ -233,6 +233,7 @@ public final class CurrentUserServiceSupplier extends BroadcastReceiver implemen private final boolean mMatchSystemAppsOnly; private volatile ServiceChangedListener mListener; + private @Nullable String mUnstableService; private CurrentUserServiceSupplier(Context context, String action, @Nullable String explicitPackage, @Nullable String callerPermission, @@ -330,6 +331,20 @@ public final class CurrentUserServiceSupplier extends BroadcastReceiver implemen } } + // Prefer any service over the unstable service. + if (mUnstableService != null && serviceInfo != null && bestServiceInfo != null) { + if (mUnstableService.equals(serviceInfo.toString())) { + Log.d(TAG, "Not choosing unstable service " + mUnstableService + + " as we already have a service " + bestServiceInfo.toString()); + continue; + } else if (mUnstableService.equals(bestServiceInfo.toString())) { + Log.d(TAG, "Choosing service " + serviceInfo.toString() + + " over the unstable service " + mUnstableService); + bestServiceInfo = serviceInfo; + continue; + } + } + if (sBoundServiceInfoComparator.compare(serviceInfo, bestServiceInfo) > 0) { bestServiceInfo = serviceInfo; } @@ -338,6 +353,17 @@ public final class CurrentUserServiceSupplier extends BroadcastReceiver implemen return bestServiceInfo; } + /** + * Alerts the supplier that the given service is unstable. + * + * The service marked as unstable will be unpreferred over any other services, + * which will last until the next device restart. + */ + @Override + public void alertUnstableService(String unstableService) { + mUnstableService = unstableService; + } + @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); diff --git a/core/java/com/android/server/servicewatcher/ServiceWatcher.java b/core/java/com/android/server/servicewatcher/ServiceWatcher.java index 38872c996596..12dad359dd92 100644 --- a/core/java/com/android/server/servicewatcher/ServiceWatcher.java +++ b/core/java/com/android/server/servicewatcher/ServiceWatcher.java @@ -140,6 +140,11 @@ public interface ServiceWatcher { * null if no service currently meets the criteria. Only invoked while registered. */ @Nullable TBoundServiceInfo getServiceInfo(); + + /** + * Alerts the supplier that the given service is unstable. + */ + void alertUnstableService(String unstableService); } /** @@ -230,6 +235,19 @@ public interface ServiceWatcher { } /** + * Creates a new ServiceWatcher instance. + */ + static <TBoundServiceInfo extends BoundServiceInfo> ServiceWatcher create( + Context context, + String tag, + boolean unstableFallbackEnabled, + ServiceSupplier<TBoundServiceInfo> serviceSupplier, + @Nullable ServiceListener<? super TBoundServiceInfo> serviceListener) { + return create(context, FgThread.getHandler(), tag, unstableFallbackEnabled, + serviceSupplier, serviceListener); + } + + /** * Creates a new ServiceWatcher instance that runs on the given handler. */ static <TBoundServiceInfo extends BoundServiceInfo> ServiceWatcher create( @@ -242,6 +260,20 @@ public interface ServiceWatcher { } /** + * Creates a new ServiceWatcher instance that runs on the given handler. + */ + static <TBoundServiceInfo extends BoundServiceInfo> ServiceWatcher create( + Context context, + Handler handler, + String tag, + boolean unstableFallbackEnabled, + ServiceSupplier<TBoundServiceInfo> serviceSupplier, + @Nullable ServiceListener<? super TBoundServiceInfo> serviceListener) { + return new ServiceWatcherImpl<>(context, handler, tag, unstableFallbackEnabled, + serviceSupplier, serviceListener); + } + + /** * Returns true if there is at least one service that the ServiceWatcher could hypothetically * bind to, as selected by the {@link ServiceSupplier}. */ diff --git a/core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java b/core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java index ccbab9fdba12..30d8710a3f12 100644 --- a/core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java +++ b/core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java @@ -21,11 +21,13 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; +import android.location.flags.Flags; import android.os.DeadObjectException; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; +import android.os.SystemClock; import android.os.UserHandle; import android.util.Log; @@ -52,12 +54,22 @@ class ServiceWatcherImpl<TBoundServiceInfo extends BoundServiceInfo> implements static final boolean D = Log.isLoggable(TAG, Log.DEBUG); static final long RETRY_DELAY_MS = 15 * 1000; + /* Used for the unstable fallback logic, it is the time period in milliseconds where the number + * of disconnections is tracked in order to determine if the service is unstable. */ + private static final long UNSTABLE_TIME_PERIOD_MS = 60 * 1000; + /* Used for the unstable fallback logic, it is the number of disconnections within the time + * period that will mark the service as unstable and allow the fallback to a stable service. */ + private static final int DISCONNECTED_COUNT_BEFORE_MARKED_AS_UNSTABLE = 10; final Context mContext; final Handler mHandler; final String mTag; final ServiceSupplier<TBoundServiceInfo> mServiceSupplier; final @Nullable ServiceListener<? super TBoundServiceInfo> mServiceListener; + private boolean mUnstableFallbackEnabled; + private @Nullable String mDisconnectedService; + private long mDisconnectedStartTime; + private int mDisconnectedCount; private final PackageMonitor mPackageMonitor = new PackageMonitor() { @Override @@ -86,6 +98,20 @@ class ServiceWatcherImpl<TBoundServiceInfo extends BoundServiceInfo> implements mServiceListener = serviceListener; } + ServiceWatcherImpl(Context context, Handler handler, String tag, + boolean unstableFallbackEnabled, + ServiceSupplier<TBoundServiceInfo> serviceSupplier, + ServiceListener<? super TBoundServiceInfo> serviceListener) { + mContext = context; + mHandler = handler; + mTag = tag; + if (Flags.serviceWatcherUnstableFallback()) { + mUnstableFallbackEnabled = unstableFallbackEnabled; + } + mServiceSupplier = serviceSupplier; + mServiceListener = serviceListener; + } + @Override public boolean checkServiceResolves() { return mServiceSupplier.hasMatchingService(); @@ -178,6 +204,7 @@ class ServiceWatcherImpl<TBoundServiceInfo extends BoundServiceInfo> implements // volatile so that isConnected can be called from any thread easily private volatile @Nullable IBinder mBinder; private @Nullable Runnable mRebinder; + private boolean mForcingRebind; MyServiceConnection(@Nullable TBoundServiceInfo boundServiceInfo) { mBoundServiceInfo = boundServiceInfo; @@ -269,6 +296,11 @@ class ServiceWatcherImpl<TBoundServiceInfo extends BoundServiceInfo> implements Log.i(TAG, "[" + mTag + "] connected to " + component.toShortString()); mBinder = binder; + /* Used to keep track of whether we are forcing a rebind, so that we don't force a + * rebind while in the process of already forcing a rebind. This is needed because + * onServiceDisconnected and onBindingDied can happen in quick succession and we only + * want one rebind to happen in this case. */ + mForcingRebind = false; if (mServiceListener != null) { try { @@ -295,6 +327,44 @@ class ServiceWatcherImpl<TBoundServiceInfo extends BoundServiceInfo> implements if (mServiceListener != null) { mServiceListener.onUnbind(); } + + // If unstable fallback is not enabled or no current service is bound, then avoid the + // unstable fallback logic below and return early. + if (!mUnstableFallbackEnabled + || mBoundServiceInfo == null + || mBoundServiceInfo.toString() == null) { + return; + } + + String currentService = mBoundServiceInfo.toString(); + // If the service has already disconnected within the time period, increment the count. + // Otherwise, set the service as disconnected, set the start time, and reset the count. + if (Objects.equals(mDisconnectedService, currentService) + && mDisconnectedStartTime > 0 + && (SystemClock.elapsedRealtime() - mDisconnectedStartTime + <= UNSTABLE_TIME_PERIOD_MS)) { + mDisconnectedCount++; + } else { + mDisconnectedService = currentService; + mDisconnectedStartTime = SystemClock.elapsedRealtime(); + mDisconnectedCount = 1; + } + Log.d(TAG, "[" + mTag + "] Service disconnected : " + currentService + " Count = " + + mDisconnectedCount); + if (mDisconnectedCount >= DISCONNECTED_COUNT_BEFORE_MARKED_AS_UNSTABLE) { + Log.i(TAG, "[" + mTag + "] Service disconnected too many times, set as unstable : " + + mDisconnectedService); + // Alert this service as unstable will last until the next device restart. + mServiceSupplier.alertUnstableService(mDisconnectedService); + mDisconnectedService = null; + mDisconnectedStartTime = 0; + mDisconnectedCount = 0; + // Force rebind to allow the possibility of fallback to a stable service. + if (!mForcingRebind) { + mForcingRebind = true; + onServiceChanged(/*forceRebind=*/ true); + } + } } @Override @@ -305,7 +375,10 @@ class ServiceWatcherImpl<TBoundServiceInfo extends BoundServiceInfo> implements // introduce a small delay to prevent spamming binding over and over, since the likely // cause of a binding dying is some package event that may take time to recover from - mHandler.postDelayed(() -> onServiceChanged(true), 500); + if (!mForcingRebind) { + mForcingRebind = true; + mHandler.postDelayed(() -> onServiceChanged(/*forceRebind=*/ true), 500); + } } @Override diff --git a/core/jni/Android.bp b/core/jni/Android.bp index 7ed73d7668b9..40f6acceecb1 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -488,6 +488,7 @@ cc_library_shared_for_libandroid_runtime { "libbinder", "libbinder_ndk", "libhidlbase", // libhwbinder is in here + "libaconfig_storage_read_api_cc", ], version_script: "platform/linux/libandroid_runtime_export.txt", }, diff --git a/core/jni/android_util_AssetManager.cpp b/core/jni/android_util_AssetManager.cpp index 1394b9f8781a..d3d838a1675b 100644 --- a/core/jni/android_util_AssetManager.cpp +++ b/core/jni/android_util_AssetManager.cpp @@ -67,6 +67,7 @@ static struct typedvalue_offsets_t { jfieldID mResourceId; jfieldID mChangingConfigurations; jfieldID mDensity; + jfieldID mUsesFeatureFlags; } gTypedValueOffsets; // This is also used by asset_manager.cpp. @@ -137,6 +138,8 @@ static jint CopyValue(JNIEnv* env, const AssetManager2::SelectedValue& value, env->SetIntField(out_typed_value, gTypedValueOffsets.mResourceId, value.resid); env->SetIntField(out_typed_value, gTypedValueOffsets.mChangingConfigurations, value.flags); env->SetIntField(out_typed_value, gTypedValueOffsets.mDensity, value.config.density); + env->SetBooleanField(out_typed_value, gTypedValueOffsets.mUsesFeatureFlags, + value.entry_flags & ResTable_entry::FLAG_USES_FEATURE_FLAGS); return static_cast<jint>(ApkAssetsCookieToJavaCookie(value.cookie)); } @@ -1664,6 +1667,7 @@ int register_android_content_AssetManager(JNIEnv* env) { gTypedValueOffsets.mChangingConfigurations = GetFieldIDOrDie(env, typedValue, "changingConfigurations", "I"); gTypedValueOffsets.mDensity = GetFieldIDOrDie(env, typedValue, "density", "I"); + gTypedValueOffsets.mUsesFeatureFlags = GetFieldIDOrDie(env, typedValue, "usesFeatureFlags", "Z"); jclass assetManager = FindClassOrDie(env, "android/content/res/AssetManager"); gAssetManagerOffsets.mObject = GetFieldIDOrDie(env, assetManager, "mObject", "J"); diff --git a/core/jni/fd_utils.cpp b/core/jni/fd_utils.cpp index 5225ce878310..0eb7c4aee287 100644 --- a/core/jni/fd_utils.cpp +++ b/core/jni/fd_utils.cpp @@ -51,8 +51,7 @@ static const char* kPathAllowlist[] = { "/dev/blkio/tasks", "/metadata/aconfig/maps/system.package.map", "/metadata/aconfig/maps/system.flag.map", - "/metadata/aconfig/boot/system.val", - "/metadata/libprocessgroup/memcg_v2_max_activation_depth" // TODO Revert after go/android-memcgv2-exp b/386797433 + "/metadata/aconfig/boot/system.val" }; static const char kFdPath[] = "/proc/self/fd"; diff --git a/core/proto/android/server/windowmanagerservice.proto b/core/proto/android/server/windowmanagerservice.proto index 5820c8e947c2..3ebb48041ecd 100644 --- a/core/proto/android/server/windowmanagerservice.proto +++ b/core/proto/android/server/windowmanagerservice.proto @@ -471,6 +471,8 @@ message WindowStateProto { repeated .android.view.InsetsSourceProto mergedLocalInsetsSources = 47; optional int32 requested_visible_types = 48; optional .android.graphics.RectProto dim_bounds = 49; + optional int32 prepare_sync_seq_id = 50; + optional int32 sync_seq_id = 51; } message IdentifierProto { diff --git a/core/res/Android.bp b/core/res/Android.bp index 1199d77d04c6..29da0d6f67ae 100644 --- a/core/res/Android.bp +++ b/core/res/Android.bp @@ -181,6 +181,7 @@ android_app { "ranging_aconfig_flags", "aconfig_settingslib_flags", "telephony_flags", + "update_engine_aconfig_declarations", ], } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 9e0200481421..636968dd1152 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -4554,12 +4554,12 @@ android:protectionLevel="signature|privileged" /> <!-- Allows application to request to stream content from an Android host to a nearby device - ({@link android.companion.AssociationRequest#DEVICE_PROFILE_SENSOR_DEVICE_STREAMING}) + ({@link android.companion.AssociationRequest#DEVICE_PROFILE_VIRTUAL_DEVICE}) by {@link android.companion.CompanionDeviceManager}. <p>Not for use by third-party applications. @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ENABLE_LIMITED_VDM_ROLE) --> - <permission android:name="android.permission.REQUEST_COMPANION_PROFILE_SENSOR_DEVICE_STREAMING" + <permission android:name="android.permission.REQUEST_COMPANION_PROFILE_VIRTUAL_DEVICE" android:protectionLevel="signature|privileged" android:featureFlag="android.companion.virtualdevice.flags.enable_limited_vdm_role" /> @@ -5180,6 +5180,13 @@ <permission android:name="android.permission.READ_LOGS" android:protectionLevel="signature|privileged|development" /> + <!-- Allows an application to read the update_engine logs + <p>Not for use by third-party applications. + @FlaggedApi("com.android.update_engine.minor_changes_2025q4") --> + <permission android:name="android.permission.READ_UPDATE_ENGINE_LOGS" + android:protectionLevel="signature|privileged|development" + android:featureFlag="com.android.update_engine.minor_changes_2025q4" /> + <!-- Configure an application for debugging. <p>Not for use by third-party applications. --> <permission android:name="android.permission.SET_DEBUG_APP" @@ -9287,11 +9294,15 @@ <receiver android:name="com.android.server.updates.CertPinInstallReceiver" android:exported="true" + android:systemUserOnly="true" android:permission="android.permission.UPDATE_CONFIG"> <intent-filter> <action android:name="android.intent.action.UPDATE_PINS" /> <data android:scheme="content" android:host="*" android:mimeType="*/*" /> </intent-filter> + <intent-filter> + <action android:name="android.intent.action.BOOT_COMPLETED" /> + </intent-filter> </receiver> <receiver android:name="com.android.server.updates.IntentFirewallInstallReceiver" diff --git a/core/res/res/drawable/ic_standby.xml b/core/res/res/drawable/ic_standby.xml new file mode 100644 index 000000000000..6736f14f1377 --- /dev/null +++ b/core/res/res/drawable/ic_standby.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="M480,600Q530,600 565,565Q600,530 600,480Q600,430 565,395Q530,360 480,360Q430,360 395,395Q360,430 360,480Q360,530 395,565Q430,600 480,600ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/> +</vector> diff --git a/core/res/res/drawable/notification_progress_indeterminate_horizontal_material.xml b/core/res/res/drawable/notification_progress_indeterminate_horizontal_material.xml new file mode 100644 index 000000000000..9b0405ad264f --- /dev/null +++ b/core/res/res/drawable/notification_progress_indeterminate_horizontal_material.xml @@ -0,0 +1,167 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt" + android:drawable="@drawable/vector_notification_progress_indeterminate_horizontal"> + <target android:name="track_behind"> + <aapt:attr name="android:animation"> + <set android:ordering="sequentially"> + <objectAnimator + android:duration="800" + android:propertyName="trimPathEnd" + android:startOffset="400" + android:valueFrom="0" + android:valueTo="0.51" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="200" + android:propertyName="trimPathEnd" + android:valueFrom="0.51" + android:valueTo="0.98" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0.2,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="100" + android:propertyName="trimPathEnd" + android:valueFrom="0.98" + android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.5 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="progress"> + <aapt:attr name="android:animation"> + <set android:ordering="sequentially"> + <objectAnimator + android:duration="200" + android:propertyName="trimPathStart" + android:startOffset="200" + android:valueFrom="0" + android:valueTo="0.02" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.963,0.8 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="800" + android:propertyName="trimPathStart" + android:valueFrom="0.02" + android:valueTo="0.53" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="200" + android:propertyName="trimPathStart" + android:valueFrom="0.53" + android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0.2,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="progress"> + <aapt:attr name="android:animation"> + <set android:ordering="sequentially"> + <objectAnimator + android:duration="200" + android:propertyName="trimPathEnd" + android:startOffset="200" + android:valueFrom="0" + android:valueTo="0.024" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.963,0.834 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="800" + android:propertyName="trimPathEnd" + android:valueFrom="0.024" + android:valueTo="0.98" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="30" + android:propertyName="trimPathEnd" + android:valueFrom="0.98" + android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="track_ahead"> + <aapt:attr name="android:animation"> + <set android:ordering="sequentially"> + <objectAnimator + android:duration="200" + android:propertyName="trimPathStart" + android:valueFrom="0" + android:valueTo="0.02" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="200" + android:propertyName="trimPathStart" + android:valueFrom="0.02" + android:valueTo="0.044000000000000004" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.963,0.834 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="800" + android:propertyName="trimPathStart" + android:valueFrom="0.044000000000000004" + android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> +</animated-vector>
\ No newline at end of file diff --git a/core/res/res/drawable/vector_notification_progress_indeterminate_horizontal.xml b/core/res/res/drawable/vector_notification_progress_indeterminate_horizontal.xml new file mode 100644 index 000000000000..fe81b79e481c --- /dev/null +++ b/core/res/res/drawable/vector_notification_progress_indeterminate_horizontal.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="300dp" + android:height="20dp" + android:viewportHeight="16" + android:viewportWidth="300"> + <group + android:name="root" + android:translateX="150" + android:translateY="8"> + <path + android:name="track_ahead" + android:pathData="M -147,0.0 L 147,0.0" + android:strokeAlpha="0.5" + android:strokeColor="?attr/colorControlActivated" + android:strokeLineCap="round" + android:strokeLineJoin="round" + android:strokeWidth="2" + android:trimPathEnd="1" + android:trimPathOffset="0" + android:trimPathStart="0" /> + <path + android:name="progress" + android:pathData="M -147,0.0 L 147,0.0" + android:strokeColor="?attr/colorControlActivated" + android:strokeLineCap="round" + android:strokeLineJoin="round" + android:strokeWidth="6" + android:trimPathEnd="0" + android:trimPathOffset="0" + android:trimPathStart="0" /> + <path + android:name="track_behind" + android:pathData="M -147,0.0 L 147,0.0" + android:strokeAlpha="0.5" + android:strokeColor="?attr/colorControlActivated" + android:strokeLineCap="round" + android:strokeLineJoin="round" + android:strokeWidth="2" + android:trimPathEnd="0" + android:trimPathOffset="0" + android:trimPathStart="0" /> + </group> +</vector>
\ No newline at end of file diff --git a/core/res/res/values-ar/strings.xml b/core/res/res/values-ar/strings.xml index 7daa4b49786d..03a31ddb2129 100644 --- a/core/res/res/values-ar/strings.xml +++ b/core/res/res/values-ar/strings.xml @@ -1808,9 +1808,9 @@ <string name="reduce_bright_colors_feature_name" msgid="3222994553174604132">"زيادة تعتيم الشاشة"</string> <string name="hearing_aids_feature_name" msgid="1125892105105852542">"سماعات الأذن الطبية"</string> <string name="autoclick_feature_name" msgid="8149248738736949630">"النقر التلقائي"</string> - <string name="hearing_device_status_disconnected" msgid="497547752953543832">"غير متّصل"</string> - <string name="hearing_device_status_connected" msgid="2149385149669918764">"متّصل"</string> - <string name="hearing_device_status_active" msgid="4770378695482566032">"متّصل حاليًا"</string> + <string name="hearing_device_status_disconnected" msgid="497547752953543832">"غير متّصلة"</string> + <string name="hearing_device_status_connected" msgid="2149385149669918764">"متّصلة"</string> + <string name="hearing_device_status_active" msgid="4770378695482566032">"متّصلة حاليًا"</string> <string name="hearing_device_status_loading" msgid="5717083847663109747">"غير متاح بعد"</string> <string name="accessibility_shortcut_enabling_service" msgid="5473495203759847687">"تم الضغط مع الاستمرار على مفتاحَي التحكّم في مستوى الصوت. تم تفعيل <xliff:g id="SERVICE_NAME">%1$s</xliff:g>."</string> <string name="accessibility_shortcut_disabling_service" msgid="8675244165062700619">"تم الضغط مع الاستمرار على مفتاحَي التحكّم في مستوى الصوت. تم إيقاف <xliff:g id="SERVICE_NAME">%1$s</xliff:g>."</string> diff --git a/core/res/res/values-el/strings.xml b/core/res/res/values-el/strings.xml index 5d83f7890d6b..981c56d639ae 100644 --- a/core/res/res/values-el/strings.xml +++ b/core/res/res/values-el/strings.xml @@ -1451,12 +1451,12 @@ <string name="usb_power_notification_message" msgid="7284765627437897702">"Φόρτιση συνδεδεμένης συσκευής. Πατήστε για περισσότερες επιλογές."</string> <string name="usb_unsupported_audio_accessory_title" msgid="2335775548086533065">"Εντοπίστηκε αναλογικό αξεσουάρ ήχου"</string> <string name="usb_unsupported_audio_accessory_message" msgid="1300168007129796621">"Η συνδεδεμένη συσκευή δεν είναι συμβατή με αυτό το τηλέφωνο. Πατήστε για να μάθετε περισσότερα."</string> - <string name="adb_active_notification_title" msgid="408390247354560331">"Eνεργός εντοπ. σφαλματών USB"</string> - <string name="adb_active_notification_message" msgid="5617264033476778211">"Πατήστε για απενεργ. εντοπ. σφαλμ. USB"</string> - <string name="adb_active_notification_message" product="tv" msgid="6624498401272780855">"Επιλογή για απενεργοποίηση του εντοπισμού σφαλμάτων USB."</string> - <string name="adbwifi_active_notification_title" msgid="6147343659168302473">"Συνδέθηκε ο ασύρματος εντοπισμός σφαλμάτων"</string> - <string name="adbwifi_active_notification_message" msgid="930987922852867972">"Πατήστε, για να απενεργοποιήσετε τον ασύρματο εντοπισμό σφαλμάτων"</string> - <string name="adbwifi_active_notification_message" product="tv" msgid="8633421848366915478">"Επιλέξτε, για να απενεργοποιήσετε τον ασύρματο εντοπισμό σφαλμάτων."</string> + <string name="adb_active_notification_title" msgid="408390247354560331">"Eνεργή αποσφαλμάτωση USB"</string> + <string name="adb_active_notification_message" msgid="5617264033476778211">"Πατήστε για απενεργ. αποσφαλμάτ. USB"</string> + <string name="adb_active_notification_message" product="tv" msgid="6624498401272780855">"Επιλογή για απενεργοποίηση του αποσφαλμάτωσης USB."</string> + <string name="adbwifi_active_notification_title" msgid="6147343659168302473">"Συνδέθηκε η ασύρματη αποσφαλμάτωση"</string> + <string name="adbwifi_active_notification_message" msgid="930987922852867972">"Πατήστε, για να απενεργοποιήσετε την ασύρματη αποσφαλμάτωση"</string> + <string name="adbwifi_active_notification_message" product="tv" msgid="8633421848366915478">"Επιλέξτε, για να απενεργοποιήσετε την ασύρματη αποσφαλμάτωση."</string> <string name="test_harness_mode_notification_title" msgid="2282785860014142511">"Η λειτουργία περιβάλλοντος δοκιμών ενεργοποιήθηκε"</string> <string name="test_harness_mode_notification_message" msgid="3039123743127958420">"Εκτελέστε επαναφορά εργοστασιακών ρυθμίσεων για να απενεργοποιήσετε τη λειτουργία περιβάλλοντος δοκιμών."</string> <string name="wrong_hsum_configuration_notification_title" msgid="7212758829332714385">"Λάθος διαμόρφωση κατασκευής HSUM"</string> @@ -2127,7 +2127,7 @@ <string name="app_category_productivity" msgid="1844422703029557883">"Παραγωγικότητα"</string> <string name="app_category_accessibility" msgid="6643521607848547683">"Προσβασιμότητα"</string> <string name="device_storage_monitor_notification_channel" msgid="5164244565844470758">"Αποθηκευτικός χώρος συσκευής"</string> - <string name="adb_debugging_notification_channel_tv" msgid="4764046459631031496">"Εντοπισμός σφαλμάτων USB"</string> + <string name="adb_debugging_notification_channel_tv" msgid="4764046459631031496">"Αποσφαλμάτωση USB"</string> <string name="time_picker_hour_label" msgid="4208590187662336864">"ώρα"</string> <string name="time_picker_minute_label" msgid="8307452311269824553">"λεπτό"</string> <string name="time_picker_header_text" msgid="9073802285051516688">"Ορισμός ώρας"</string> diff --git a/core/res/res/values-eu/strings.xml b/core/res/res/values-eu/strings.xml index 7a49905cb250..bbf4c996cc4f 100644 --- a/core/res/res/values-eu/strings.xml +++ b/core/res/res/values-eu/strings.xml @@ -2273,18 +2273,12 @@ <string name="accessibility_autoclick_scroll" msgid="3499385943728726933">"Egin gora eta behera"</string> <string name="accessibility_autoclick_pause" msgid="3272200156172573568">"Pausatu"</string> <string name="accessibility_autoclick_position" msgid="2933660969907663545">"Ezarri posizioan"</string> - <!-- no translation found for accessibility_autoclick_scroll_up (2044948780797117443) --> - <skip /> - <!-- no translation found for accessibility_autoclick_scroll_down (3733401063292018116) --> - <skip /> - <!-- no translation found for accessibility_autoclick_scroll_left (8564421367992824198) --> - <skip /> - <!-- no translation found for accessibility_autoclick_scroll_right (8932417330753984265) --> - <skip /> - <!-- no translation found for accessibility_autoclick_scroll_exit (3788610039146769696) --> - <skip /> - <!-- no translation found for accessibility_autoclick_scroll_panel_title (7120598166296447036) --> - <skip /> + <string name="accessibility_autoclick_scroll_up" msgid="2044948780797117443">"Egin gora"</string> + <string name="accessibility_autoclick_scroll_down" msgid="3733401063292018116">"Egin behera"</string> + <string name="accessibility_autoclick_scroll_left" msgid="8564421367992824198">"Egin ezkerrera"</string> + <string name="accessibility_autoclick_scroll_right" msgid="8932417330753984265">"Egin eskuinera"</string> + <string name="accessibility_autoclick_scroll_exit" msgid="3788610039146769696">"Irten gora/behera egiteko modutik"</string> + <string name="accessibility_autoclick_scroll_panel_title" msgid="7120598166296447036">"Gora/Behera egiteko panela"</string> <string name="as_app_forced_to_restricted_bucket" msgid="8233871289353898964">"Murriztuen edukiontzian ezarri da <xliff:g id="PACKAGE_NAME">%1$s</xliff:g>"</string> <string name="conversation_single_line_name_display" msgid="8958948312915255999">"<xliff:g id="SENDER_NAME">%1$s</xliff:g>:"</string> <string name="conversation_single_line_image_placeholder" msgid="6983271082911936900">"erabiltzaileak irudi bat bidali du"</string> diff --git a/core/res/res/values-fi/strings.xml b/core/res/res/values-fi/strings.xml index dce063d0b033..0083fd9a709b 100644 --- a/core/res/res/values-fi/strings.xml +++ b/core/res/res/values-fi/strings.xml @@ -1214,8 +1214,8 @@ <string name="Noon" msgid="6902418443846838189">"Keskipäivä"</string> <string name="midnight" msgid="3646671134282785114">"keskiyö"</string> <string name="Midnight" msgid="8176019203622191377">"Keskiyö"</string> - <string name="elapsed_time_short_format_mm_ss" msgid="8689459651807876423">"<xliff:g id="MINUTES">%1$02d</xliff:g>:<xliff:g id="SECONDS">%2$02d</xliff:g>"</string> - <string name="elapsed_time_short_format_h_mm_ss" msgid="2302144714803345056">"<xliff:g id="HOURS">%1$d</xliff:g>:<xliff:g id="MINUTES">%2$02d</xliff:g>:<xliff:g id="SECONDS">%3$02d</xliff:g>"</string> + <string name="elapsed_time_short_format_mm_ss" msgid="8689459651807876423">"<xliff:g id="MINUTES">%1$02d</xliff:g>.<xliff:g id="SECONDS">%2$02d</xliff:g>"</string> + <string name="elapsed_time_short_format_h_mm_ss" msgid="2302144714803345056">"<xliff:g id="HOURS">%1$d</xliff:g>.<xliff:g id="MINUTES">%2$02d</xliff:g>.<xliff:g id="SECONDS">%3$02d</xliff:g>"</string> <string name="selectAll" msgid="1532369154488982046">"Valitse kaikki"</string> <string name="cut" msgid="2561199725874745819">"Leikkaa"</string> <string name="copy" msgid="5472512047143665218">"Kopioi"</string> diff --git a/core/res/res/values-gu/strings.xml b/core/res/res/values-gu/strings.xml index 6b1e2840522a..1d2f975da5e6 100644 --- a/core/res/res/values-gu/strings.xml +++ b/core/res/res/values-gu/strings.xml @@ -2486,8 +2486,7 @@ <string name="profile_label_work_3" msgid="4834572253956798917">"ઑફિસ 3"</string> <string name="profile_label_test" msgid="9168641926186071947">"પરીક્ષણ કરો"</string> <string name="profile_label_communal" msgid="8743921499944800427">"કૉમ્યુનલ"</string> - <!-- no translation found for profile_label_supervising (5649312778545745371) --> - <skip /> + <string name="profile_label_supervising" msgid="5649312778545745371">"નિરીક્ષણ કરનાર"</string> <string name="accessibility_label_managed_profile" msgid="3366526886209832641">"ઑફિસની પ્રોફાઇલ"</string> <string name="accessibility_label_private_profile" msgid="1436459319135548969">"ખાનગી સ્પેસ"</string> <string name="accessibility_label_clone_profile" msgid="7579118375042398784">"ક્લોન"</string> diff --git a/core/res/res/values-ne/strings.xml b/core/res/res/values-ne/strings.xml index 5e32b967ec71..b3ce1c8eb9b6 100644 --- a/core/res/res/values-ne/strings.xml +++ b/core/res/res/values-ne/strings.xml @@ -2486,8 +2486,7 @@ <string name="profile_label_work_3" msgid="4834572253956798917">"कार्य प्रोफाइल ३"</string> <string name="profile_label_test" msgid="9168641926186071947">"परीक्षण"</string> <string name="profile_label_communal" msgid="8743921499944800427">"सामुदायिक"</string> - <!-- no translation found for profile_label_supervising (5649312778545745371) --> - <skip /> + <string name="profile_label_supervising" msgid="5649312778545745371">"सुपरिवेक्षण गरिँदै छ"</string> <string name="accessibility_label_managed_profile" msgid="3366526886209832641">"कार्य प्रोफाइल"</string> <string name="accessibility_label_private_profile" msgid="1436459319135548969">"निजी स्पेस"</string> <string name="accessibility_label_clone_profile" msgid="7579118375042398784">"क्लोन"</string> diff --git a/core/res/res/values-sq/strings.xml b/core/res/res/values-sq/strings.xml index a15cf4ea3b5c..5c9becec9550 100644 --- a/core/res/res/values-sq/strings.xml +++ b/core/res/res/values-sq/strings.xml @@ -2273,18 +2273,12 @@ <string name="accessibility_autoclick_scroll" msgid="3499385943728726933">"Lëviz"</string> <string name="accessibility_autoclick_pause" msgid="3272200156172573568">"Vendos në pauzë"</string> <string name="accessibility_autoclick_position" msgid="2933660969907663545">"Pozicioni"</string> - <!-- no translation found for accessibility_autoclick_scroll_up (2044948780797117443) --> - <skip /> - <!-- no translation found for accessibility_autoclick_scroll_down (3733401063292018116) --> - <skip /> - <!-- no translation found for accessibility_autoclick_scroll_left (8564421367992824198) --> - <skip /> - <!-- no translation found for accessibility_autoclick_scroll_right (8932417330753984265) --> - <skip /> - <!-- no translation found for accessibility_autoclick_scroll_exit (3788610039146769696) --> - <skip /> - <!-- no translation found for accessibility_autoclick_scroll_panel_title (7120598166296447036) --> - <skip /> + <string name="accessibility_autoclick_scroll_up" msgid="2044948780797117443">"Lëviz lart"</string> + <string name="accessibility_autoclick_scroll_down" msgid="3733401063292018116">"Lëviz poshtë"</string> + <string name="accessibility_autoclick_scroll_left" msgid="8564421367992824198">"Lëviz majtas"</string> + <string name="accessibility_autoclick_scroll_right" msgid="8932417330753984265">"Lëviz djathtas"</string> + <string name="accessibility_autoclick_scroll_exit" msgid="3788610039146769696">"Dil nga modaliteti i lëvizjes"</string> + <string name="accessibility_autoclick_scroll_panel_title" msgid="7120598166296447036">"Paneli i lëvizjes"</string> <string name="as_app_forced_to_restricted_bucket" msgid="8233871289353898964">"<xliff:g id="PACKAGE_NAME">%1$s</xliff:g> është vendosur në grupin E KUFIZUAR"</string> <string name="conversation_single_line_name_display" msgid="8958948312915255999">"<xliff:g id="SENDER_NAME">%1$s</xliff:g>:"</string> <string name="conversation_single_line_image_placeholder" msgid="6983271082911936900">"dërgoi një imazh"</string> diff --git a/core/res/res/values-watch/config.xml b/core/res/res/values-watch/config.xml index 57a09ea34ba1..42210c40e7ba 100644 --- a/core/res/res/values-watch/config.xml +++ b/core/res/res/values-watch/config.xml @@ -31,7 +31,7 @@ <dimen name="config_viewMinFlingVelocity">500dp</dimen> <!-- Maximum velocity to initiate a fling, as measured in dips per second. --> - <dimen name="config_viewMaxFlingVelocity">8000dp</dimen> + <dimen name="config_viewMaxFlingVelocity">2750dp</dimen> <!-- Minimum velocity (absolute value) to initiate a fling from a rotary encoder device, as measured in dips per second. Setting this to -1dp disables rotary encoder fling. --> @@ -39,7 +39,7 @@ <!-- Maximum velocity (absolute value) to initiate a fling from a rotary encoder device, as measured in dips per second. Setting this to -1dp disables rotary encoder fling. --> - <dimen name="config_viewMaxRotaryEncoderFlingVelocity">8000dp</dimen> + <dimen name="config_viewMaxRotaryEncoderFlingVelocity">2750dp</dimen> <!-- Whether the View-based scroll haptic feedback implementation is enabled for {@link InputDevice#SOURCE_ROTARY_ENCODER}s. --> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 7bb799a24ef1..785b8c78863d 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2029,6 +2029,9 @@ <!-- Class name of WallpaperManagerService. --> <string name="config_wallpaperManagerServiceName" translatable="false">com.android.server.wallpaper.WallpaperManagerService</string> + <!-- True if live wallpapers can be supported in the deskop experience --> + <bool name="config_isLiveWallpaperSupportedInDesktopExperience">false</bool> + <!-- Specifies priority of automatic time sources. Suggestions from higher entries in the list take precedence over lower ones. See com.android.server.timedetector.TimeDetectorStrategy for available sources. --> @@ -2125,14 +2128,27 @@ <!-- Package name providing fused location support. Used only when config_enableFusedLocationOverlay is false. --> <string name="config_fusedLocationProviderPackageName" translatable="false">com.android.location.fused</string> + <!-- If true, will fallback to use a different app if the chosen overlay app is determined to + be unstable. Used only when config_enableFusedLocationOverlay is true. --> + <bool name="config_fusedLocationOverlayUnstableFallback" translatable="false">false</bool> <!-- If true will use the GNSS hardware implementation to service the GPS_PROVIDER. If false will allow the GPS_PROVIDER to be replaced by an app at run-time (restricted to the package specified by config_gnssLocationProviderPackageName). --> <bool name="config_useGnssHardwareProvider" translatable="false">true</bool> + <!-- Whether to enable gnss location provider overlay which allows gnss location provider to + be replaced by an app at run-time. When disabled, only the + config_gnssLocationProviderPackageName package will be searched for gnss location + provider, otherwise any system package is eligible. Anyone who wants to disable the overlay + mechanism can set it to false. Used only when config_useGnssHardwareProvider is false --> + <bool name="config_enableGnssLocationOverlay" translatable="false">true</bool> <!-- Package name providing GNSS location support. Used only when config_useGnssHardwareProvider is false. --> <string name="config_gnssLocationProviderPackageName" translatable="false">@null</string> + <!-- If true, will fallback to use a different app if the chosen overlay app is determined to + be unstable. Used only when config_useGnssHardwareProvider is false and + config_enableGnssLocationOverlay is true. --> + <bool name="config_gnssLocationOverlayUnstableFallback" translatable="false">false</bool> <!-- Default value for the ADAS GNSS Location Enabled setting if this setting has never been set before. --> @@ -3823,6 +3839,7 @@ "lockdown" = Lock down device until the user authenticates "logout" = Logout the current user "system_update" = Launch System Update screen + "standby" = Bring the device to standby --> <string-array translatable="false" name="config_globalActionsList"> <item>emergency</item> @@ -4074,6 +4091,11 @@ <item>-44</item> </integer-array> + <!-- Provides default value for operator name in status bar option in setting: + 0 - Hide operator name in status bar + 1 - Show operator name in status bar (default) --> + <integer name="config_showOperatorNameDefault">1</integer> + <!-- Enabled built-in zen mode condition providers --> <string-array translatable="false" name="config_system_condition_providers"> <item>countdown</item> @@ -7449,4 +7471,8 @@ <!-- By default ActivityOptions#makeScaleUpAnimation is only used between activities. This config enables OEMs to support its usage across tasks.--> <bool name="config_enableCrossTaskScaleUpAnimation">false</bool> + + <!-- Biometrics fingerprint frr notification target activity component name, it shall be customized by OEMs --> + <string translatable="false" name="config_fingerprintFrrTargetComponent"></string> + </resources> diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml index 1d40110dc7ca..fc46418478c8 100644 --- a/core/res/res/values/config_telephony.xml +++ b/core/res/res/values/config_telephony.xml @@ -318,6 +318,11 @@ <bool name="config_send_satellite_datagram_to_modem_in_demo_mode">false</bool> <java-symbol type="bool" name="config_send_satellite_datagram_to_modem_in_demo_mode" /> + <!-- Whether the device supports disabling satellite while satellite enabling is in progress. + --> + <bool name="config_support_disable_satellite_while_enable_in_progress">true</bool> + <java-symbol type="bool" name="config_support_disable_satellite_while_enable_in_progress" /> + <!-- List of country codes where oem-enabled satellite services are either allowed or disallowed by the device. Each country code is a lowercase 2 character ISO-3166-1 alpha-2. --> diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index ac1e841d3143..ed524054a5d4 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -128,7 +128,7 @@ <staging-public-group type="id" first-id="0x01b20000"> <!-- @FlaggedApi(android.appwidget.flags.Flags.FLAG_ENGAGEMENT_METRICS) --> - <public name="remoteViewsMetricsId"/> + <public name="removed_remoteViewsMetricsId"/> <!-- @FlaggedApi("android.view.accessibility.a11y_selection_api") --> <public name="accessibilityActionSetExtendedSelection"/> </staging-public-group> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index d94d659446ac..43ba327f4069 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -739,6 +739,9 @@ <!-- label for screenshot item in power menu [CHAR LIMIT=24]--> <string name="global_action_screenshot">Screenshot</string> + <!-- label for standby item in power menu [CHAR LIMIT=24]--> + <string name="global_action_standby">Standby</string> + <!-- Take bug report menu title [CHAR LIMIT=30] --> <string name="bugreport_title">Bug report</string> <!-- Message in bugreport dialog describing what it does [CHAR LIMIT=NONE] --> @@ -6864,4 +6867,10 @@ ul.</string> <string name="usb_apm_usb_plugged_in_when_locked_notification_text">USB device is plugged in when Android is locked. To use device, please unlock Android first and then reinsert USB device to use it.</string> <string name="usb_apm_usb_suspicious_activity_notification_title">Suspicious USB activity</string> <string name="usb_apm_usb_suspicious_activity_notification_text">USB data signal has been disabled.</string> + + <!-- Fingerprint failed rate too high notification title --> + <string name="fingerprint_frr_notification_title">Having trouble with Fingerprint Unlock?</string> + <!-- Fingerprint failed rate too high notification msg --> + <string name="fingerprint_frr_notification_msg">Tap to review tips to improve your unlocking experience</string> + </resources> diff --git a/core/res/res/values/styles_material.xml b/core/res/res/values/styles_material.xml index 8f13ee1ccb49..b6142592296c 100644 --- a/core/res/res/values/styles_material.xml +++ b/core/res/res/values/styles_material.xml @@ -508,6 +508,7 @@ please see styles_device_defaults.xml. <item name="segPointGap">@dimen/notification_progress_segPoint_gap</item> <item name="progressDrawable">@drawable/notification_progress</item> <item name="trackerHeight">@dimen/notification_progress_tracker_height</item> + <item name="indeterminateDrawable">@drawable/notification_progress_indeterminate_horizontal_material</item> </style> <style name="Widget.Material.Notification.Text" parent="Widget.Material.Light.TextView"> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 2dc5687a1253..246035ae8119 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1958,6 +1958,7 @@ <java-symbol type="string" name="global_action_voice_assist" /> <java-symbol type="string" name="global_action_assist" /> <java-symbol type="string" name="global_action_screenshot" /> + <java-symbol type="string" name="global_action_standby" /> <java-symbol type="string" name="invalidPuk" /> <java-symbol type="string" name="lockscreen_carrier_default" /> <java-symbol type="style" name="Animation.LockScreen" /> @@ -2050,6 +2051,9 @@ <java-symbol type="bool" name="config_enableActivityRecognitionHardwareOverlay" /> <java-symbol type="bool" name="config_defaultAdasGnssLocationEnabled" /> <java-symbol type="bool" name="config_enableFusedLocationOverlay" /> + <java-symbol type="bool" name="config_enableGnssLocationOverlay" /> + <java-symbol type="bool" name="config_fusedLocationOverlayUnstableFallback" /> + <java-symbol type="bool" name="config_gnssLocationOverlayUnstableFallback" /> <java-symbol type="bool" name="config_useGnssHardwareProvider" /> <java-symbol type="bool" name="config_enableGeocoderOverlay" /> <java-symbol type="bool" name="config_enableGeofenceOverlay" /> @@ -2278,6 +2282,7 @@ <java-symbol type="string" name="heavy_weight_notification_detail" /> <java-symbol type="string" name="image_wallpaper_component" /> <java-symbol type="string" name="fallback_wallpaper_component" /> + <java-symbol type="bool" name="config_isLiveWallpaperSupportedInDesktopExperience" /> <java-symbol type="string" name="input_method_binding_label" /> <java-symbol type="string" name="input_method_ime_switch_long_click_action_desc" /> <java-symbol type="string" name="launch_warning_original" /> @@ -2714,6 +2719,9 @@ <java-symbol type="string" name="muted_by" /> <java-symbol type="string" name="zen_mode_alarm" /> + <!-- For default state of operator name in status bar --> + <java-symbol type="integer" name="config_showOperatorNameDefault"/> + <java-symbol type="string" name="select_day" /> <java-symbol type="string" name="select_year" /> @@ -3718,6 +3726,7 @@ <java-symbol type="drawable" name="ic_screenshot" /> <java-symbol type="drawable" name="ic_faster_emergency" /> <java-symbol type="drawable" name="ic_media_seamless" /> + <java-symbol type="drawable" name="ic_standby" /> <java-symbol type="drawable" name="emergency_icon" /> <java-symbol type="array" name="config_convert_to_emergency_number_map" /> @@ -5221,6 +5230,7 @@ <java-symbol type="id" name="remote_views_next_child" /> <java-symbol type="id" name="remote_views_stable_id" /> <java-symbol type="id" name="remote_views_override_id" /> + <java-symbol type="id" name="remoteViewsMetricsId" /> <!-- View and control prompt --> <java-symbol type="drawable" name="ic_accessibility_24dp" /> @@ -5985,4 +5995,10 @@ <!-- Default height of desktop view header for freeform tasks on launch. --> <java-symbol type="dimen" name="desktop_view_default_header_height" /> + + <!-- Enable OEMs to support different frr notification target component activity --> + <java-symbol type="string" name="config_fingerprintFrrTargetComponent" /> + <java-symbol type="string" name="fingerprint_frr_notification_title" /> + <java-symbol type="string" name="fingerprint_frr_notification_msg" /> + </resources> diff --git a/core/tests/coretests/res/xml/flags.xml b/core/tests/coretests/res/xml/flags.xml new file mode 100644 index 000000000000..e580ea5dea00 --- /dev/null +++ b/core/tests/coretests/res/xml/flags.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<first xmlns:android="http://schemas.android.com/apk/res/android"> + <second android:featureFlag="android.content.res.always_false"/> +</first>
\ No newline at end of file diff --git a/core/tests/coretests/src/android/app/NotificationManagerTest.java b/core/tests/coretests/src/android/app/NotificationManagerTest.java index 250b9ce8d89d..001eb620dd0f 100644 --- a/core/tests/coretests/src/android/app/NotificationManagerTest.java +++ b/core/tests/coretests/src/android/app/NotificationManagerTest.java @@ -442,6 +442,44 @@ public class NotificationManagerTest { @Test @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannel_localModificationDoesNotChangeCache() throws Exception { + NotificationManager.invalidateNotificationChannelCache(); + NotificationChannel original = new NotificationChannel("id", "name", + NotificationManager.IMPORTANCE_DEFAULT); + NotificationChannel originalConv = new NotificationChannel("", "name_conversation", + NotificationManager.IMPORTANCE_DEFAULT); + originalConv.setConversationId("id", "id_conversation"); + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), + anyInt())).thenReturn(new ParceledListSlice<>( + List.of(original.copy(), originalConv.copy()))); + + // modify the output channel, but only locally + NotificationChannel out = mNotificationManager.getNotificationChannel("id"); + out.setName("modified"); + + // This should not change the result of getNotificationChannel + assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(original); + assertThat(mNotificationManager.getNotificationChannel("id")).isNotEqualTo(out); + + // and also check the conversation channel + NotificationChannel outConv = mNotificationManager.getNotificationChannel("id", + "id_conversation"); + outConv.setName("conversation_modified"); + assertThat(mNotificationManager.getNotificationChannel("id", "id_conversation")).isEqualTo( + originalConv); + assertThat( + mNotificationManager.getNotificationChannel("id", "id_conversation")).isNotEqualTo( + outConv); + + // nonexistent conversation returns the (not modified) parent channel + assertThat(mNotificationManager.getNotificationChannel("id", "nonexistent")).isEqualTo( + original); + assertThat(mNotificationManager.getNotificationChannel("id", "nonexistent")).isNotEqualTo( + out); + } + + @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) public void getNotificationChannelGroup_cachedUntilInvalidated() throws Exception { // Data setup: group has some channels in it NotificationChannelGroup g1 = new NotificationChannelGroup("g1", "group one"); @@ -521,6 +559,37 @@ public class NotificationManagerTest { } @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannelGroup_localModificationDoesNotChangeCache() throws Exception { + // Group setup + NotificationChannelGroup g1 = new NotificationChannelGroup("g1", "group one"); + NotificationChannel nc1 = new NotificationChannel("nc1", "channel one", + NotificationManager.IMPORTANCE_DEFAULT); + nc1.setGroup("g1"); + NotificationChannel nc2 = new NotificationChannel("nc2", "channel two", + NotificationManager.IMPORTANCE_DEFAULT); + nc2.setGroup("g1"); + + NotificationManager.invalidateNotificationChannelCache(); + NotificationManager.invalidateNotificationChannelGroupCache(); + when(mNotificationManager.mBackendService.getNotificationChannelGroupsWithoutChannels( + any())).thenReturn(new ParceledListSlice<>(List.of(g1.clone()))); + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), anyInt())) + .thenReturn(new ParceledListSlice<>(List.of(nc1.copy(), nc2.copy()))); + + NotificationChannelGroup g1result = mNotificationManager.getNotificationChannelGroup("g1"); + g1result.setDescription("something different!"); + for (NotificationChannel c : g1result.getChannels()) { + c.setDescription("also something different"); + } + + // expected output equivalent to original, unchanged + NotificationChannelGroup expectedG1 = g1.clone(); + expectedG1.setChannels(List.of(nc1, nc2)); + assertThat(mNotificationManager.getNotificationChannelGroup("g1")).isEqualTo(expectedG1); + } + + @Test @EnableFlags(Flags.FLAG_MODES_UI) public void areAutomaticZenRulesUserManaged_handheld_isTrue() { PackageManager pm = mock(PackageManager.class); diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java index 157c74abc5de..0287956bd07f 100644 --- a/core/tests/coretests/src/android/app/NotificationTest.java +++ b/core/tests/coretests/src/android/app/NotificationTest.java @@ -462,7 +462,7 @@ public class NotificationTest { @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) - public void testHasPromotableCharacteristics() { + public void testHasPromotableCharacteristics_bigText_bigTitle() { Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG")) @@ -475,6 +475,20 @@ public class NotificationTest { @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testHasPromotableCharacteristics_bigText_normalTitle() { + Notification n = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(new Notification.BigTextStyle()) + .setContentTitle("TITLE") + .setColor(Color.WHITE) + .setColorized(true) + .setOngoing(true) + .build(); + assertThat(n.hasPromotableCharacteristics()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableCharacteristics_notOngoing() { Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) @@ -526,6 +540,51 @@ public class NotificationTest { @Test @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testHasPromotableCharacteristics_noStyle_onlyBigTitle() { + Bundle extras = new Bundle(); + extras.putString(Notification.EXTRA_TITLE_BIG, "BIG"); + Notification n = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setColor(Color.WHITE) + .setColorized(true) + .setOngoing(true) + .addExtras(extras) + .build(); + assertThat(n.hasPromotableCharacteristics()).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testHasPromotableCharacteristics_ongoingCallStyle_notColorized() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE); + Person person = new Person.Builder().setName("Caller").build(); + Notification n = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(Notification.CallStyle.forOngoingCall(person, intent)) + .setColor(Color.WHITE) + .setOngoing(true) + .build(); + assertThat(n.hasPromotableCharacteristics()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testHasPromotableCharacteristics_incomingCallStyle_notColorized() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE); + Person person = new Person.Builder().setName("Caller").build(); + Notification n = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(Notification.CallStyle.forIncomingCall(person, intent, intent)) + .setColor(Color.WHITE) + .setOngoing(true) + .build(); + assertThat(n.hasPromotableCharacteristics()).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) public void testHasPromotableCharacteristics_groupSummary() { Notification n = new Notification.Builder(mContext, "test") .setSmallIcon(android.R.drawable.sym_def_app_icon) diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java index 1f1000f2800d..74023670c460 100644 --- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java +++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java @@ -25,6 +25,8 @@ import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; +import static com.android.window.flags.Flags.FLAG_SUPPORT_WIDGET_INTENTS_ON_CONNECTED_DISPLAY; + import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -39,11 +41,15 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.Activity; +import android.app.ActivityOptions; import android.app.ActivityThread; import android.app.ActivityThread.ActivityClientRecord; +import android.app.ActivityThread.ReceiverData; import android.app.Application; import android.app.IApplicationThread; import android.app.PictureInPictureParams; @@ -158,10 +164,7 @@ public class ActivityThreadTest { @After public void tearDown() { - if (mCreatedVirtualDisplays != null) { - mCreatedVirtualDisplays.forEach(VirtualDisplay::release); - mCreatedVirtualDisplays = null; - } + tearDownVirtualDisplays(); WindowTokenClientController.overrideForTesting(mOriginalWindowTokenClientController); ClientTransactionListenerController.getInstance() .unregisterActivityWindowInfoChangedListener(mActivityWindowInfoListener); @@ -1007,6 +1010,92 @@ public class ActivityThreadTest { .that(systemContext.getApplicationInfo()).isSameInstanceAs(newAppInfo); } + @Test + @RequiresFlagsEnabled(FLAG_SUPPORT_WIDGET_INTENTS_ON_CONNECTED_DISPLAY) + public void tesScheduleReceiver_withLaunchDisplayId_receivesDisplayContext() { + final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + final Display virtualDisplay = createVirtualDisplay(context, 100 /* w */, 100 /* h */); + final int virtualDisplayId = virtualDisplay.getDisplayId(); + final ActivityOptions activityOptions = + ActivityOptions.makeBasic().setLaunchDisplayId(virtualDisplayId); + final ActivityThread activityThread = ActivityThread.currentActivityThread(); + + final ReceiverData data = createReceiverData(activityOptions.toBundle()); + final Context resultContext = + activityThread.createDisplayContextIfNeeded(context, data); + + final Display resultDisplay = resultContext.getDisplayNoVerify(); + assertThat(resultDisplay).isNotNull(); + assertThat(resultDisplay.getDisplayId()).isEqualTo(virtualDisplayId); + assertThat(resultContext.getAssociatedDisplayId()).isEqualTo(virtualDisplayId); + } + + @Test + @RequiresFlagsEnabled(FLAG_SUPPORT_WIDGET_INTENTS_ON_CONNECTED_DISPLAY) + public void tesScheduleReceiver_withNotExistDisplayId_receivesNoneUiContext() { + final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + final Display virtualDisplay = createVirtualDisplay(context, 100 /* w */, 100 /* h */); + final int virtualDisplayId = virtualDisplay.getDisplayId(); + final ActivityOptions activityOptions = + ActivityOptions.makeBasic().setLaunchDisplayId(virtualDisplayId); + final ActivityThread activityThread = ActivityThread.currentActivityThread(); + tearDownVirtualDisplays(); + + final ReceiverData data = createReceiverData(activityOptions.toBundle()); + final Context resultContext = activityThread.createDisplayContextIfNeeded(context, data); + + assertThat(resultContext).isEqualTo(context); + } + + @Test + @RequiresFlagsEnabled(FLAG_SUPPORT_WIDGET_INTENTS_ON_CONNECTED_DISPLAY) + public void tesScheduleReceiver_withInvalidDisplay_receivesNoneUiContext() { + final Context context = mock(Context.class); + final ActivityOptions activityOptions = + ActivityOptions.makeBasic().setLaunchDisplayId(INVALID_DISPLAY); + final ActivityThread activityThread = ActivityThread.currentActivityThread(); + + final ReceiverData data = createReceiverData(activityOptions.toBundle()); + final Context resultContext = activityThread.createDisplayContextIfNeeded(context, data); + + verify(context, never()).createDisplayContext(any()); + assertThat(resultContext).isEqualTo(context); + } + + @Test + @RequiresFlagsEnabled(FLAG_SUPPORT_WIDGET_INTENTS_ON_CONNECTED_DISPLAY) + public void tesScheduleReceiver_withoutDisplayManagerService_receivesNoneUiContext() { + final Context context = mock(Context.class); + when(context.getSystemService(DisplayManager.class)).thenReturn(null); + final ActivityThread activityThread = ActivityThread.currentActivityThread(); + + final ReceiverData data = createReceiverData(null /* resultExtras */); + final Context resultContext = activityThread.createDisplayContextIfNeeded(context, data); + + verify(context, never()).createDisplayContext(any()); + assertThat(resultContext).isEqualTo(context); + } + + @Test + public void tesScheduleReceiver_withoutActivityOptions_receivesNoneUiContext() { + final Context context = mock(Context.class); + final ActivityThread activityThread = ActivityThread.currentActivityThread(); + + final ReceiverData data = createReceiverData(null /* resultExtras */); + final Context resultContext = activityThread.createDisplayContextIfNeeded(context, data); + + verify(context, never()).createDisplayContext(any()); + assertThat(resultContext).isEqualTo(context); + } + + @NonNull + private ReceiverData createReceiverData(@Nullable Bundle resultExtras) { + return new ReceiverData(new Intent("test.action.WIDGET_ITEM_CLICK"), + 0 /* resultCode */, null /* resultData */, resultExtras, false /* ordered */, + false /* sticky */, false /* assumeDelivered */, null /* token */, + 0 /* sendingUser */, -1 /* sendingUid */, null /* sendingPackage */); + } + /** * Calls {@link ActivityThread#handleActivityConfigurationChanged(ActivityClientRecord, * Configuration, int, ActivityWindowInfo)} to try to push activity configuration to the @@ -1056,6 +1145,13 @@ public class ActivityThreadTest { return virtualDisplay.getDisplay(); } + private void tearDownVirtualDisplays() { + if (mCreatedVirtualDisplays != null) { + mCreatedVirtualDisplays.forEach(VirtualDisplay::release); + mCreatedVirtualDisplays = null; + } + } + private static ActivityClientRecord getActivityClientRecord(Activity activity) { final ActivityThread thread = activity.getActivityThread(); final IBinder token = activity.getActivityToken(); diff --git a/core/tests/coretests/src/android/appwidget/AppWidgetEventsTest.kt b/core/tests/coretests/src/android/appwidget/AppWidgetEventsTest.kt index ea1158c88055..0135378ba681 100644 --- a/core/tests/coretests/src/android/appwidget/AppWidgetEventsTest.kt +++ b/core/tests/coretests/src/android/appwidget/AppWidgetEventsTest.kt @@ -16,14 +16,33 @@ package android.appwidget +import android.app.PendingIntent +import android.appwidget.AppWidgetHostView.InteractionLogger.MAX_NUM_ITEMS +import android.content.Intent import android.graphics.Rect +import android.view.View +import android.widget.ListView +import android.widget.RemoteViews import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.android.frameworks.coretests.R import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class AppWidgetEventsTest { + private val context = InstrumentationRegistry.getInstrumentation().targetContext!! + private val hostView = AppWidgetHostView(context).apply { + setAppWidget(0, AppWidgetManager.getInstance(context).installedProviders.first()) + } + private val pendingIntent = PendingIntent.getActivity( + context, + 0, + Intent(), + PendingIntent.FLAG_IMMUTABLE, + ) + @Test fun createWidgetInteractionEvent() { val appWidgetId = 1 @@ -48,4 +67,123 @@ class AppWidgetEventsTest { assertThat(bundle.getIntArray(AppWidgetManager.EXTRA_EVENT_SCROLLED_VIEWS)) .asList().containsExactly(scrolled[0], scrolled[1], scrolled[2]) } + + @Test + fun interactionLogger_click() { + val itemCount = MAX_NUM_ITEMS + 1 + // Set a different value for the viewId to test that the logger always uses the + // metrics tag if available. + fun viewId(i: Int) = i + Int.MIN_VALUE + val remoteViews = RemoteViews(context.packageName, R.layout.remote_views_test).apply { + for (i in 0 until itemCount) { + val metricsTag = i + val item = + RemoteViews(context.packageName, R.layout.remote_views_text, viewId(i)).apply { + setUsageEventTag(viewId(i), metricsTag) + setOnClickPendingIntent(viewId(i), pendingIntent) + } + addView(R.id.layout, item) + } + } + hostView.updateAppWidget(remoteViews) + assertThat(hostView.interactionLogger.clickedIds).isEmpty() + + + for (i in 0 until itemCount.minus(1)) { + val item = hostView.findViewById<View>(viewId(i)) + assertThat(item).isNotNull() + assertThat(item.performClick()).isTrue() + assertThat(hostView.interactionLogger.clickedIds) + .containsExactlyElementsIn(0..i) + } + assertThat(hostView.interactionLogger.clickedIds).hasSize(MAX_NUM_ITEMS) + + // Last item click should not be recorded because we've reached MAX_VIEW_IDS + val lastItem = hostView.findViewById<View>(viewId(itemCount - 1)) + assertThat(lastItem).isNotNull() + assertThat(lastItem.performClick()).isTrue() + assertThat(hostView.interactionLogger.clickedIds).hasSize(MAX_NUM_ITEMS) + assertThat(hostView.interactionLogger.clickedIds) + .containsExactlyElementsIn(0..itemCount.minus(2)) + } + + @Test + fun interactionLogger_click_listItem() { + val itemCount = 5 + val remoteViews = RemoteViews(context.packageName, R.layout.remote_views_list).apply { + setPendingIntentTemplate(R.id.list, pendingIntent) + setRemoteAdapter( + R.id.list, + RemoteViews.RemoteCollectionItems.Builder().run { + for (i in 0 until itemCount) { + val item = RemoteViews(context.packageName, R.layout.remote_views_test) + item.setOnClickFillInIntent(R.id.text, Intent()) + item.setUsageEventTag(R.id.text, i) + addItem(i.toLong(), item) + } + build() + } + ) + setUsageEventTag(R.id.list, -1) + } + hostView.updateAppWidget(remoteViews) + assertThat(hostView.interactionLogger.clickedIds).isEmpty() + + val list = hostView.findViewById<ListView>(R.id.list) + assertThat(list).isNotNull() + list.layout(0, 0, 500, 500) + for (i in 0 until itemCount) { + val item = list.getChildAt(i).findViewById<View>(R.id.text) + assertThat(item.performClick()).isTrue() + assertThat(hostView.interactionLogger.clickedIds) + .containsExactlyElementsIn(0..i) + } + } + + @Test + fun interactionLogger_scroll() { + val itemCount = MAX_NUM_ITEMS + 1 + // Set a different value for the viewId to test that the logger always uses the + // metrics tag if available. + fun viewId(i: Int) = i + Int.MIN_VALUE + val remoteViews = RemoteViews(context.packageName, R.layout.remote_views_test).apply { + for (i in 0 until itemCount) { + val metricsTag = i + val item = + RemoteViews(context.packageName, R.layout.remote_views_list, viewId(i)).apply { + setUsageEventTag(viewId(i), metricsTag) + setRemoteAdapter( + viewId(i), + RemoteViews.RemoteCollectionItems.Builder().run { + addItem( + 0L, + RemoteViews(context.packageName, R.layout.remote_views_test) + ) + build() + } + ) + } + addView(R.id.layout, item) + } + } + hostView.updateAppWidget(remoteViews) + assertThat(hostView.interactionLogger.scrolledIds).isEmpty() + + for (i in 0 until itemCount.minus(1)) { + val item = hostView.findViewById<ListView>(viewId(i)) + assertThat(item).isNotNull() + item.fling(/* velocityY= */ 100) + assertThat(hostView.interactionLogger.scrolledIds) + .containsExactlyElementsIn(0..i) + } + assertThat(hostView.interactionLogger.scrolledIds).hasSize(MAX_NUM_ITEMS) + + // Last item scroll should not be recorded because we've reached MAX_VIEW_IDS + val lastItem = hostView.findViewById<ListView>(viewId(itemCount - 1)) + assertThat(lastItem).isNotNull() + lastItem.fling(/* velocityY= */ 100) + assertThat(hostView.interactionLogger.scrolledIds).hasSize(MAX_NUM_ITEMS) + assertThat(hostView.interactionLogger.scrolledIds) + .containsExactlyElementsIn(0..itemCount.minus(2)) + } } diff --git a/core/tests/coretests/src/android/content/res/XmlResourcesFlaggedTest.kt b/core/tests/coretests/src/android/content/res/XmlResourcesFlaggedTest.kt new file mode 100644 index 000000000000..820bcfc03724 --- /dev/null +++ b/core/tests/coretests/src/android/content/res/XmlResourcesFlaggedTest.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.content.res + +import android.platform.test.annotations.Presubmit +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.CheckFlagsRule +import android.platform.test.flag.junit.DeviceFlagsValueProvider +import android.util.TypedValue + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry + +import com.android.frameworks.coretests.R +import com.android.internal.pm.pkg.parsing.ParsingPackageUtils + +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertTrue + + +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException + +import java.io.IOException + +/** +* Tests for flag handling within Resources.loadXmlResourceParser() and methods that call it. +*/ +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.DisabledOnRavenwood(bug = 396458006, + reason = "Resource flags don't fully work on Ravenwood yet") +class XmlResourcesFlaggedTest { + @get:Rule + val mCheckFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + + private var mResources: Resources = Resources(null) + + @Before + fun setup() { + mResources = InstrumentationRegistry.getInstrumentation().getContext().getResources() + mResources.getImpl().flushLayoutCache() + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_LAYOUT_READWRITE_FLAGS) + fun flaggedXmlTypedValueMarkedAsSuch() { + val tv = TypedValue() + mResources.getImpl().getValue(R.xml.flags, tv, false) + assertTrue(tv.usesFeatureFlags) + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_LAYOUT_READWRITE_FLAGS) + @Throws(IOException::class, XmlPullParserException::class) + fun parsedFlaggedXmlWithTrueOneElement() { + ParsingPackageUtils.getAconfigFlags() + .addFlagValuesForTesting(mapOf("android.content.res.always_false" to false)) + val tv = TypedValue() + mResources.getImpl().getValue(R.xml.flags, tv, false) + val parser = mResources.loadXmlResourceParser( + tv.string.toString(), + R.xml.flags, + tv.assetCookie, + "xml", + true + ) + assertEquals(XmlPullParser.START_DOCUMENT, parser.next()) + assertEquals(XmlPullParser.START_TAG, parser.next()) + assertEquals("first", parser.getName()) + assertEquals(XmlPullParser.END_TAG, parser.next()) + assertEquals(XmlPullParser.END_DOCUMENT, parser.next()) + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_LAYOUT_READWRITE_FLAGS) + @Throws(IOException::class, XmlPullParserException::class) + fun parsedFlaggedXmlWithFalseTwoElements() { + val tv = TypedValue() + mResources.getImpl().getValue(R.xml.flags, tv, false) + val parser = mResources.loadXmlResourceParser( + tv.string.toString(), + R.xml.flags, + tv.assetCookie, + "xml", + false + ) + assertEquals(XmlPullParser.START_DOCUMENT, parser.next()) + assertEquals(XmlPullParser.START_TAG, parser.next()) + assertEquals("first", parser.getName()) + assertEquals(XmlPullParser.START_TAG, parser.next()) + assertEquals("second", parser.getName()) + assertEquals(XmlPullParser.END_TAG, parser.next()) + assertEquals(XmlPullParser.END_TAG, parser.next()) + assertEquals(XmlPullParser.END_DOCUMENT, parser.next()) + } +} diff --git a/core/tests/coretests/src/android/text/LayoutTest.java b/core/tests/coretests/src/android/text/LayoutTest.java index 11ec9f8e1912..7d8afcabad7b 100644 --- a/core/tests/coretests/src/android/text/LayoutTest.java +++ b/core/tests/coretests/src/android/text/LayoutTest.java @@ -1029,51 +1029,16 @@ public class LayoutTest { @Test @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT) - public void highContrastTextEnabled_testWhitespaceText_DrawsBackgroundsWithAdjacentLetters() { - mTextPaint.setColor(Color.BLACK); - SpannableString spannedText = new SpannableString("Test\tTap and Space"); - - // Set the entire text to white initially - spannedText.setSpan( - new ForegroundColorSpan(Color.WHITE), - /* start= */ 0, - /* end= */ spannedText.length(), - Spanned.SPAN_INCLUSIVE_EXCLUSIVE - ); - - // Find the whitespace character and set its color to black - for (int i = 0; i < spannedText.length(); i++) { - if (Character.isWhitespace(spannedText.charAt(i))) { - spannedText.setSpan( - new ForegroundColorSpan(Color.BLACK), - i, - i + 1, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE - ); - } - } - - Layout layout = new StaticLayout(spannedText, mTextPaint, mWidth, - mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false); - - MockCanvas c = new MockCanvas(/* width= */ 256, /* height= */ 256); - c.setHighContrastTextEnabled(true); - layout.draw( - c, - /* highlightPaths= */ null, - /* highlightPaints= */ null, - /* selectionPath= */ null, - /* selectionPaint= */ null, - /* cursorOffsetVertical= */ 0 - ); + public void highContrastTextEnabled_testWhiteSpaceWithinText_drawsSameBackgroundswithText() { + SpannableString spannedText = new SpannableString("Hello\tWorld !"); + testSpannableStringAppliesAllColorsCorrectly(spannedText); + } - List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands(); - for (int i = 0; i < drawCommands.size(); i++) { - MockCanvas.DrawCommand drawCommand = drawCommands.get(i); - if (drawCommand.rect != null) { - expect.that(removeAlpha(drawCommand.paint.getColor())).isEqualTo(Color.BLACK); - } - } + @Test + @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT) + public void highContrastTextEnabled_testWhiteSpaceAtStart_drawsCorrectBackgroundsOnText() { + SpannableString spannedText = new SpannableString(" HelloWorld!"); + testSpannableStringAppliesAllColorsCorrectly(spannedText); } @Test @@ -1331,5 +1296,54 @@ public class LayoutTest { "", new boolean[]{false}); } + + private void testSpannableStringAppliesAllColorsCorrectly(SpannableString spannedText) { + for (int textColor : new int[] {Color.WHITE, Color.BLACK}) { + final int contrastingColor = textColor == Color.WHITE ? Color.BLACK : Color.WHITE; + // Set the paint color to the contrasting color to verify the high contrast text + // background rect color is correct. + mTextPaint.setColor(contrastingColor); + + // Set the entire text to test color initially + spannedText.setSpan( + new ForegroundColorSpan(textColor), + /* start= */ 0, + /* end= */ spannedText.length(), + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ); + + Layout layout = new StaticLayout(spannedText, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false); + + MockCanvas c = new MockCanvas(/* width= */ 256, /* height= */ 256); + c.setHighContrastTextEnabled(true); + layout.draw( + c, + /* highlightPaths= */ null, + /* highlightPaints= */ null, + /* selectionPath= */ null, + /* selectionPaint= */ null, + /* cursorOffsetVertical= */ 0 + ); + + int numBackgroundsFound = 0; + List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands(); + for (int i = 0; i < drawCommands.size(); i++) { + MockCanvas.DrawCommand drawCommand = drawCommands.get(i); + + if (drawCommand.rect != null) { + numBackgroundsFound++; + // Verifies the background color of the high-contrast rectangle drawn behind + // the text. In high-contrast mode, the background color should contrast with + // the text color. 'contrastingColor' represents the expected background color, + // which is the inverse of the text color (e.g., if text is white, background + // is black, and vice versa). + expect.that(removeAlpha(drawCommand.paint.getColor())) + .isEqualTo(contrastingColor); + } + } + expect.that(numBackgroundsFound).isLessThan(spannedText.length()); + } + } } diff --git a/core/tests/coretests/src/android/util/ArrayMapTest.java b/core/tests/coretests/src/android/util/ArrayMapTest.java index d71a60323311..a00f530ee227 100644 --- a/core/tests/coretests/src/android/util/ArrayMapTest.java +++ b/core/tests/coretests/src/android/util/ArrayMapTest.java @@ -18,7 +18,6 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; -import android.platform.test.annotations.DisabledOnRavenwood; import android.platform.test.annotations.Presubmit; import android.platform.test.ravenwood.RavenwoodRule; @@ -57,7 +56,6 @@ public class ArrayMapTest { */ @Test @Ignore("Failing; b/399137661") - @DisabledOnRavenwood(reason = "Long test runtime") public void testConcurrentModificationException() throws Exception { final int TEST_LEN_MS = 5000; System.out.println("Starting ArrayMap concurrency test"); diff --git a/core/tests/coretests/src/android/util/ArraySetTest.java b/core/tests/coretests/src/android/util/ArraySetTest.java index 8888991ffcda..dec7b596a1e9 100644 --- a/core/tests/coretests/src/android/util/ArraySetTest.java +++ b/core/tests/coretests/src/android/util/ArraySetTest.java @@ -22,6 +22,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; import org.junit.After; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -50,6 +51,7 @@ public class ArraySetTest { * internals. */ @Test + @Ignore("Failing; b/399137661") public void testConcurrentModificationException() throws Exception { final int testDurMs = 10_000; System.out.println("Starting ArraySet concurrency test"); diff --git a/core/tests/coretests/src/android/view/ScrollCaptureConnectionTest.java b/core/tests/coretests/src/android/view/ScrollCaptureConnectionTest.java index bee5dc4bf3c0..81954cb9a1a9 100644 --- a/core/tests/coretests/src/android/view/ScrollCaptureConnectionTest.java +++ b/core/tests/coretests/src/android/view/ScrollCaptureConnectionTest.java @@ -16,8 +16,6 @@ package android.view; -import static androidx.test.InstrumentationRegistry.getTargetContext; - import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -32,7 +30,6 @@ import static org.mockito.Mockito.when; import android.graphics.Point; import android.graphics.Rect; import android.os.Binder; -import android.os.Handler; import android.os.IBinder; import android.os.ICancellationSignal; import android.os.RemoteException; @@ -54,7 +51,6 @@ import java.util.concurrent.Executor; /** * Tests of {@link ScrollCaptureConnection}. */ -@SuppressWarnings("UnnecessaryLocalVariable") @Presubmit @SmallTest @RunWith(AndroidJUnit4.class) @@ -68,9 +64,8 @@ public class ScrollCaptureConnectionTest { private ScrollCaptureTarget mTarget; private ScrollCaptureConnection mConnection; - private IBinder mConnectionBinder = new Binder("ScrollCaptureConnection Test"); + private final IBinder mConnectionBinder = new Binder("ScrollCaptureConnection Test"); - private Handler mHandler; @Mock private Surface mSurface; @@ -85,7 +80,6 @@ public class ScrollCaptureConnectionTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); - mHandler = new Handler(getTargetContext().getMainLooper()); when(mSurface.isValid()).thenReturn(true); when(mView.getScrollCaptureHint()).thenReturn(View.SCROLL_CAPTURE_HINT_INCLUDE); when(mRemote.asBinder()).thenReturn(mConnectionBinder); @@ -269,8 +263,68 @@ public class ScrollCaptureConnectionTest { assertFalse(mConnection.isConnected()); } + @Test(expected = RemoteException.class) + public void testRequestImage_beforeStarted() throws RemoteException { + mConnection.requestImage(new Rect(0, 1, 2, 3)); + } + + + @Test(expected = RemoteException.class) + public void testRequestImage_beforeStartCompleted() throws RemoteException { + mFakeUiThread.setImmediate(false); + mConnection.startCapture(mSurface, mRemote); + mConnection.requestImage(new Rect(0, 1, 2, 3)); + mFakeUiThread.runAll(); + } + + @Test + public void testCompleteStart_afterClosing() throws RemoteException { + mConnection.startCapture(mSurface, mRemote); + mConnection.close(); + mFakeUiThread.setImmediate(false); + mCallback.completeStartRequest(); + mFakeUiThread.runAll(); + } + + @Test + public void testLateCallbacks() throws RemoteException { + mConnection.startCapture(mSurface, mRemote); + mCallback.completeStartRequest(); + mConnection.requestImage(new Rect(1, 2, 3, 4)); + mConnection.endCapture(); + mFakeUiThread.setImmediate(false); + mCallback.completeImageRequest(new Rect(1, 2, 3, 4)); + mCallback.completeEndRequest(); + mFakeUiThread.runAll(); + } + + @Test + public void testDelayedClose() throws RemoteException { + mConnection.startCapture(mSurface, mRemote); + mCallback.completeStartRequest(); + mFakeUiThread.setImmediate(false); + mConnection.endCapture(); + mFakeUiThread.runAll(); + mConnection.close(); + mCallback.completeEndRequest(); + mFakeUiThread.runAll(); + } + + @Test + public void testRequestImage_delayedCancellation() throws Exception { + mConnection.startCapture(mSurface, mRemote); + mCallback.completeStartRequest(); + + ICancellationSignal signal = mConnection.requestImage(new Rect(1, 2, 3, 4)); + mFakeUiThread.setImmediate(false); + + signal.cancel(); + mCallback.completeImageRequest(new Rect(1, 2, 3, 4)); + } + + static class FakeExecutor implements Executor { - private Queue<Runnable> mQueue = new ArrayDeque<>(); + private final Queue<Runnable> mQueue = new ArrayDeque<>(); private boolean mImmediate; @Override diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java index 5774109e1451..1b7805c351db 100644 --- a/core/tests/coretests/src/android/view/ViewRootImplTest.java +++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java @@ -16,8 +16,6 @@ package android.view; -import static android.app.UiModeManager.MODE_NIGHT_NO; -import static android.app.UiModeManager.MODE_NIGHT_YES; import static android.util.SequenceUtils.getInitSeq; import static android.view.HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING; import static android.view.InputDevice.SOURCE_ROTARY_ENCODER; @@ -69,10 +67,9 @@ import static org.junit.Assume.assumeTrue; import android.annotation.NonNull; import android.app.Instrumentation; import android.app.UiModeManager; -import android.app.UiModeManager.ForceInvertType; import android.content.Context; +import android.graphics.Color; import android.graphics.ForceDarkType; -import android.graphics.ForceDarkType.ForceDarkTypeDef; import android.graphics.Rect; import android.hardware.display.DisplayManagerGlobal; import android.os.Binder; @@ -101,8 +98,6 @@ import com.android.compatibility.common.util.TestUtils; import com.android.cts.input.BlockingQueueEventVerifier; import com.android.window.flags.Flags; -import com.google.common.truth.Expect; - import org.hamcrest.Matcher; import org.junit.After; import org.junit.AfterClass; @@ -131,8 +126,6 @@ public class ViewRootImplTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - @Rule - public final Expect mExpect = Expect.create(); private ViewRootImpl mViewRootImpl; private View mView; @@ -1516,29 +1509,83 @@ public class ViewRootImplTest { } @Test - @RequiresFlagsEnabled(FLAG_FORCE_INVERT_COLOR) - public void updateConfiguration_returnsExpectedForceDarkMode() { + @EnableFlags(FLAG_FORCE_INVERT_COLOR) + public void determineForceDarkType_systemLightMode_returnsNone() throws Exception { + waitForSystemNightModeActivated(false); + + TestUtils.waitUntil("Waiting for ForceDarkType to be ready", + () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE)); + + } + + @Test + @EnableFlags(FLAG_FORCE_INVERT_COLOR) + public void determineForceDarkType_systemNightModeAndDisableForceInvertColor_returnsNone() + throws Exception { waitForSystemNightModeActivated(true); - verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ true, - UiModeManager.FORCE_INVERT_TYPE_DARK, ForceDarkType.FORCE_INVERT_COLOR_DARK); - verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ false, - UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE); - verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ true, - UiModeManager.FORCE_INVERT_TYPE_DARK, ForceDarkType.FORCE_INVERT_COLOR_DARK); - verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ false, - UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE); + enableForceInvertColor(false); - waitForSystemNightModeActivated(false); + TestUtils.waitUntil("Waiting for ForceDarkType to be ready", + () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE)); + } + + @Test + @EnableFlags(FLAG_FORCE_INVERT_COLOR) + public void + determineForceDarkType_isLightThemeAndIsLightBackground_returnsForceInvertColorDark() + throws Exception { + // Set up configurations for force invert color + waitForSystemNightModeActivated(true); + enableForceInvertColor(true); + + setUpViewAttributes(/* isLightTheme= */ true, /* isLightBackground = */ true); - verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ true, - UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE); - verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ false, - UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE); - verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ true, - UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE); - verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ false, - UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE); + TestUtils.waitUntil("Waiting for ForceDarkType to be ready", + () -> (mViewRootImpl.determineForceDarkType() + == ForceDarkType.FORCE_INVERT_COLOR_DARK)); + } + + @Test + @EnableFlags(FLAG_FORCE_INVERT_COLOR) + public void determineForceDarkType_isLightThemeAndNotLightBackground_returnsNone() + throws Exception { + // Set up configurations for force invert color + waitForSystemNightModeActivated(true); + enableForceInvertColor(true); + + setUpViewAttributes(/* isLightTheme= */ true, /* isLightBackground = */ false); + + TestUtils.waitUntil("Waiting for ForceDarkType to be ready", + () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE)); + } + + @Test + @EnableFlags(FLAG_FORCE_INVERT_COLOR) + public void determineForceDarkType_notLightThemeAndIsLightBackground_returnsNone() + throws Exception { + // Set up configurations for force invert color + waitForSystemNightModeActivated(true); + enableForceInvertColor(true); + + setUpViewAttributes(/* isLightTheme= */ false, /* isLightBackground = */ true); + + TestUtils.waitUntil("Waiting for ForceDarkType to be ready", + () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE)); + } + + @Test + @EnableFlags(FLAG_FORCE_INVERT_COLOR) + public void determineForceDarkType_notLightThemeAndNotLightBackground_returnsNone() + throws Exception { + // Set up configurations for force invert color + waitForSystemNightModeActivated(true); + enableForceInvertColor(true); + + setUpViewAttributes(/* isLightTheme= */ false, /* isLightBackground = */ false); + + TestUtils.waitUntil("Waiting for ForceDarkType to be ready", + () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE)); } @Test @@ -1792,29 +1839,35 @@ public class ViewRootImplTest { sInstrumentation.waitForIdleSync(); } - private void verifyForceDarkType(boolean isAppInNightMode, boolean isForceInvertEnabled, - @ForceInvertType int expectedForceInvertType, - @ForceDarkTypeDef int expectedForceDarkType) { - var uiModeManager = sContext.getSystemService(UiModeManager.class); + private void enableForceInvertColor(boolean enabled) { ShellIdentityUtils.invokeWithShellPermissions(() -> { - uiModeManager.setApplicationNightMode( - isAppInNightMode ? MODE_NIGHT_YES : MODE_NIGHT_NO); Settings.Secure.putInt( sContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED, - isForceInvertEnabled ? 1 : 0); + enabled ? 1 : 0 + ); }); + } - sInstrumentation.runOnMainSync(() -> - mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId())); - try { - TestUtils.waitUntil("Waiting for force invert state changed", - () -> (uiModeManager.getForceInvertState() == expectedForceInvertType)); - } catch (Exception e) { - Log.e(TAG, "Unexpected error trying to apply force invert state. " + e); - e.printStackTrace(); - } + private void setUpViewAttributes(boolean isLightTheme, boolean isLightBackground) { + ShellIdentityUtils.invokeWithShellPermissions(() -> { + sContext.setTheme(isLightTheme ? android.R.style.Theme_DeviceDefault_Light + : android.R.style.Theme_DeviceDefault); + }); - mExpect.that(mViewRootImpl.determineForceDarkType()).isEqualTo(expectedForceDarkType); + sInstrumentation.runOnMainSync(() -> { + View view = new View(sContext); + WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams( + TYPE_APPLICATION_OVERLAY); + layoutParams.token = new Binder(); + view.setLayoutParams(layoutParams); + if (isLightBackground) { + view.setBackgroundColor(Color.WHITE); + } else { + view.setBackgroundColor(Color.BLACK); + } + mViewRootImpl.setView(view, layoutParams, /* panelParentView= */ null); + mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId()); + }); } } diff --git a/core/tests/coretests/src/com/android/internal/app/MediaRouteChooserContentManagerTest.kt b/core/tests/coretests/src/com/android/internal/app/MediaRouteChooserContentManagerTest.kt new file mode 100644 index 000000000000..bbed6e0c3618 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/app/MediaRouteChooserContentManagerTest.kt @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.app + +import android.content.Context +import android.media.MediaRouter +import android.testing.TestableLooper.RunWithLooper +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.widget.LinearLayout +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.internal.R +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +@SmallTest +@RunWithLooper(setAsMainLooper = true) +@RunWith(AndroidJUnit4::class) +class MediaRouteChooserContentManagerTest { + private val context: Context = getInstrumentation().context + + @Test + fun bindViews_showProgressBarWhenEmptyTrue_progressBarVisible() { + val delegate = mock<MediaRouteChooserContentManager.Delegate> { + on { showProgressBarWhenEmpty() } doReturn true + } + val contentManager = MediaRouteChooserContentManager(context, delegate) + val containerView = inflateMediaRouteChooserDialog() + contentManager.bindViews(containerView) + + assertThat(containerView.findViewById<View>(R.id.media_route_progress_bar).visibility) + .isEqualTo(View.VISIBLE) + } + + @Test + fun bindViews_showProgressBarWhenEmptyFalse_progressBarNotVisible() { + val delegate = mock<MediaRouteChooserContentManager.Delegate> { + on { showProgressBarWhenEmpty() } doReturn false + } + val contentManager = MediaRouteChooserContentManager(context, delegate) + val containerView = inflateMediaRouteChooserDialog() + contentManager.bindViews(containerView) + val emptyView = containerView.findViewById<View>(android.R.id.empty) + val emptyViewLayout = emptyView.layoutParams as? LinearLayout.LayoutParams + + assertThat(containerView.findViewById<View>(R.id.media_route_progress_bar).visibility) + .isEqualTo(View.GONE) + assertThat(emptyView.visibility).isEqualTo(View.VISIBLE) + assertThat(emptyViewLayout?.gravity).isEqualTo(Gravity.CENTER) + } + + @Test + fun onFilterRoute_routeDefault_returnsFalse() { + val delegate: MediaRouteChooserContentManager.Delegate = mock() + val contentManager = MediaRouteChooserContentManager(context, delegate) + val route: MediaRouter.RouteInfo = mock<MediaRouter.RouteInfo> { + on { isDefault } doReturn true + } + + assertThat(contentManager.onFilterRoute(route)).isEqualTo(false) + } + + @Test + fun onFilterRoute_routeNotEnabled_returnsFalse() { + val delegate: MediaRouteChooserContentManager.Delegate = mock() + val contentManager = MediaRouteChooserContentManager(context, delegate) + val route: MediaRouter.RouteInfo = mock<MediaRouter.RouteInfo> { + on { isEnabled } doReturn false + } + + assertThat(contentManager.onFilterRoute(route)).isEqualTo(false) + } + + @Test + fun onFilterRoute_routeNotMatch_returnsFalse() { + val delegate: MediaRouteChooserContentManager.Delegate = mock() + val contentManager = MediaRouteChooserContentManager(context, delegate) + val route: MediaRouter.RouteInfo = mock<MediaRouter.RouteInfo> { + on { matchesTypes(anyInt()) } doReturn false + } + + assertThat(contentManager.onFilterRoute(route)).isEqualTo(false) + } + + @Test + fun onFilterRoute_returnsTrue() { + val delegate: MediaRouteChooserContentManager.Delegate = mock() + val contentManager = MediaRouteChooserContentManager(context, delegate) + val route: MediaRouter.RouteInfo = mock<MediaRouter.RouteInfo> { + on { isDefault } doReturn false + on { isEnabled } doReturn true + on { matchesTypes(anyInt()) } doReturn true + } + + assertThat(contentManager.onFilterRoute(route)).isEqualTo(true) + } + + @Test + fun onAttachedToWindow() { + val delegate: MediaRouteChooserContentManager.Delegate = mock() + val mediaRouter: MediaRouter = mock() + val layoutInflater: LayoutInflater = mock() + val context: Context = mock<Context> { + on { getSystemServiceName(MediaRouter::class.java) } doReturn Context.MEDIA_ROUTER_SERVICE + on { getSystemService(MediaRouter::class.java) } doReturn mediaRouter + on { getSystemService(Context.LAYOUT_INFLATER_SERVICE) } doReturn layoutInflater + } + val contentManager = MediaRouteChooserContentManager(context, delegate) + contentManager.routeTypes = MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY + + contentManager.onAttachedToWindow() + + verify(mediaRouter).addCallback(eq(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY), any(), + eq(MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN)) + } + + @Test + fun onDetachedFromWindow() { + val delegate: MediaRouteChooserContentManager.Delegate = mock() + val layoutInflater: LayoutInflater = mock() + val mediaRouter: MediaRouter = mock() + val context: Context = mock<Context> { + on { getSystemServiceName(MediaRouter::class.java) } doReturn Context.MEDIA_ROUTER_SERVICE + on { getSystemService(MediaRouter::class.java) } doReturn mediaRouter + on { getSystemService(Context.LAYOUT_INFLATER_SERVICE) } doReturn layoutInflater + } + val contentManager = MediaRouteChooserContentManager(context, delegate) + + contentManager.onDetachedFromWindow() + + verify(mediaRouter).removeCallback(any()) + } + + @Test + fun setRouteTypes() { + val delegate: MediaRouteChooserContentManager.Delegate = mock() + val mediaRouter: MediaRouter = mock() + val layoutInflater: LayoutInflater = mock() + val context: Context = mock<Context> { + on { getSystemServiceName(MediaRouter::class.java) } doReturn Context.MEDIA_ROUTER_SERVICE + on { getSystemService(MediaRouter::class.java) } doReturn mediaRouter + on { getSystemService(Context.LAYOUT_INFLATER_SERVICE) } doReturn layoutInflater + } + val contentManager = MediaRouteChooserContentManager(context, delegate) + contentManager.onAttachedToWindow() + + contentManager.routeTypes = MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY + + assertThat(contentManager.routeTypes).isEqualTo(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY) + verify(mediaRouter).addCallback(eq(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY), any(), + eq(MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN)) + } + + private fun inflateMediaRouteChooserDialog(): View { + return LayoutInflater.from(context) + .inflate(R.layout.media_route_chooser_dialog, null, false) + } +} diff --git a/core/tests/coretests/src/com/android/internal/logging/ProtoLogTest.java b/core/tests/coretests/src/com/android/internal/logging/ProtoLogTest.java new file mode 100644 index 000000000000..90b6902629e6 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/logging/ProtoLogTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.logging; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.internal.protolog.ProtoLog; +import com.android.internal.protolog.common.IProtoLogGroup; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ProtoLogTest { + @Test + public void canTrace() { + ProtoLog.init(TEST_GROUP_1, TEST_GROUP_2); + + ProtoLog.v(TEST_GROUP_1, "Verbose message"); + ProtoLog.d(TEST_GROUP_1, "Debug message"); + ProtoLog.i(TEST_GROUP_1, "Info message"); + ProtoLog.w(TEST_GROUP_1, "Warning message"); + ProtoLog.e(TEST_GROUP_1, "Error message"); + ProtoLog.wtf(TEST_GROUP_1, "Wtf message"); + + ProtoLog.v(TEST_GROUP_2, "Verbose message"); + ProtoLog.d(TEST_GROUP_2, "Debug message"); + ProtoLog.i(TEST_GROUP_2, "Info message"); + ProtoLog.w(TEST_GROUP_2, "Warning message"); + ProtoLog.e(TEST_GROUP_2, "Error message"); + ProtoLog.wtf(TEST_GROUP_2, "Wtf message"); + } + + private static final IProtoLogGroup TEST_GROUP_1 = new ProtoLogTestGroup("TEST_TAG_1", 1); + private static final IProtoLogGroup TEST_GROUP_2 = new ProtoLogTestGroup("TEST_TAG_2", 2); + +} diff --git a/core/tests/coretests/src/com/android/internal/logging/ProtoLogTestGroup.java b/core/tests/coretests/src/com/android/internal/logging/ProtoLogTestGroup.java new file mode 100644 index 000000000000..ecfaae5ea0c5 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/logging/ProtoLogTestGroup.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.logging; + +import com.android.internal.protolog.common.IProtoLogGroup; + +class ProtoLogTestGroup implements IProtoLogGroup { + private final boolean mEnabled; + private volatile boolean mLogToProto; + private volatile boolean mLogToLogcat; + private final String mTag; + private final int mId; + + ProtoLogTestGroup(String tag, int id) { + this(true, true, false, tag, id); + } + + ProtoLogTestGroup( + boolean enabled, boolean logToProto, boolean logToLogcat, String tag, int id) { + this.mEnabled = enabled; + this.mLogToProto = logToProto; + this.mLogToLogcat = logToLogcat; + this.mTag = tag; + this.mId = id; + } + + @Override + public String name() { + return mTag; + } + + @Override + public boolean isEnabled() { + return mEnabled; + } + + @Override + public boolean isLogToProto() { + return mLogToProto; + } + + @Override + public boolean isLogToLogcat() { + return mLogToLogcat; + } + + @Override + public boolean isLogToAny() { + return mLogToLogcat || mLogToProto; + } + + @Override + public String getTag() { + return mTag; + } + + @Override + public void setLogToProto(boolean logToProto) { + this.mLogToProto = logToProto; + } + + @Override + public void setLogToLogcat(boolean logToLogcat) { + this.mLogToLogcat = logToLogcat; + } + + @Override + public int getId() { + return mId; + } +} diff --git a/core/tests/overlaytests/device/Android.bp b/core/tests/overlaytests/device/Android.bp index 2b22344a4ef2..db1bf9c295b6 100644 --- a/core/tests/overlaytests/device/Android.bp +++ b/core/tests/overlaytests/device/Android.bp @@ -28,8 +28,8 @@ android_test { certificate: "platform", static_libs: [ "androidx.test.rules", - "testng", "compatibility-device-util-axt", + "testng", ], test_suites: ["device-tests"], data: [ diff --git a/core/tests/overlaytests/device/res/values/config.xml b/core/tests/overlaytests/device/res/values/config.xml index a30d66f82128..e031b95f5d22 100644 --- a/core/tests/overlaytests/device/res/values/config.xml +++ b/core/tests/overlaytests/device/res/values/config.xml @@ -2,7 +2,7 @@ <resources> <string name="str">none</string> <string name="str2">none</string> - <integer name="overlaid">0</integer> + <integer name="overlaidInt">0</integer> <integer name="matrix_100000">100</integer> <integer name="matrix_100001">100</integer> <integer name="matrix_100010">100</integer> @@ -58,6 +58,8 @@ <item>19</item> </integer-array> + <item name="overlaidFloat" format="float" type="dimen">0</item> + <attr name="customAttribute" /> <id name="view_1" /> <id name="view_2" /> diff --git a/core/tests/overlaytests/device/src/com/android/overlaytest/FabricatedOverlaysTest.java b/core/tests/overlaytests/device/src/com/android/overlaytest/FabricatedOverlaysTest.java index 2da9a2ebbdb6..b48e3b7423ff 100644 --- a/core/tests/overlaytests/device/src/com/android/overlaytest/FabricatedOverlaysTest.java +++ b/core/tests/overlaytests/device/src/com/android/overlaytest/FabricatedOverlaysTest.java @@ -22,6 +22,7 @@ import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertThrows; +import android.annotation.NonNull; import android.content.Context; import android.content.om.FabricatedOverlay; import android.content.om.OverlayIdentifier; @@ -44,14 +45,17 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.util.Collections; +import java.util.Objects; import java.util.concurrent.TimeoutException; +import java.util.function.Function; @RunWith(JUnit4.class) @MediumTest public class FabricatedOverlaysTest { private static final String TAG = "FabricatedOverlaysTest"; - private final String TEST_RESOURCE = "integer/overlaid"; - private final String TEST_OVERLAY_NAME = "Test"; + private static final String TEST_INT_RESOURCE = "integer/overlaidInt"; + private static final String TEST_FLOAT_RESOURCE = "dimen/overlaidFloat"; + private static final String TEST_OVERLAY_NAME = "Test"; private Context mContext; private Resources mResources; @@ -84,10 +88,10 @@ public class FabricatedOverlaysTest { public void testFabricatedOverlay() throws Exception { final FabricatedOverlay overlay = new FabricatedOverlay.Builder( mContext.getPackageName(), TEST_OVERLAY_NAME, mContext.getPackageName()) - .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1) + .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1) .build(); - waitForResourceValue(0); + waitForIntResourceValue(0); mOverlayManager.commit(new OverlayManagerTransaction.Builder() .registerFabricatedOverlay(overlay) .build()); @@ -104,63 +108,63 @@ public class FabricatedOverlaysTest { assertNotNull(info); assertTrue(info.isEnabled()); - waitForResourceValue(1); + waitForIntResourceValue(1); mOverlayManager.commit(new OverlayManagerTransaction.Builder() .unregisterFabricatedOverlay(overlay.getIdentifier()) .build()); - waitForResourceValue(0); + waitForIntResourceValue(0); } @Test public void testRegisterEnableAtomic() throws Exception { final FabricatedOverlay overlay = new FabricatedOverlay.Builder( mContext.getPackageName(), TEST_OVERLAY_NAME, mContext.getPackageName()) - .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1) + .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1) .build(); - waitForResourceValue(0); + waitForIntResourceValue(0); mOverlayManager.commit(new OverlayManagerTransaction.Builder() .registerFabricatedOverlay(overlay) .setEnabled(overlay.getIdentifier(), true, mUserId) .build()); - waitForResourceValue(1); + waitForIntResourceValue(1); } @Test public void testRegisterTwice() throws Exception { FabricatedOverlay overlay = new FabricatedOverlay.Builder( mContext.getPackageName(), TEST_OVERLAY_NAME, mContext.getPackageName()) - .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1) + .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1) .build(); - waitForResourceValue(0); + waitForIntResourceValue(0); mOverlayManager.commit(new OverlayManagerTransaction.Builder() .registerFabricatedOverlay(overlay) .setEnabled(overlay.getIdentifier(), true, mUserId) .build()); - waitForResourceValue(1); + waitForIntResourceValue(1); overlay = new FabricatedOverlay.Builder( mContext.getPackageName(), TEST_OVERLAY_NAME, mContext.getPackageName()) - .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 2) + .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 2) .build(); mOverlayManager.commit(new OverlayManagerTransaction.Builder() .registerFabricatedOverlay(overlay) .build()); - waitForResourceValue(2); + waitForIntResourceValue(2); } @Test public void testInvalidOwningPackageName() throws Exception { final FabricatedOverlay overlay = new FabricatedOverlay.Builder( "android", TEST_OVERLAY_NAME, mContext.getPackageName()) - .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1) + .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1) .build(); - waitForResourceValue(0); + waitForIntResourceValue(0); assertThrows(SecurityException.class, () -> mOverlayManager.commit(new OverlayManagerTransaction.Builder() .registerFabricatedOverlay(overlay) @@ -174,10 +178,10 @@ public class FabricatedOverlaysTest { public void testInvalidOverlayName() throws Exception { final FabricatedOverlay overlay = new FabricatedOverlay.Builder( mContext.getPackageName(), "invalid@name", mContext.getPackageName()) - .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1) + .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1) .build(); - waitForResourceValue(0); + waitForIntResourceValue(0); assertThrows(SecurityException.class, () -> mOverlayManager.commit(new OverlayManagerTransaction.Builder() .registerFabricatedOverlay(overlay) @@ -195,7 +199,7 @@ public class FabricatedOverlaysTest { { FabricatedOverlay overlay = new FabricatedOverlay.Builder(mContext.getPackageName(), longestName, mContext.getPackageName()) - .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1) + .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1) .build(); mOverlayManager.commit(new OverlayManagerTransaction.Builder() @@ -206,7 +210,7 @@ public class FabricatedOverlaysTest { { FabricatedOverlay overlay = new FabricatedOverlay.Builder(mContext.getPackageName(), longestName + "a", mContext.getPackageName()) - .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1) + .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1) .build(); assertThrows(SecurityException.class, () -> @@ -267,11 +271,11 @@ public class FabricatedOverlaysTest { public void testInvalidResourceValues() throws Exception { final FabricatedOverlay overlay = new FabricatedOverlay.Builder( "android", TEST_OVERLAY_NAME, mContext.getPackageName()) - .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1) + .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1) .setResourceValue("color/something", TypedValue.TYPE_INT_DEC, 1) .build(); - waitForResourceValue(0); + waitForIntResourceValue(0); assertThrows(SecurityException.class, () -> mOverlayManager.commit(new OverlayManagerTransaction.Builder() .registerFabricatedOverlay(overlay) @@ -285,10 +289,10 @@ public class FabricatedOverlaysTest { public void testTransactionFailRollback() throws Exception { final FabricatedOverlay overlay = new FabricatedOverlay.Builder( mContext.getPackageName(), TEST_OVERLAY_NAME, mContext.getPackageName()) - .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1) + .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1) .build(); - waitForResourceValue(0); + waitForIntResourceValue(0); assertThrows(SecurityException.class, () -> mOverlayManager.commit(new OverlayManagerTransaction.Builder() .registerFabricatedOverlay(overlay) @@ -299,16 +303,40 @@ public class FabricatedOverlaysTest { assertNull(mOverlayManager.getOverlayInfo(overlay.getIdentifier(), mUserHandle)); } - void waitForResourceValue(final int expectedValue) throws TimeoutException { + @Test + public void setResourceValue_forFloatType_succeeds() throws Exception { + final float overlaidValue = 5.7f; + final FabricatedOverlay overlay = new FabricatedOverlay.Builder( + mContext.getPackageName(), TEST_OVERLAY_NAME, mContext.getPackageName()).build(); + overlay.setResourceValue(TEST_FLOAT_RESOURCE, overlaidValue, null /* configuration */); + + waitForFloatResourceValue(0); + mOverlayManager.commit(new OverlayManagerTransaction.Builder() + .registerFabricatedOverlay(overlay) + .setEnabled(overlay.getIdentifier(), true, mUserId) + .build()); + + waitForFloatResourceValue(overlaidValue); + } + + private void waitForIntResourceValue(final int expectedValue) throws TimeoutException { + waitForResourceValue(expectedValue, TEST_INT_RESOURCE, id -> mResources.getInteger(id)); + } + + private void waitForFloatResourceValue(final float expectedValue) throws TimeoutException { + waitForResourceValue(expectedValue, TEST_FLOAT_RESOURCE, id -> mResources.getFloat(id)); + } + + private <T> void waitForResourceValue(final T expectedValue, final String resourceName, + @NonNull Function<Integer, T> resourceValueEmitter) throws TimeoutException { final long timeOutDuration = 10000; final long endTime = System.currentTimeMillis() + timeOutDuration; - final String resourceName = TEST_RESOURCE; final int resourceId = mResources.getIdentifier(resourceName, "", mContext.getPackageName()); - int resourceValue = 0; + T resourceValue = null; while (System.currentTimeMillis() < endTime) { - resourceValue = mResources.getInteger(resourceId); - if (resourceValue == expectedValue) { + resourceValue = resourceValueEmitter.apply(resourceId); + if (Objects.equals(expectedValue, resourceValue)) { return; } } diff --git a/data/etc/platform.xml b/data/etc/platform.xml index ca20aebf95d8..ea1ce48fe001 100644 --- a/data/etc/platform.xml +++ b/data/etc/platform.xml @@ -62,6 +62,12 @@ <permission name="android.permission.READ_LOGS" > <group gid="log" /> + <group gid="update_engine_log" /> + </permission> + + <permission name="android.permission.READ_UPDATE_ENGINE_LOGS" + featureFlag="com.android.update_engine.minor_changes_2025q4" > + <group gid="update_engine_log" /> </permission> <permission name="android.permission.ACCESS_MTP" > diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 1dd0465f691e..62e14d368a1c 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -424,7 +424,7 @@ applications that come with the platform <permission name="android.permission.REQUEST_COMPANION_PROFILE_APP_STREAMING" /> <permission name="android.permission.REQUEST_COMPANION_PROFILE_WATCH" /> <permission name="android.permission.REQUEST_COMPANION_PROFILE_NEARBY_DEVICE_STREAMING" /> - <permission name="android.permission.REQUEST_COMPANION_PROFILE_SENSOR_DEVICE_STREAMING" /> + <permission name="android.permission.REQUEST_COMPANION_PROFILE_VIRTUAL_DEVICE" /> <permission name="android.permission.REQUEST_COMPANION_PROFILE_COMPUTER" /> <permission name="android.permission.REQUEST_COMPANION_SELF_MANAGED" /> <permission name="android.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE" /> @@ -621,6 +621,8 @@ applications that come with the platform <permission name="android.permission.READ_COLOR_ZONES"/> <!-- Permission required for CTS test - CtsTextClassifierTestCases --> <permission name="android.permission.ACCESS_TEXT_CLASSIFIER_BY_TYPE"/> + <!-- Permission required for CTS test - CtsSecurityTestCases --> + <permission name="android.permission.MANAGE_DEVICE_POLICY_MTE"/> </privapp-permissions> <privapp-permissions package="com.android.soundpicker"> diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 19455a313a9d..e873dc7c94e8 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -179,6 +179,16 @@ flag { } flag { + name: "fix_missing_user_change_callbacks" + namespace: "multitasking" + description: "Fix a race condition that could make Shell miss a user change callback." + bug: "404251029" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_bubble_bar_on_phones" namespace: "multitasking" description: "Try out bubble bar on phones" diff --git a/libs/WindowManager/Shell/res/values-af/strings.xml b/libs/WindowManager/Shell/res/values-af/strings.xml index 05c4c56a5c81..f42fea6a0d17 100644 --- a/libs/WindowManager/Shell/res/values-af/strings.xml +++ b/libs/WindowManager/Shell/res/values-af/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Nuwe venster"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Bestuur vensters"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Verander aspekverhouding"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Maak toe"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Maak kieslys toe"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (werkskermvensters)"</string> diff --git a/libs/WindowManager/Shell/res/values-am/strings.xml b/libs/WindowManager/Shell/res/values-am/strings.xml index 450419dcc40d..c65bb3822ec2 100644 --- a/libs/WindowManager/Shell/res/values-am/strings.xml +++ b/libs/WindowManager/Shell/res/values-am/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"አዲስ መስኮት"</string> <string name="manage_windows_text" msgid="5567366688493093920">"መስኮቶችን አስተዳድር"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"ምጥጥነ ገፅታ ለውጥ"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"ዝጋ"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"ምናሌ ዝጋ"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (ዴስክቶፕ መስኮት)"</string> diff --git a/libs/WindowManager/Shell/res/values-ar/strings.xml b/libs/WindowManager/Shell/res/values-ar/strings.xml index 70a23b73b6f5..d06d99203245 100644 --- a/libs/WindowManager/Shell/res/values-ar/strings.xml +++ b/libs/WindowManager/Shell/res/values-ar/strings.xml @@ -100,8 +100,7 @@ <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"ألم يتم حل المشكلة؟\nانقر للعودة"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"أليس هناك مشاكل في الكاميرا؟ انقر للإغلاق."</string> <string name="windowing_app_handle_education_tooltip" msgid="2929643449849791854">"يمكن العثور على قائمة التطبيقات هنا"</string> - <!-- no translation found for windowing_desktop_mode_image_button_education_tooltip (7171915734817051666) --> - <skip /> + <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="7171915734817051666">"يمكنك الدخول إلى وضع عرض المحتوى في النافذة الحالية على سطح المكتب لفتح عدة تطبيقات معًا"</string> <string name="windowing_desktop_mode_exit_education_tooltip" msgid="5225660258192054132">"يمكنك الرجوع إلى وضع ملء الشاشة في أي وقت من قائمة التطبيقات"</string> <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"استخدام تطبيقات متعدّدة في وقت واحد"</string> <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"اسحب تطبيقًا آخر لاستخدام وضع تقسيم الشاشة."</string> @@ -122,8 +121,7 @@ <string name="handle_text" msgid="4419667835599523257">"مقبض التطبيق"</string> <string name="app_icon_text" msgid="2823268023931811747">"رمز التطبيق"</string> <string name="fullscreen_text" msgid="1162316685217676079">"ملء الشاشة"</string> - <!-- no translation found for desktop_text (9058641752519570266) --> - <skip /> + <string name="desktop_text" msgid="9058641752519570266">"عرض المحتوى في النافذة الحالية على سطح المكتب"</string> <string name="split_screen_text" msgid="1396336058129570886">"تقسيم الشاشة"</string> <string name="more_button_text" msgid="3655388105592893530">"المزيد"</string> <string name="float_button_text" msgid="9221657008391364581">"نافذة عائمة"</string> @@ -134,10 +132,11 @@ <string name="new_window_text" msgid="6318648868380652280">"نافذة جديدة"</string> <string name="manage_windows_text" msgid="5567366688493093920">"إدارة النوافذ"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"تغيير نسبة العرض إلى الارتفاع"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"إغلاق"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"إغلاق القائمة"</string> - <!-- no translation found for desktop_mode_app_header_chip_text (7617377295944971651) --> - <skip /> + <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (عرض المحتوى في النافذة الحالية على سطح المكتب)"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"تكبير الشاشة إلى أقصى حدّ"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="5673738963174074006">"تغيير الحجم"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"لا يمكن نقل التطبيق إلى هنا"</string> diff --git a/libs/WindowManager/Shell/res/values-as/strings.xml b/libs/WindowManager/Shell/res/values-as/strings.xml index b1826db57a2c..1d1a048675ac 100644 --- a/libs/WindowManager/Shell/res/values-as/strings.xml +++ b/libs/WindowManager/Shell/res/values-as/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"নতুন ৱিণ্ড’"</string> <string name="manage_windows_text" msgid="5567366688493093920">"ৱিণ্ড’ পৰিচালনা কৰক"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"আকাৰৰ অনুপাত সলনি কৰক"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"বন্ধ কৰক"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"মেনু বন্ধ কৰক"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (ডেস্কটপ ৱিণ্ড’ৱিং)"</string> diff --git a/libs/WindowManager/Shell/res/values-az/strings.xml b/libs/WindowManager/Shell/res/values-az/strings.xml index c5493b573d0f..2770ede1a85a 100644 --- a/libs/WindowManager/Shell/res/values-az/strings.xml +++ b/libs/WindowManager/Shell/res/values-az/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Yeni pəncərə"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Pəncərələri idarə edin"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Tərəflər nisbətini dəyişin"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Bağlayın"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Menyunu bağlayın"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (Masaüstü pəncərə rejimi)"</string> diff --git a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml index 307c47ab48eb..615b558a23a9 100644 --- a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml +++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Novi prozor"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Upravljajte prozorima"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Promeni razmeru"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Zatvorite"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zatvorite meni"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (prozorski prikaz za računare)"</string> diff --git a/libs/WindowManager/Shell/res/values-be/strings.xml b/libs/WindowManager/Shell/res/values-be/strings.xml index c53e37c67cfc..d83ed575a34f 100644 --- a/libs/WindowManager/Shell/res/values-be/strings.xml +++ b/libs/WindowManager/Shell/res/values-be/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Новае акно"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Кіраваць вокнамі"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Змяніць суадносіны бакоў"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Закрыць"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Закрыць меню"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (рэжым вокнаў працоўнага стала)"</string> diff --git a/libs/WindowManager/Shell/res/values-bg/strings.xml b/libs/WindowManager/Shell/res/values-bg/strings.xml index 29af2ed1c38b..9b91d3d540e9 100644 --- a/libs/WindowManager/Shell/res/values-bg/strings.xml +++ b/libs/WindowManager/Shell/res/values-bg/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Нов прозорец"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Управление на прозорците"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Промяна на съотношението"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Затваряне"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Затваряне на менюто"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (режим за настолни компютри)"</string> diff --git a/libs/WindowManager/Shell/res/values-bn/strings.xml b/libs/WindowManager/Shell/res/values-bn/strings.xml index d4488a220ac7..9fd156fe6dcd 100644 --- a/libs/WindowManager/Shell/res/values-bn/strings.xml +++ b/libs/WindowManager/Shell/res/values-bn/strings.xml @@ -100,8 +100,7 @@ <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"এখনও সমাধান হয়নি?\nরিভার্ট করার জন্য ট্যাপ করুন"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"ক্যামেরা সংক্রান্ত সমস্যা নেই? বাতিল করতে ট্যাপ করুন।"</string> <string name="windowing_app_handle_education_tooltip" msgid="2929643449849791854">"অ্যাপ মেনু এখানে খুঁজে পাওয়া যাবে"</string> - <!-- no translation found for windowing_desktop_mode_image_button_education_tooltip (7171915734817051666) --> - <skip /> + <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="7171915734817051666">"একসাথে একাধিক অ্যাপ খোলার জন্য ডেস্কটপ উইন্ডোইংয়ে এন্টার করুন"</string> <string name="windowing_desktop_mode_exit_education_tooltip" msgid="5225660258192054132">"অ্যাপ মেনু থেকে ফুল-স্ক্রিন মোডে যেকোনও সময়ে ফিরে আসুন"</string> <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"দেখুন ও আরও অনেক কিছু করুন"</string> <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"স্প্লিট স্ক্রিনের ক্ষেত্রে অন্য কোনও অ্যাপ টেনে আনুন"</string> @@ -122,8 +121,7 @@ <string name="handle_text" msgid="4419667835599523257">"অ্যাপের হ্যান্ডেল"</string> <string name="app_icon_text" msgid="2823268023931811747">"অ্যাপ আইকন"</string> <string name="fullscreen_text" msgid="1162316685217676079">"ফুলস্ক্রিন"</string> - <!-- no translation found for desktop_text (9058641752519570266) --> - <skip /> + <string name="desktop_text" msgid="9058641752519570266">"ডেস্কটপ উইন্ডোইং"</string> <string name="split_screen_text" msgid="1396336058129570886">"স্প্লিট স্ক্রিন"</string> <string name="more_button_text" msgid="3655388105592893530">"আরও"</string> <string name="float_button_text" msgid="9221657008391364581">"ফ্লোট"</string> @@ -134,10 +132,11 @@ <string name="new_window_text" msgid="6318648868380652280">"নতুন উইন্ডো"</string> <string name="manage_windows_text" msgid="5567366688493093920">"উইন্ডো ম্যানেজ করুন"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"অ্যাস্পেক্ট রেশিও পরিবর্তন করুন"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"বন্ধ করুন"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"\'মেনু\' বন্ধ করুন"</string> - <!-- no translation found for desktop_mode_app_header_chip_text (7617377295944971651) --> - <skip /> + <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (ডেস্কটপ উইন্ডোইং)"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"স্ক্রিন বড় করুন"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="5673738963174074006">"ছোট বড় করুন"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"অ্যাপটি এখানে সরানো যাবে না"</string> diff --git a/libs/WindowManager/Shell/res/values-bs/strings.xml b/libs/WindowManager/Shell/res/values-bs/strings.xml index 537afdcc6de4..4834ad86db67 100644 --- a/libs/WindowManager/Shell/res/values-bs/strings.xml +++ b/libs/WindowManager/Shell/res/values-bs/strings.xml @@ -100,7 +100,7 @@ <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Nije popravljeno?\nDodirnite da vratite"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nema problema s kamerom? Dodirnite da odbacite."</string> <string name="windowing_app_handle_education_tooltip" msgid="2929643449849791854">"Ovdje možete pronaći meni aplikacije"</string> - <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="7171915734817051666">"Otvorite prikaz u prozorima na računalu da biste otvorili više aplikacija zajedno"</string> + <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="7171915734817051666">"Ulazak u računarski prikaz prozora radi istovremenog otvaranja više aplikacija"</string> <string name="windowing_desktop_mode_exit_education_tooltip" msgid="5225660258192054132">"Povratak na prikaz preko cijelog ekrana bilo kada putem menija aplikacije"</string> <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Pogledajte i učinite više"</string> <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Prevucite još jednu aplikaciju za podijeljeni ekran"</string> @@ -121,7 +121,7 @@ <string name="handle_text" msgid="4419667835599523257">"Ručica aplikacije"</string> <string name="app_icon_text" msgid="2823268023931811747">"Ikona aplikacije"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Cijeli ekran"</string> - <string name="desktop_text" msgid="9058641752519570266">"Prikaz u prozorima na računalu"</string> + <string name="desktop_text" msgid="9058641752519570266">"Računarski prikaz prozora"</string> <string name="split_screen_text" msgid="1396336058129570886">"Podijeljeni ekran"</string> <string name="more_button_text" msgid="3655388105592893530">"Više"</string> <string name="float_button_text" msgid="9221657008391364581">"Lebdeći"</string> @@ -132,9 +132,11 @@ <string name="new_window_text" msgid="6318648868380652280">"Novi prozor"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Upravljanje prozorima"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Promjena formata slike"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Zatvaranje"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zatvaranje menija"</string> - <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (prikaz u prozorima na računalu)"</string> + <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (računarski prikaz prozora)"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimiziraj ekran"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="5673738963174074006">"Promijeni veličinu"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Ne možete premjestiti aplikaciju ovdje"</string> diff --git a/libs/WindowManager/Shell/res/values-ca/strings.xml b/libs/WindowManager/Shell/res/values-ca/strings.xml index 42b07ef3d049..662bd81fe460 100644 --- a/libs/WindowManager/Shell/res/values-ca/strings.xml +++ b/libs/WindowManager/Shell/res/values-ca/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Finestra nova"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Gestiona les finestres"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Canvia la relació d\'aspecte"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Tanca"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Tanca el menú"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (enfinestrament d\'escriptori)"</string> diff --git a/libs/WindowManager/Shell/res/values-cs/strings.xml b/libs/WindowManager/Shell/res/values-cs/strings.xml index 44548682cbbc..12c9e294e9c4 100644 --- a/libs/WindowManager/Shell/res/values-cs/strings.xml +++ b/libs/WindowManager/Shell/res/values-cs/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Nové okno"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Spravovat okna"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Změnit poměr stran"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Zavřít"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zavřít nabídku"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (okna na ploše)"</string> diff --git a/libs/WindowManager/Shell/res/values-da/strings.xml b/libs/WindowManager/Shell/res/values-da/strings.xml index 4d14f93d7b77..5df06ea2b9f7 100644 --- a/libs/WindowManager/Shell/res/values-da/strings.xml +++ b/libs/WindowManager/Shell/res/values-da/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Nyt vindue"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Administrer vinduer"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Skift billedformat"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Luk"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Luk menu"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (vinduer på skrivebordet)"</string> diff --git a/libs/WindowManager/Shell/res/values-de/strings.xml b/libs/WindowManager/Shell/res/values-de/strings.xml index 82bbfc4eff29..b3444e0ac2b8 100644 --- a/libs/WindowManager/Shell/res/values-de/strings.xml +++ b/libs/WindowManager/Shell/res/values-de/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Neues Fenster"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Fenster verwalten"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Seitenverhältnis ändern"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Schließen"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Menü schließen"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (Desktop-Freiform-Fenster)"</string> diff --git a/libs/WindowManager/Shell/res/values-el/strings.xml b/libs/WindowManager/Shell/res/values-el/strings.xml index a8696aff1f0c..c137513b4d7c 100644 --- a/libs/WindowManager/Shell/res/values-el/strings.xml +++ b/libs/WindowManager/Shell/res/values-el/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Νέο παράθυρο"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Διαχείριση παραθύρων"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Αλλαγή λόγου διαστάσεων"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Κλείσιμο"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Κλείσιμο μενού"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (Προσαρμογή σε παράθυρο στην επιφάνεια εργασίας)"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml index 61b68a2b2515..a3156bc551eb 100644 --- a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml @@ -132,9 +132,11 @@ <string name="new_window_text" msgid="6318648868380652280">"New window"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Manage windows"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Change aspect ratio"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Close"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Close menu"</string> - <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (desktop windowing)"</string> + <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (Desktop windowing)"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximise screen"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="5673738963174074006">"Resize"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"App can\'t be moved here"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml index 0802f83ab837..3e2de7cd3b56 100644 --- a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml @@ -132,6 +132,7 @@ <string name="new_window_text" msgid="6318648868380652280">"New Window"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Manage Windows"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Change aspect ratio"</string> + <string name="handle_menu_restart_text" msgid="3907767216238298098">"Optimize View"</string> <string name="close_text" msgid="4986518933445178928">"Close"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Close Menu"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (Desktop windowing)"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml index 61b68a2b2515..a3156bc551eb 100644 --- a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml @@ -132,9 +132,11 @@ <string name="new_window_text" msgid="6318648868380652280">"New window"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Manage windows"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Change aspect ratio"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Close"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Close menu"</string> - <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (desktop windowing)"</string> + <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (Desktop windowing)"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximise screen"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="5673738963174074006">"Resize"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"App can\'t be moved here"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml index 61b68a2b2515..a3156bc551eb 100644 --- a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml @@ -132,9 +132,11 @@ <string name="new_window_text" msgid="6318648868380652280">"New window"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Manage windows"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Change aspect ratio"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Close"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Close menu"</string> - <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (desktop windowing)"</string> + <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (Desktop windowing)"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximise screen"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="5673738963174074006">"Resize"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"App can\'t be moved here"</string> diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml index bb70f6e419a5..6027f7df4272 100644 --- a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml +++ b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml @@ -100,8 +100,7 @@ <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"¿No se resolvió?\nPresiona para revertir los cambios"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"¿No tienes problemas con la cámara? Presionar para descartar."</string> <string name="windowing_app_handle_education_tooltip" msgid="2929643449849791854">"El menú de la app se encuentra aquí"</string> - <!-- no translation found for windowing_desktop_mode_image_button_education_tooltip (7171915734817051666) --> - <skip /> + <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="7171915734817051666">"Entra a la renderización en ventanas de escritorio para abrir varias apps a la vez"</string> <string name="windowing_desktop_mode_exit_education_tooltip" msgid="5225660258192054132">"Regresa a pantalla completa en cualquier momento desde el menú de la app"</string> <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Aprovecha más"</string> <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Arrastra otra app para el modo de pantalla dividida"</string> @@ -122,8 +121,7 @@ <string name="handle_text" msgid="4419667835599523257">"Controlador de la app"</string> <string name="app_icon_text" msgid="2823268023931811747">"Ícono de la app"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Pantalla completa"</string> - <!-- no translation found for desktop_text (9058641752519570266) --> - <skip /> + <string name="desktop_text" msgid="9058641752519570266">"Renderización en ventanas de escritorio"</string> <string name="split_screen_text" msgid="1396336058129570886">"Pantalla dividida"</string> <string name="more_button_text" msgid="3655388105592893530">"Más"</string> <string name="float_button_text" msgid="9221657008391364581">"Flotante"</string> @@ -134,10 +132,11 @@ <string name="new_window_text" msgid="6318648868380652280">"Nueva ventana"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Administrar ventanas"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Cambiar relación de aspecto"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Cerrar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Cerrar menú"</string> - <!-- no translation found for desktop_mode_app_header_chip_text (7617377295944971651) --> - <skip /> + <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (renderización en ventanas de escritorio)"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizar pantalla"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="5673738963174074006">"Dividir pantalla"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"No se puede mover la app aquí"</string> diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml index a6595aa9292d..81c9e1b214c4 100644 --- a/libs/WindowManager/Shell/res/values-es/strings.xml +++ b/libs/WindowManager/Shell/res/values-es/strings.xml @@ -100,8 +100,7 @@ <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"¿No se ha solucionado?\nToca para revertir"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"¿No hay problemas con la cámara? Toca para cerrar."</string> <string name="windowing_app_handle_education_tooltip" msgid="2929643449849791854">"El menú de la aplicación se encuentra aquí"</string> - <!-- no translation found for windowing_desktop_mode_image_button_education_tooltip (7171915734817051666) --> - <skip /> + <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="7171915734817051666">"Entra en el escritorio basado en ventanas si quieres abrir varias aplicaciones a la vez"</string> <string name="windowing_desktop_mode_exit_education_tooltip" msgid="5225660258192054132">"Vuelve a la pantalla completa en cualquier momento desde el menú de aplicaciones"</string> <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Consulta más información y haz más"</string> <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Arrastra otra aplicación para activar la pantalla dividida"</string> @@ -122,8 +121,7 @@ <string name="handle_text" msgid="4419667835599523257">"Controlador de la aplicación"</string> <string name="app_icon_text" msgid="2823268023931811747">"Icono de la aplicación"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Pantalla completa"</string> - <!-- no translation found for desktop_text (9058641752519570266) --> - <skip /> + <string name="desktop_text" msgid="9058641752519570266">"Escritorio basado en ventanas"</string> <string name="split_screen_text" msgid="1396336058129570886">"Pantalla dividida"</string> <string name="more_button_text" msgid="3655388105592893530">"Más"</string> <string name="float_button_text" msgid="9221657008391364581">"Flotante"</string> @@ -134,10 +132,11 @@ <string name="new_window_text" msgid="6318648868380652280">"Ventana nueva"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Gestionar ventanas"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Cambiar relación de aspecto"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Cerrar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Cerrar menú"</string> - <!-- no translation found for desktop_mode_app_header_chip_text (7617377295944971651) --> - <skip /> + <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (escritorio basado en ventanas)"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizar pantalla"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="5673738963174074006">"Dividir pantalla"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"La aplicación no se puede mover aquí"</string> diff --git a/libs/WindowManager/Shell/res/values-et/strings.xml b/libs/WindowManager/Shell/res/values-et/strings.xml index c0e4eb36b541..f43348d1f2dc 100644 --- a/libs/WindowManager/Shell/res/values-et/strings.xml +++ b/libs/WindowManager/Shell/res/values-et/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Uus aken"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Akende haldamine"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Kuvasuhte muutmine"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Sule"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Sule menüü"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (töölaua aknad)"</string> diff --git a/libs/WindowManager/Shell/res/values-eu/strings.xml b/libs/WindowManager/Shell/res/values-eu/strings.xml index 446839a8eb06..4ac27c2c248e 100644 --- a/libs/WindowManager/Shell/res/values-eu/strings.xml +++ b/libs/WindowManager/Shell/res/values-eu/strings.xml @@ -100,8 +100,7 @@ <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Ez al da konpondu?\nLeheneratzeko, sakatu hau."</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Ez daukazu arazorik kamerarekin? Baztertzeko, sakatu hau."</string> <string name="windowing_app_handle_education_tooltip" msgid="2929643449849791854">"Aplikazioaren menua dago hemen"</string> - <!-- no translation found for windowing_desktop_mode_image_button_education_tooltip (7171915734817051666) --> - <skip /> + <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="7171915734817051666">"Sartu ordenagailuan leihoak erabiltzeko modua aplikazio bat baino gehiago batera irekitzeko"</string> <string name="windowing_desktop_mode_exit_education_tooltip" msgid="5225660258192054132">"Pantaila osoko modura itzultzeko, erabili aplikazioaren menua"</string> <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Ikusi eta egin gauza gehiago"</string> <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Pantaila zatitua ikusteko, arrastatu beste aplikazio bat"</string> @@ -122,8 +121,7 @@ <string name="handle_text" msgid="4419667835599523257">"Aplikazioaren kontrol-puntua"</string> <string name="app_icon_text" msgid="2823268023931811747">"Aplikazioaren ikonoa"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Pantaila osoa"</string> - <!-- no translation found for desktop_text (9058641752519570266) --> - <skip /> + <string name="desktop_text" msgid="9058641752519570266">"Ordenagailuan leihoak erabiltzeko modua"</string> <string name="split_screen_text" msgid="1396336058129570886">"Pantaila zatitzea"</string> <string name="more_button_text" msgid="3655388105592893530">"Gehiago"</string> <string name="float_button_text" msgid="9221657008391364581">"Leiho gainerakorra"</string> @@ -134,10 +132,11 @@ <string name="new_window_text" msgid="6318648868380652280">"Leiho berria"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Kudeatu leihoak"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Aldatu aspektu-erlazioa"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Itxi"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Itxi menua"</string> - <!-- no translation found for desktop_mode_app_header_chip_text (7617377295944971651) --> - <skip /> + <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (ordenagailuan leihoak erabiltzeko modua)"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Handitu pantaila"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="5673738963174074006">"Aldatu tamaina"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Aplikazioa ezin da hona ekarri"</string> diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml index 4879965d1ae7..93a9438ba045 100644 --- a/libs/WindowManager/Shell/res/values-fa/strings.xml +++ b/libs/WindowManager/Shell/res/values-fa/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"پنجره جدید"</string> <string name="manage_windows_text" msgid="5567366688493093920">"مدیریت کردن پنجرهها"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"تغییر نسبت ابعادی"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"بستن"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"بستن منو"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (پردازش پنجرهای رایانه)"</string> diff --git a/libs/WindowManager/Shell/res/values-fi/strings.xml b/libs/WindowManager/Shell/res/values-fi/strings.xml index 8fd6b1bacbbe..7c9e6e6c5229 100644 --- a/libs/WindowManager/Shell/res/values-fi/strings.xml +++ b/libs/WindowManager/Shell/res/values-fi/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Uusi ikkuna"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Hallinnoi ikkunoita"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Vaihda kuvasuhdetta"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Sulje"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Sulje valikko"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (työpöydän ikkunointi)"</string> diff --git a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml index b729ececfccd..c78b3130cdac 100644 --- a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Nouvelle fenêtre"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Gérer les fenêtres"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Modifier les proportions"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Fermer"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Fermer le menu"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (fenêtrage du bureau)"</string> diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml index ed87a1388304..708212fe77bc 100644 --- a/libs/WindowManager/Shell/res/values-fr/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Nouvelle fenêtre"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Gérer les fenêtres"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Modifier le format"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Fermer"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Fermer le menu"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (fenêtrage de bureau)"</string> diff --git a/libs/WindowManager/Shell/res/values-gl/strings.xml b/libs/WindowManager/Shell/res/values-gl/strings.xml index a2b871120464..a0a4df17edfe 100644 --- a/libs/WindowManager/Shell/res/values-gl/strings.xml +++ b/libs/WindowManager/Shell/res/values-gl/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Ventá nova"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Xestionar as ventás"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Cambiar a proporción"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Pechar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Pechar o menú"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (escritorio baseado en ventás)"</string> diff --git a/libs/WindowManager/Shell/res/values-gu/strings.xml b/libs/WindowManager/Shell/res/values-gu/strings.xml index ddef9e1fd07b..2a546fa2a23c 100644 --- a/libs/WindowManager/Shell/res/values-gu/strings.xml +++ b/libs/WindowManager/Shell/res/values-gu/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"નવી વિન્ડો"</string> <string name="manage_windows_text" msgid="5567366688493093920">"વિન્ડો મેનેજ કરો"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"સાપેક્ષ ગુણોત્તર બદલો"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"બંધ કરો"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"મેનૂ બંધ કરો"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (ડેસ્કટૉપ વિન્ડોઇંગ)"</string> diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml index bbe43a1727f8..c2eb2b207740 100644 --- a/libs/WindowManager/Shell/res/values-hi/strings.xml +++ b/libs/WindowManager/Shell/res/values-hi/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"नई विंडो"</string> <string name="manage_windows_text" msgid="5567366688493093920">"विंडो मैनेज करें"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"आसपेक्ट रेशियो (लंबाई-चौड़ाई का अनुपात) बदलें"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"बंद करें"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"मेन्यू बंद करें"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (डेस्कटॉप विंडोविंग)"</string> diff --git a/libs/WindowManager/Shell/res/values-hr/strings.xml b/libs/WindowManager/Shell/res/values-hr/strings.xml index 80ee56102cf2..41315fe466c2 100644 --- a/libs/WindowManager/Shell/res/values-hr/strings.xml +++ b/libs/WindowManager/Shell/res/values-hr/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Novi prozor"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Upravljanje prozorima"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Promijeni omjer slike"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Zatvorite"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zatvorite izbornik"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (prikaz u prozorima na računalu)"</string> diff --git a/libs/WindowManager/Shell/res/values-hu/strings.xml b/libs/WindowManager/Shell/res/values-hu/strings.xml index d24e4da8c982..0ebf2cb23946 100644 --- a/libs/WindowManager/Shell/res/values-hu/strings.xml +++ b/libs/WindowManager/Shell/res/values-hu/strings.xml @@ -100,7 +100,7 @@ <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Nem sikerült a hiba kijavítása?\nKoppintson a visszaállításhoz."</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nincsenek problémái kamerával? Koppintson az elvetéshez."</string> <string name="windowing_app_handle_education_tooltip" msgid="2929643449849791854">"Az alkalmazásmenü itt található"</string> - <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="7171915734817051666">"Asztali ablakkezelési módba lépve több alkalmazást nyithat meg egyidejűleg"</string> + <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="7171915734817051666">"Asztali ablakkezelési módba lépve egyidejűleg több alkalmazást is megnyithat"</string> <string name="windowing_desktop_mode_exit_education_tooltip" msgid="5225660258192054132">"Az alkalmazásmenüből bármikor visszatérhet a teljes képernyőre"</string> <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Több mindent láthat és tehet"</string> <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Húzzon ide egy másik alkalmazást az osztott képernyő használatához"</string> @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Új ablak"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Ablakok kezelése"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Méretarány módosítása"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Bezárás"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Menü bezárása"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (Asztali ablakkezelési mód)"</string> diff --git a/libs/WindowManager/Shell/res/values-hy/strings.xml b/libs/WindowManager/Shell/res/values-hy/strings.xml index 6bc3c37cadf4..a792f5bd8844 100644 --- a/libs/WindowManager/Shell/res/values-hy/strings.xml +++ b/libs/WindowManager/Shell/res/values-hy/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Նոր պատուհան"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Կառավարել պատուհանները"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Փոխել կողմերի հարաբերակցությունը"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Փակել"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Փակել ընտրացանկը"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (համակարգչային պատուհաններ)"</string> diff --git a/libs/WindowManager/Shell/res/values-in/strings.xml b/libs/WindowManager/Shell/res/values-in/strings.xml index c15c2ea6600b..720104511ce6 100644 --- a/libs/WindowManager/Shell/res/values-in/strings.xml +++ b/libs/WindowManager/Shell/res/values-in/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Jendela Baru"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Kelola Jendela"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Ubah rasio aspek"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Tutup"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Tutup Menu"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (Mode jendela desktop)"</string> diff --git a/libs/WindowManager/Shell/res/values-is/strings.xml b/libs/WindowManager/Shell/res/values-is/strings.xml index 7d98d3b01fc2..8488c308e725 100644 --- a/libs/WindowManager/Shell/res/values-is/strings.xml +++ b/libs/WindowManager/Shell/res/values-is/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Nýr gluggi"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Stjórna gluggum"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Breyta myndhlutfalli"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Loka"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Loka valmynd"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (gluggastilling í tölvu)"</string> diff --git a/libs/WindowManager/Shell/res/values-it/strings.xml b/libs/WindowManager/Shell/res/values-it/strings.xml index 72f805693146..4fc60ddc51f8 100644 --- a/libs/WindowManager/Shell/res/values-it/strings.xml +++ b/libs/WindowManager/Shell/res/values-it/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Nuova finestra"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Gestisci finestre"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Cambia proporzioni"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Chiudi"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Chiudi il menu"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (windowing del desktop)"</string> diff --git a/libs/WindowManager/Shell/res/values-iw/strings.xml b/libs/WindowManager/Shell/res/values-iw/strings.xml index c3e85230c319..be14b0081451 100644 --- a/libs/WindowManager/Shell/res/values-iw/strings.xml +++ b/libs/WindowManager/Shell/res/values-iw/strings.xml @@ -100,8 +100,7 @@ <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"הבעיה לא נפתרה?\nאפשר ללחוץ כדי לחזור לגרסה הקודמת"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"אין בעיות במצלמה? אפשר ללחוץ כדי לסגור."</string> <string name="windowing_app_handle_education_tooltip" msgid="2929643449849791854">"תפריט האפליקציה נמצא כאן"</string> - <!-- no translation found for windowing_desktop_mode_image_button_education_tooltip (7171915734817051666) --> - <skip /> + <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="7171915734817051666">"כדי לפתוח כמה אפליקציות יחד, צריך להיכנס למצב \"שינוי דינמי של חלונות במחשב\" (desktop windowing)"</string> <string name="windowing_desktop_mode_exit_education_tooltip" msgid="5225660258192054132">"אפשר לחזור למסך מלא בכל שלב מתפריט האפליקציה"</string> <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"רוצה לראות ולעשות יותר?"</string> <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"צריך לגרור אפליקציה אחרת כדי להשתמש במסך המפוצל"</string> @@ -122,8 +121,7 @@ <string name="handle_text" msgid="4419667835599523257">"נקודת אחיזה לאפליקציה"</string> <string name="app_icon_text" msgid="2823268023931811747">"סמל האפליקציה"</string> <string name="fullscreen_text" msgid="1162316685217676079">"מסך מלא"</string> - <!-- no translation found for desktop_text (9058641752519570266) --> - <skip /> + <string name="desktop_text" msgid="9058641752519570266">"שינוי דינמי של חלונות במחשב"</string> <string name="split_screen_text" msgid="1396336058129570886">"מסך מפוצל"</string> <string name="more_button_text" msgid="3655388105592893530">"עוד"</string> <string name="float_button_text" msgid="9221657008391364581">"בלונים"</string> @@ -134,10 +132,11 @@ <string name="new_window_text" msgid="6318648868380652280">"חלון חדש"</string> <string name="manage_windows_text" msgid="5567366688493093920">"ניהול החלונות"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"שינוי יחס הגובה-רוחב"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"סגירה"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"סגירת התפריט"</string> - <!-- no translation found for desktop_mode_app_header_chip_text (7617377295944971651) --> - <skip /> + <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (שינוי דינמי של חלונות במחשב)"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"הגדלת המסך"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="5673738963174074006">"שינוי הגודל"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"לא ניתן להעביר את האפליקציה לכאן"</string> diff --git a/libs/WindowManager/Shell/res/values-ja/strings.xml b/libs/WindowManager/Shell/res/values-ja/strings.xml index c95ec4ee25a3..761df5cc560c 100644 --- a/libs/WindowManager/Shell/res/values-ja/strings.xml +++ b/libs/WindowManager/Shell/res/values-ja/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"新しいウィンドウ"</string> <string name="manage_windows_text" msgid="5567366688493093920">"ウィンドウを管理する"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"アスペクト比を変更"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"閉じる"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"メニューを閉じる"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g>(デスクトップ ウィンドウ)"</string> diff --git a/libs/WindowManager/Shell/res/values-ka/strings.xml b/libs/WindowManager/Shell/res/values-ka/strings.xml index 0c7264cf9ede..d5c44fb8a963 100644 --- a/libs/WindowManager/Shell/res/values-ka/strings.xml +++ b/libs/WindowManager/Shell/res/values-ka/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"ახალი ფანჯარა"</string> <string name="manage_windows_text" msgid="5567366688493093920">"ფანჯრების მართვა"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"თანაფარდობის შეცვლა"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"დახურვა"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"მენიუს დახურვა"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (დესკტოპის ფანჯრის რეჟიმი)"</string> diff --git a/libs/WindowManager/Shell/res/values-kk/strings.xml b/libs/WindowManager/Shell/res/values-kk/strings.xml index c1085db12f08..3a9711e2337a 100644 --- a/libs/WindowManager/Shell/res/values-kk/strings.xml +++ b/libs/WindowManager/Shell/res/values-kk/strings.xml @@ -100,7 +100,7 @@ <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Жөнделмеді ме?\nҚайтару үшін түртіңіз."</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Камерада қателер шықпады ма? Жабу үшін түртіңіз."</string> <string name="windowing_app_handle_education_tooltip" msgid="2929643449849791854">"Қолданба мәзірін осы жерден табуға болады."</string> - <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="7171915734817051666">"Бірнеше қолданбаны бірге ашу үшін жұмыс үстелі көрінісіне кіріңіз."</string> + <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="7171915734817051666">"Бірнеше қолданбаны бірге ашу үшін компьютерлік терезелер режиміне кіріңіз."</string> <string name="windowing_desktop_mode_exit_education_tooltip" msgid="5225660258192054132">"Қолданба мәзірінен кез келген уақытта толық экранға оралыңыз."</string> <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Қосымша ақпаратты қарап, әрекеттер жасау"</string> <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Экранды бөлу үшін басқа қолданбаға өтіңіз."</string> @@ -121,7 +121,7 @@ <string name="handle_text" msgid="4419667835599523257">"Қолданба идентификаторы"</string> <string name="app_icon_text" msgid="2823268023931811747">"Қолданба белгішесі"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Толық экран"</string> - <string name="desktop_text" msgid="9058641752519570266">"Жұмыс үстелі көрінісі"</string> + <string name="desktop_text" msgid="9058641752519570266">"Компьютерлік терезелер режимі"</string> <string name="split_screen_text" msgid="1396336058129570886">"Экранды бөлу"</string> <string name="more_button_text" msgid="3655388105592893530">"Қосымша"</string> <string name="float_button_text" msgid="9221657008391364581">"Қалқыма"</string> @@ -132,9 +132,11 @@ <string name="new_window_text" msgid="6318648868380652280">"Жаңа терезе"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Терезелерді басқару"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Арақатынасты өзгерту"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Жабу"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Мәзірді жабу"</string> - <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (жұмыс үстелі көрінісі)"</string> + <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (компьютерлік терезелер режимі)"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Экранды ұлғайту"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="5673738963174074006">"Өлшемін өзгерту"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Қолданба бұл жерге қойылмайды."</string> diff --git a/libs/WindowManager/Shell/res/values-km/strings.xml b/libs/WindowManager/Shell/res/values-km/strings.xml index afbd9e0c9422..c4b5a9b2009f 100644 --- a/libs/WindowManager/Shell/res/values-km/strings.xml +++ b/libs/WindowManager/Shell/res/values-km/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"វិនដូថ្មី"</string> <string name="manage_windows_text" msgid="5567366688493093920">"គ្រប់គ្រងវិនដូ"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"ប្ដូរសមាមាត្រ"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"បិទ"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"បិទម៉ឺនុយ"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (មុខងារវិនដូកុំព្យូទ័រ)"</string> diff --git a/libs/WindowManager/Shell/res/values-kn/strings.xml b/libs/WindowManager/Shell/res/values-kn/strings.xml index 34076e8ecebe..9d3ec2a347af 100644 --- a/libs/WindowManager/Shell/res/values-kn/strings.xml +++ b/libs/WindowManager/Shell/res/values-kn/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"ಹೊಸ ವಿಂಡೋ"</string> <string name="manage_windows_text" msgid="5567366688493093920">"ವಿಂಡೋಗಳನ್ನು ನಿರ್ವಹಿಸಿ"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"ದೃಶ್ಯಾನುಪಾತವನ್ನು ಬದಲಾಯಿಸಿ"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"ಮುಚ್ಚಿ"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"ಮೆನು ಮುಚ್ಚಿ"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (ಡೆಸ್ಕ್ಟಾಪ್ ವಿಂಡೋಯಿಂಗ್)"</string> diff --git a/libs/WindowManager/Shell/res/values-ko/strings.xml b/libs/WindowManager/Shell/res/values-ko/strings.xml index a0fa7e480663..5206b83ef17a 100644 --- a/libs/WindowManager/Shell/res/values-ko/strings.xml +++ b/libs/WindowManager/Shell/res/values-ko/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"새 창"</string> <string name="manage_windows_text" msgid="5567366688493093920">"창 관리"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"가로세로 비율 변경"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"닫기"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"메뉴 닫기"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g>(데스크톱 윈도윙)"</string> diff --git a/libs/WindowManager/Shell/res/values-ky/strings.xml b/libs/WindowManager/Shell/res/values-ky/strings.xml index 629070cbe810..810a63841cfb 100644 --- a/libs/WindowManager/Shell/res/values-ky/strings.xml +++ b/libs/WindowManager/Shell/res/values-ky/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Жаңы терезе"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Терезелерди тескөө"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Тараптардын катнашын өзгөртүү"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Жабуу"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Менюну жабуу"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (Иш тактанын терезелери)"</string> diff --git a/libs/WindowManager/Shell/res/values-lo/strings.xml b/libs/WindowManager/Shell/res/values-lo/strings.xml index f2d0e6bd7af4..7a4fb61bdf0d 100644 --- a/libs/WindowManager/Shell/res/values-lo/strings.xml +++ b/libs/WindowManager/Shell/res/values-lo/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"ໜ້າຈໍໃໝ່"</string> <string name="manage_windows_text" msgid="5567366688493093920">"ຈັດການໜ້າຈໍ"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"ປ່ຽນອັດຕາສ່ວນຮູບ"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"ປິດ"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"ປິດເມນູ"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (ໜ້າຈໍເດັສທັອບ)"</string> diff --git a/libs/WindowManager/Shell/res/values-lt/strings.xml b/libs/WindowManager/Shell/res/values-lt/strings.xml index ed4b14cd94dd..75619eaefd17 100644 --- a/libs/WindowManager/Shell/res/values-lt/strings.xml +++ b/libs/WindowManager/Shell/res/values-lt/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Naujas langas"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Tvarkyti langus"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Keisti kraštinių santykį"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Uždaryti"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Uždaryti meniu"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"„<xliff:g id="APP_NAME">%1$s</xliff:g>“ (versijos staliniams kompiuteriams rodinys)"</string> diff --git a/libs/WindowManager/Shell/res/values-lv/strings.xml b/libs/WindowManager/Shell/res/values-lv/strings.xml index c24b43a686c3..5eea17c60053 100644 --- a/libs/WindowManager/Shell/res/values-lv/strings.xml +++ b/libs/WindowManager/Shell/res/values-lv/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Jauns logs"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Pārvaldīt logus"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Mainīt malu attiecību"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Aizvērt"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Aizvērt izvēlni"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (darbvirsmas logošana)"</string> diff --git a/libs/WindowManager/Shell/res/values-mk/strings.xml b/libs/WindowManager/Shell/res/values-mk/strings.xml index 06ed3232514b..4653aa2ca7c5 100644 --- a/libs/WindowManager/Shell/res/values-mk/strings.xml +++ b/libs/WindowManager/Shell/res/values-mk/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Нов прозорец"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Управувајте со прозорците"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Промени го соодносот"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Затворете"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Затворете го менито"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (режим со прозорци на работната површина)"</string> diff --git a/libs/WindowManager/Shell/res/values-ml/strings.xml b/libs/WindowManager/Shell/res/values-ml/strings.xml index dd4ec1bbf1cd..50c2f6e1a8b2 100644 --- a/libs/WindowManager/Shell/res/values-ml/strings.xml +++ b/libs/WindowManager/Shell/res/values-ml/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"പുതിയ വിന്ഡോ"</string> <string name="manage_windows_text" msgid="5567366688493093920">"വിൻഡോകൾ മാനേജ് ചെയ്യുക"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"വീക്ഷണ അനുപാതം മാറ്റുക"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"അടയ്ക്കുക"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"മെനു അടയ്ക്കുക"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (ഡെസ്ക്ടോപ്പ് വിൻഡോയിംഗ്)"</string> diff --git a/libs/WindowManager/Shell/res/values-mn/strings.xml b/libs/WindowManager/Shell/res/values-mn/strings.xml index 8683827ae004..80e83a5a3901 100644 --- a/libs/WindowManager/Shell/res/values-mn/strings.xml +++ b/libs/WindowManager/Shell/res/values-mn/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Шинэ цонх"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Цонхнуудыг удирдах"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Аспектын харьцааг өөрчлөх"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Хаах"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Цэсийг хаах"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (Дэлгэцийн цонх үүсгэх онцлог)"</string> diff --git a/libs/WindowManager/Shell/res/values-mr/strings.xml b/libs/WindowManager/Shell/res/values-mr/strings.xml index b7b3fb673d96..deb0bafa2058 100644 --- a/libs/WindowManager/Shell/res/values-mr/strings.xml +++ b/libs/WindowManager/Shell/res/values-mr/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"नवीन विंडो"</string> <string name="manage_windows_text" msgid="5567366688493093920">"विंडो व्यवस्थापित करा"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"आस्पेक्ट रेशो बदला"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"बंद करा"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"मेनू बंद करा"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (डेस्कटॉप विंडोइंग)"</string> diff --git a/libs/WindowManager/Shell/res/values-ms/strings.xml b/libs/WindowManager/Shell/res/values-ms/strings.xml index 0896b5ee88cb..1f56033e8c97 100644 --- a/libs/WindowManager/Shell/res/values-ms/strings.xml +++ b/libs/WindowManager/Shell/res/values-ms/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Tetingkap Baharu"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Urus Tetingkap"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Tukar nisbah bidang"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Tutup"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Tutup Menu"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (Tetingkap desktop)"</string> diff --git a/libs/WindowManager/Shell/res/values-my/strings.xml b/libs/WindowManager/Shell/res/values-my/strings.xml index 0f336e828f47..061ad0405d89 100644 --- a/libs/WindowManager/Shell/res/values-my/strings.xml +++ b/libs/WindowManager/Shell/res/values-my/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"ဝင်းဒိုးအသစ်"</string> <string name="manage_windows_text" msgid="5567366688493093920">"ဝင်းဒိုးများ စီမံရန်"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"အချိုးအစား ပြောင်းရန်"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"ပိတ်ရန်"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"မီနူး ပိတ်ရန်"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (ဒက်စ်တော့ဝင်းဒိုးမုဒ်)"</string> diff --git a/libs/WindowManager/Shell/res/values-nb/strings.xml b/libs/WindowManager/Shell/res/values-nb/strings.xml index 3207614c013f..232fb2b04f92 100644 --- a/libs/WindowManager/Shell/res/values-nb/strings.xml +++ b/libs/WindowManager/Shell/res/values-nb/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Nytt vindu"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Administrer vinduene"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Endre høyde/bredde-forholdet"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Lukk"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Lukk menyen"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (datamaskin-vindusvisning)"</string> diff --git a/libs/WindowManager/Shell/res/values-ne/strings.xml b/libs/WindowManager/Shell/res/values-ne/strings.xml index 2c7be991dd67..2d6ab7d816ab 100644 --- a/libs/WindowManager/Shell/res/values-ne/strings.xml +++ b/libs/WindowManager/Shell/res/values-ne/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"नयाँ विन्डो"</string> <string name="manage_windows_text" msgid="5567366688493093920">"विन्डोहरू व्यवस्थापन गर्नुहोस्"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"एस्पेक्ट रेसियो परिवर्तन गर्नुहोस्"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"बन्द गर्नुहोस्"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"मेनु बन्द गर्नुहोस्"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (डेस्कटप विन्डोइङ)"</string> diff --git a/libs/WindowManager/Shell/res/values-nl/strings.xml b/libs/WindowManager/Shell/res/values-nl/strings.xml index 099f875e0eb9..23f251f8ef60 100644 --- a/libs/WindowManager/Shell/res/values-nl/strings.xml +++ b/libs/WindowManager/Shell/res/values-nl/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Nieuw venster"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Vensters beheren"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Beeldverhouding wijzigen"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Sluiten"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Menu sluiten"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (desktopvensterfunctie)"</string> diff --git a/libs/WindowManager/Shell/res/values-or/strings.xml b/libs/WindowManager/Shell/res/values-or/strings.xml index 19cc8ced6517..8b7f0c758ac7 100644 --- a/libs/WindowManager/Shell/res/values-or/strings.xml +++ b/libs/WindowManager/Shell/res/values-or/strings.xml @@ -100,8 +100,7 @@ <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"ଏହାର ସମାଧାନ ହୋଇନାହିଁ?\nଫେରିଯିବା ପାଇଁ ଟାପ କରନ୍ତୁ"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"କ୍ୟାମେରାରେ କିଛି ସମସ୍ୟା ନାହିଁ? ଖାରଜ କରିବାକୁ ଟାପ କରନ୍ତୁ।"</string> <string name="windowing_app_handle_education_tooltip" msgid="2929643449849791854">"ଆପ ମେନୁ ଏଠାରେ ମିଳିପାରିବ"</string> - <!-- no translation found for windowing_desktop_mode_image_button_education_tooltip (7171915734817051666) --> - <skip /> + <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="7171915734817051666">"ଏକାଠି ଏକାଧିକ ଆପ୍ସ ଖୋଲିବାକୁ ଡେସ୍କଟପ ୱିଣ୍ଡୋଇଂରେ ଏଣ୍ଟର କରନ୍ତୁ"</string> <string name="windowing_desktop_mode_exit_education_tooltip" msgid="5225660258192054132">"ଆପ ମେନୁରୁ ଯେ କୌଣସି ସମୟରେ ପୂର୍ଣ୍ଣ ସ୍କ୍ରିନକୁ ଫେରନ୍ତୁ"</string> <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"ଦେଖନ୍ତୁ ଏବଂ ଆହୁରି ଅନେକ କିଛି କରନ୍ତୁ"</string> <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ପାଇଁ ଅନ୍ୟ ଏକ ଆପକୁ ଡ୍ରାଗ କରନ୍ତୁ"</string> @@ -122,8 +121,7 @@ <string name="handle_text" msgid="4419667835599523257">"ଆପର ହେଣ୍ଡେଲ"</string> <string name="app_icon_text" msgid="2823268023931811747">"ଆପ ଆଇକନ"</string> <string name="fullscreen_text" msgid="1162316685217676079">"ପୂର୍ଣ୍ଣସ୍କ୍ରିନ"</string> - <!-- no translation found for desktop_text (9058641752519570266) --> - <skip /> + <string name="desktop_text" msgid="9058641752519570266">"ଡେସ୍କଟପ ୱିଣ୍ଡୋଇଂ"</string> <string name="split_screen_text" msgid="1396336058129570886">"ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ"</string> <string name="more_button_text" msgid="3655388105592893530">"ଅଧିକ"</string> <string name="float_button_text" msgid="9221657008391364581">"ଫ୍ଲୋଟ"</string> @@ -134,10 +132,11 @@ <string name="new_window_text" msgid="6318648868380652280">"ନୂଆ ୱିଣ୍ଡୋ"</string> <string name="manage_windows_text" msgid="5567366688493093920">"ୱିଣ୍ଡୋଗୁଡ଼ିକୁ ପରିଚାଳନା କରନ୍ତୁ"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"ଚଉଡ଼ା ଓ ଉଚ୍ଚତାର ଅନୁପାତ ପରିବର୍ତ୍ତନ କରନ୍ତୁ"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"ବନ୍ଦ କରନ୍ତୁ"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"ମେନୁ ବନ୍ଦ କରନ୍ତୁ"</string> - <!-- no translation found for desktop_mode_app_header_chip_text (7617377295944971651) --> - <skip /> + <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (ଡେସ୍କଟପ ୱିଣ୍ଡୋଇଂ)"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ସ୍କ୍ରିନକୁ ବଡ଼ କରନ୍ତୁ"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="5673738963174074006">"ରିସାଇଜ କରନ୍ତୁ"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"ଆପକୁ ଏଠାକୁ ମୁଭ କରାଯାଇପାରିବ ନାହିଁ"</string> diff --git a/libs/WindowManager/Shell/res/values-pa/strings.xml b/libs/WindowManager/Shell/res/values-pa/strings.xml index 50e3cd6a3022..e074a073de00 100644 --- a/libs/WindowManager/Shell/res/values-pa/strings.xml +++ b/libs/WindowManager/Shell/res/values-pa/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"ਨਵੀਂ ਵਿੰਡੋ"</string> <string name="manage_windows_text" msgid="5567366688493093920">"ਵਿੰਡੋਆਂ ਦਾ ਪ੍ਰਬੰਧਨ ਕਰੋ"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"ਆਕਾਰ ਅਨੁਪਾਤ ਬਦਲੋ"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"ਬੰਦ ਕਰੋ"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"ਮੀਨੂ ਬੰਦ ਕਰੋ"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (ਡੈਸਕਟਾਪ ਵਿੰਡੋ)"</string> diff --git a/libs/WindowManager/Shell/res/values-pl/strings.xml b/libs/WindowManager/Shell/res/values-pl/strings.xml index 58a691f83cca..861d94723317 100644 --- a/libs/WindowManager/Shell/res/values-pl/strings.xml +++ b/libs/WindowManager/Shell/res/values-pl/strings.xml @@ -100,8 +100,7 @@ <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Naprawa się nie udała?\nKliknij, aby cofnąć"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Brak problemów z aparatem? Kliknij, aby zamknąć"</string> <string name="windowing_app_handle_education_tooltip" msgid="2929643449849791854">"Tu znajdziesz menu aplikacji"</string> - <!-- no translation found for windowing_desktop_mode_image_button_education_tooltip (7171915734817051666) --> - <skip /> + <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="7171915734817051666">"Aby otworzyć kilka aplikacji jednocześnie, przejdź do trybu okien na pulpicie"</string> <string name="windowing_desktop_mode_exit_education_tooltip" msgid="5225660258192054132">"Z menu aplikacji w każdej chwili możesz wrócić do pełnego ekranu"</string> <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Zobacz i zrób więcej"</string> <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Aby podzielić ekran, przeciągnij drugą aplikację"</string> @@ -122,8 +121,7 @@ <string name="handle_text" msgid="4419667835599523257">"Uchwyt aplikacji"</string> <string name="app_icon_text" msgid="2823268023931811747">"Ikona aplikacji"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Pełny ekran"</string> - <!-- no translation found for desktop_text (9058641752519570266) --> - <skip /> + <string name="desktop_text" msgid="9058641752519570266">"Tryb okien na pulpicie"</string> <string name="split_screen_text" msgid="1396336058129570886">"Podzielony ekran"</string> <string name="more_button_text" msgid="3655388105592893530">"Więcej"</string> <string name="float_button_text" msgid="9221657008391364581">"Pływające"</string> @@ -134,10 +132,11 @@ <string name="new_window_text" msgid="6318648868380652280">"Nowe okno"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Zarządzaj oknami"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Zmień format obrazu"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Zamknij"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zamknij menu"</string> - <!-- no translation found for desktop_mode_app_header_chip_text (7617377295944971651) --> - <skip /> + <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (tryb okien na pulpicie)"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksymalizuj ekran"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="5673738963174074006">"Zmień rozmiar"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Nie można przenieść aplikacji tutaj"</string> diff --git a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml index 2d9bc2bc5b80..53db421ac0f0 100644 --- a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml @@ -100,8 +100,7 @@ <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"O problema não foi corrigido?\nToque para reverter"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Não tem problemas com a câmera? Toque para dispensar."</string> <string name="windowing_app_handle_education_tooltip" msgid="2929643449849791854">"O menu do app está aqui"</string> - <!-- no translation found for windowing_desktop_mode_image_button_education_tooltip (7171915734817051666) --> - <skip /> + <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="7171915734817051666">"Abra vários apps ao mesmo tempo usando o modo janela para computador"</string> <string name="windowing_desktop_mode_exit_education_tooltip" msgid="5225660258192054132">"Volte para a tela cheia a qualquer momento no menu do app"</string> <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Veja e faça mais"</string> <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Arraste outro app para dividir a tela"</string> @@ -122,8 +121,7 @@ <string name="handle_text" msgid="4419667835599523257">"Identificador do app"</string> <string name="app_icon_text" msgid="2823268023931811747">"Ícone do app"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Tela cheia"</string> - <!-- no translation found for desktop_text (9058641752519570266) --> - <skip /> + <string name="desktop_text" msgid="9058641752519570266">"Modo janela para computador"</string> <string name="split_screen_text" msgid="1396336058129570886">"Tela dividida"</string> <string name="more_button_text" msgid="3655388105592893530">"Mais"</string> <string name="float_button_text" msgid="9221657008391364581">"Ponto flutuante"</string> @@ -134,10 +132,11 @@ <string name="new_window_text" msgid="6318648868380652280">"Nova janela"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Gerenciar janelas"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Mudar a proporção"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Fechar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Fechar menu"</string> - <!-- no translation found for desktop_mode_app_header_chip_text (7617377295944971651) --> - <skip /> + <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (modo janela para computador)"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ampliar tela"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="5673738963174074006">"Redimensionar"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Não é possível mover o app para cá"</string> diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml index d5535183551e..5a4da3c07389 100644 --- a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Nova janela"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Gerir janelas"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Alterar formato"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Fechar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Fechar menu"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (janelas de computador)"</string> diff --git a/libs/WindowManager/Shell/res/values-pt/strings.xml b/libs/WindowManager/Shell/res/values-pt/strings.xml index 2d9bc2bc5b80..53db421ac0f0 100644 --- a/libs/WindowManager/Shell/res/values-pt/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt/strings.xml @@ -100,8 +100,7 @@ <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"O problema não foi corrigido?\nToque para reverter"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Não tem problemas com a câmera? Toque para dispensar."</string> <string name="windowing_app_handle_education_tooltip" msgid="2929643449849791854">"O menu do app está aqui"</string> - <!-- no translation found for windowing_desktop_mode_image_button_education_tooltip (7171915734817051666) --> - <skip /> + <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="7171915734817051666">"Abra vários apps ao mesmo tempo usando o modo janela para computador"</string> <string name="windowing_desktop_mode_exit_education_tooltip" msgid="5225660258192054132">"Volte para a tela cheia a qualquer momento no menu do app"</string> <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Veja e faça mais"</string> <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Arraste outro app para dividir a tela"</string> @@ -122,8 +121,7 @@ <string name="handle_text" msgid="4419667835599523257">"Identificador do app"</string> <string name="app_icon_text" msgid="2823268023931811747">"Ícone do app"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Tela cheia"</string> - <!-- no translation found for desktop_text (9058641752519570266) --> - <skip /> + <string name="desktop_text" msgid="9058641752519570266">"Modo janela para computador"</string> <string name="split_screen_text" msgid="1396336058129570886">"Tela dividida"</string> <string name="more_button_text" msgid="3655388105592893530">"Mais"</string> <string name="float_button_text" msgid="9221657008391364581">"Ponto flutuante"</string> @@ -134,10 +132,11 @@ <string name="new_window_text" msgid="6318648868380652280">"Nova janela"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Gerenciar janelas"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Mudar a proporção"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Fechar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Fechar menu"</string> - <!-- no translation found for desktop_mode_app_header_chip_text (7617377295944971651) --> - <skip /> + <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (modo janela para computador)"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ampliar tela"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="5673738963174074006">"Redimensionar"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Não é possível mover o app para cá"</string> diff --git a/libs/WindowManager/Shell/res/values-ro/strings.xml b/libs/WindowManager/Shell/res/values-ro/strings.xml index 286443c69d72..159040082a40 100644 --- a/libs/WindowManager/Shell/res/values-ro/strings.xml +++ b/libs/WindowManager/Shell/res/values-ro/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Fereastră nouă"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Gestionează ferestrele"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Schimbă raportul de dimensiuni"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Închide"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Închide meniul"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (ferestre pe desktop)"</string> diff --git a/libs/WindowManager/Shell/res/values-ru/strings.xml b/libs/WindowManager/Shell/res/values-ru/strings.xml index 472a239ca344..bd7fa6e72b35 100644 --- a/libs/WindowManager/Shell/res/values-ru/strings.xml +++ b/libs/WindowManager/Shell/res/values-ru/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Новое окно"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Управление окнами"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Изменить соотношение сторон"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Закрыть"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Закрыть меню"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (режим компьютерных окон)"</string> diff --git a/libs/WindowManager/Shell/res/values-si/strings.xml b/libs/WindowManager/Shell/res/values-si/strings.xml index 6c91955a425e..7a72f99034ac 100644 --- a/libs/WindowManager/Shell/res/values-si/strings.xml +++ b/libs/WindowManager/Shell/res/values-si/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"නව කවුළුව"</string> <string name="manage_windows_text" msgid="5567366688493093920">"කවුළු කළමනාකරණය කරන්න"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"දර්ශන අනුපාතය වෙනස් කරන්න"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"වසන්න"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"මෙනුව වසන්න"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (ඩෙස්ක්ටොප් කවුළුකරණය)"</string> diff --git a/libs/WindowManager/Shell/res/values-sk/strings.xml b/libs/WindowManager/Shell/res/values-sk/strings.xml index 08404d016789..f0f3e50dccd4 100644 --- a/libs/WindowManager/Shell/res/values-sk/strings.xml +++ b/libs/WindowManager/Shell/res/values-sk/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Nové okno"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Spravovať okná"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Zmeniť pomer strán"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Zavrieť"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zavrieť ponuku"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (windowing na pracovnej ploche)"</string> diff --git a/libs/WindowManager/Shell/res/values-sl/strings.xml b/libs/WindowManager/Shell/res/values-sl/strings.xml index deed0e0fe27a..d86cf6baca67 100644 --- a/libs/WindowManager/Shell/res/values-sl/strings.xml +++ b/libs/WindowManager/Shell/res/values-sl/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Novo okno"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Upravljanje oken"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Sprememba razmerja stranic"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Zapri"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zapri meni"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (namizni način prikaza več oken hkrati)"</string> diff --git a/libs/WindowManager/Shell/res/values-sq/strings.xml b/libs/WindowManager/Shell/res/values-sq/strings.xml index c6b8c6da56c2..ca4fe6fd3615 100644 --- a/libs/WindowManager/Shell/res/values-sq/strings.xml +++ b/libs/WindowManager/Shell/res/values-sq/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Dritare e re"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Menaxho dritaret"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Ndrysho raportin e pamjes"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Mbyll"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Mbyll menynë"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (ndërfaqja me dritare në desktop)"</string> diff --git a/libs/WindowManager/Shell/res/values-sr/strings.xml b/libs/WindowManager/Shell/res/values-sr/strings.xml index 3fe25f9d3d9e..58d9398d8d6c 100644 --- a/libs/WindowManager/Shell/res/values-sr/strings.xml +++ b/libs/WindowManager/Shell/res/values-sr/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Нови прозор"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Управљајте прозорима"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Промени размеру"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Затворите"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Затворите мени"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (прозорски приказ за рачунаре)"</string> diff --git a/libs/WindowManager/Shell/res/values-sv/strings.xml b/libs/WindowManager/Shell/res/values-sv/strings.xml index 404bdaf6294d..5231a673d1f6 100644 --- a/libs/WindowManager/Shell/res/values-sv/strings.xml +++ b/libs/WindowManager/Shell/res/values-sv/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Nytt fönster"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Hantera fönster"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Ändra bildformat"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Stäng"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Stäng menyn"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (fönsterstapling)"</string> diff --git a/libs/WindowManager/Shell/res/values-sw/strings.xml b/libs/WindowManager/Shell/res/values-sw/strings.xml index 3bd7988874b8..9c3c10734a00 100644 --- a/libs/WindowManager/Shell/res/values-sw/strings.xml +++ b/libs/WindowManager/Shell/res/values-sw/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Dirisha Jipya"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Dhibiti Windows"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Badilisha uwiano"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Funga"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Funga Menyu"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (Kupanga madirisha ya kompyuta ya mezani)"</string> diff --git a/libs/WindowManager/Shell/res/values-ta/strings.xml b/libs/WindowManager/Shell/res/values-ta/strings.xml index 12780dfd1747..811fce3b2cb1 100644 --- a/libs/WindowManager/Shell/res/values-ta/strings.xml +++ b/libs/WindowManager/Shell/res/values-ta/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"புதிய சாளரம்"</string> <string name="manage_windows_text" msgid="5567366688493093920">"சாளரங்களை நிர்வகிக்கலாம்"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"தோற்ற விகிதத்தை மாற்று"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"மூடும்"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"மெனுவை மூடும்"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (டெஸ்க்டாப் சாளரமாக்குதல்)"</string> diff --git a/libs/WindowManager/Shell/res/values-te/strings.xml b/libs/WindowManager/Shell/res/values-te/strings.xml index 2044fe70c7c3..7a809f4d4684 100644 --- a/libs/WindowManager/Shell/res/values-te/strings.xml +++ b/libs/WindowManager/Shell/res/values-te/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"కొత్త విండో"</string> <string name="manage_windows_text" msgid="5567366688493093920">"విండోలను మేనేజ్ చేయండి"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"ఆకార నిష్పత్తిని మార్చండి"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"మూసివేయండి"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"మెనూను మూసివేయండి"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (డెస్క్టాప్ వీక్షణ)"</string> diff --git a/libs/WindowManager/Shell/res/values-th/strings.xml b/libs/WindowManager/Shell/res/values-th/strings.xml index 60632ada32bb..66996356587e 100644 --- a/libs/WindowManager/Shell/res/values-th/strings.xml +++ b/libs/WindowManager/Shell/res/values-th/strings.xml @@ -100,8 +100,7 @@ <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"หากไม่ได้แก้ไข\nแตะเพื่อเปลี่ยนกลับ"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"หากไม่พบปัญหากับกล้อง แตะเพื่อปิด"</string> <string name="windowing_app_handle_education_tooltip" msgid="2929643449849791854">"ดูเมนูแอปที่นี่ได้"</string> - <!-- no translation found for windowing_desktop_mode_image_button_education_tooltip (7171915734817051666) --> - <skip /> + <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="7171915734817051666">"เข้าสู่หน้าต่างเดสก์ท็อปเพื่อเปิดหลายแอปพร้อมกัน"</string> <string name="windowing_desktop_mode_exit_education_tooltip" msgid="5225660258192054132">"กลับไปที่โหมดเต็มหน้าจอได้ทุกเมื่อจากเมนูแอป"</string> <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"รับชมและทำสิ่งต่างๆ ได้มากขึ้น"</string> <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"ลากไปไว้ในแอปอื่นเพื่อแยกหน้าจอ"</string> @@ -122,8 +121,7 @@ <string name="handle_text" msgid="4419667835599523257">"แฮนเดิลแอป"</string> <string name="app_icon_text" msgid="2823268023931811747">"ไอคอนแอป"</string> <string name="fullscreen_text" msgid="1162316685217676079">"เต็มหน้าจอ"</string> - <!-- no translation found for desktop_text (9058641752519570266) --> - <skip /> + <string name="desktop_text" msgid="9058641752519570266">"หน้าต่างเดสก์ท็อป"</string> <string name="split_screen_text" msgid="1396336058129570886">"แยกหน้าจอ"</string> <string name="more_button_text" msgid="3655388105592893530">"เพิ่มเติม"</string> <string name="float_button_text" msgid="9221657008391364581">"ล่องลอย"</string> @@ -134,10 +132,11 @@ <string name="new_window_text" msgid="6318648868380652280">"หน้าต่างใหม่"</string> <string name="manage_windows_text" msgid="5567366688493093920">"จัดการหน้าต่าง"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"เปลี่ยนสัดส่วนการแสดงผล"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"ปิด"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"ปิดเมนู"</string> - <!-- no translation found for desktop_mode_app_header_chip_text (7617377295944971651) --> - <skip /> + <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (หน้าต่างเดสก์ท็อป)"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ขยายหน้าจอให้ใหญ่สุด"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="5673738963174074006">"ปรับขนาด"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"ย้ายแอปมาที่นี่ไม่ได้"</string> diff --git a/libs/WindowManager/Shell/res/values-tl/strings.xml b/libs/WindowManager/Shell/res/values-tl/strings.xml index 586d655d7901..82c085bcdbe7 100644 --- a/libs/WindowManager/Shell/res/values-tl/strings.xml +++ b/libs/WindowManager/Shell/res/values-tl/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Bagong Window"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Pamahalaan ang Mga Window"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Baguhin ang aspect ratio"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Isara"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Isara ang Menu"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (Desktop windowing)"</string> diff --git a/libs/WindowManager/Shell/res/values-tr/strings.xml b/libs/WindowManager/Shell/res/values-tr/strings.xml index 9605acfb9a8b..4d6775a9102d 100644 --- a/libs/WindowManager/Shell/res/values-tr/strings.xml +++ b/libs/WindowManager/Shell/res/values-tr/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Yeni Pencere"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Pencereleri yönet"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"En boy oranını değiştir"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Kapat"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Menüyü kapat"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (masaüstü pencereleme)"</string> diff --git a/libs/WindowManager/Shell/res/values-uk/strings.xml b/libs/WindowManager/Shell/res/values-uk/strings.xml index 54af06762029..4b2aad06cec6 100644 --- a/libs/WindowManager/Shell/res/values-uk/strings.xml +++ b/libs/WindowManager/Shell/res/values-uk/strings.xml @@ -100,8 +100,7 @@ <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Проблему не вирішено?\nНатисніть, щоб скасувати зміни"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Немає проблем із камерою? Торкніться, щоб закрити."</string> <string name="windowing_app_handle_education_tooltip" msgid="2929643449849791854">"Тут ви знайдете меню додатка"</string> - <!-- no translation found for windowing_desktop_mode_image_button_education_tooltip (7171915734817051666) --> - <skip /> + <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="7171915734817051666">"Щоб відкрити кілька додатків одночасно, перейдіть у режим вікон робочого стола"</string> <string name="windowing_desktop_mode_exit_education_tooltip" msgid="5225660258192054132">"З меню додатка можна будь-коли повернутися в повноекранний режим"</string> <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Більше простору та можливостей"</string> <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Щоб перейти в режим розділення екрана, перетягніть сюди інший додаток"</string> @@ -122,8 +121,7 @@ <string name="handle_text" msgid="4419667835599523257">"Дескриптор додатка"</string> <string name="app_icon_text" msgid="2823268023931811747">"Значок додатка"</string> <string name="fullscreen_text" msgid="1162316685217676079">"На весь екран"</string> - <!-- no translation found for desktop_text (9058641752519570266) --> - <skip /> + <string name="desktop_text" msgid="9058641752519570266">"Режим вікон робочого стола"</string> <string name="split_screen_text" msgid="1396336058129570886">"Розділити екран"</string> <string name="more_button_text" msgid="3655388105592893530">"Більше"</string> <string name="float_button_text" msgid="9221657008391364581">"Плаваюче вікно"</string> @@ -134,10 +132,11 @@ <string name="new_window_text" msgid="6318648868380652280">"Нове вікно"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Керувати вікнами"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Змінити формат"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Закрити"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Закрити меню"</string> - <!-- no translation found for desktop_mode_app_header_chip_text (7617377295944971651) --> - <skip /> + <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (режим вікон робочого стола)"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Розгорнути екран"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="5673738963174074006">"Змінити розмір"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Сюди не можна перемістити додаток"</string> diff --git a/libs/WindowManager/Shell/res/values-ur/strings.xml b/libs/WindowManager/Shell/res/values-ur/strings.xml index 9614ce9112f7..f9972d229062 100644 --- a/libs/WindowManager/Shell/res/values-ur/strings.xml +++ b/libs/WindowManager/Shell/res/values-ur/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"نئی ونڈو"</string> <string name="manage_windows_text" msgid="5567366688493093920">"ونڈوز کا نظم کریں"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"تناسبی شرح کو تبدیل کریں"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"بند کریں"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"مینیو بند کریں"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (ڈیسک ٹاپ ونڈوئنگ)"</string> diff --git a/libs/WindowManager/Shell/res/values-uz/strings.xml b/libs/WindowManager/Shell/res/values-uz/strings.xml index 6025467ea44b..231cb738940b 100644 --- a/libs/WindowManager/Shell/res/values-uz/strings.xml +++ b/libs/WindowManager/Shell/res/values-uz/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Yangi oyna"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Oynalarni boshqarish"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Tomonlar nisbatini oʻzgartirish"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Yopish"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Menyuni yopish"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (Desktop rejimidagi oynalar)"</string> diff --git a/libs/WindowManager/Shell/res/values-vi/strings.xml b/libs/WindowManager/Shell/res/values-vi/strings.xml index 4c394b2aec46..88ef07186dab 100644 --- a/libs/WindowManager/Shell/res/values-vi/strings.xml +++ b/libs/WindowManager/Shell/res/values-vi/strings.xml @@ -121,7 +121,7 @@ <string name="handle_text" msgid="4419667835599523257">"Ô điều khiển ứng dụng"</string> <string name="app_icon_text" msgid="2823268023931811747">"Biểu tượng ứng dụng"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Toàn màn hình"</string> - <string name="desktop_text" msgid="9058641752519570266">"Cửa sổ trên máy tính"</string> + <string name="desktop_text" msgid="9058641752519570266">"Chế độ cửa sổ trên máy tính"</string> <string name="split_screen_text" msgid="1396336058129570886">"Chia đôi màn hình"</string> <string name="more_button_text" msgid="3655388105592893530">"Tuỳ chọn khác"</string> <string name="float_button_text" msgid="9221657008391364581">"Nổi"</string> @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Cửa sổ mới"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Quản lý cửa sổ"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Thay đổi tỷ lệ khung hình"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Đóng"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Đóng trình đơn"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (Chế độ cửa sổ trên máy tính)"</string> diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml index b29711df14b9..a07767d0d09a 100644 --- a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml @@ -100,8 +100,7 @@ <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"没有解决此问题?\n点按即可恢复"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"相机没有问题?点按即可忽略。"</string> <string name="windowing_app_handle_education_tooltip" msgid="2929643449849791854">"您可以在此处找到应用菜单"</string> - <!-- no translation found for windowing_desktop_mode_image_button_education_tooltip (7171915734817051666) --> - <skip /> + <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="7171915734817051666">"进入桌面设备窗口化模式可同时打开多个应用"</string> <string name="windowing_desktop_mode_exit_education_tooltip" msgid="5225660258192054132">"随时从应用菜单返回全屏模式"</string> <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"查看和处理更多任务"</string> <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"拖入另一个应用,即可使用分屏模式"</string> @@ -122,8 +121,7 @@ <string name="handle_text" msgid="4419667835599523257">"应用手柄"</string> <string name="app_icon_text" msgid="2823268023931811747">"应用图标"</string> <string name="fullscreen_text" msgid="1162316685217676079">"全屏"</string> - <!-- no translation found for desktop_text (9058641752519570266) --> - <skip /> + <string name="desktop_text" msgid="9058641752519570266">"桌面设备窗口化"</string> <string name="split_screen_text" msgid="1396336058129570886">"分屏"</string> <string name="more_button_text" msgid="3655388105592893530">"更多"</string> <string name="float_button_text" msgid="9221657008391364581">"悬浮"</string> @@ -134,10 +132,11 @@ <string name="new_window_text" msgid="6318648868380652280">"新窗口"</string> <string name="manage_windows_text" msgid="5567366688493093920">"管理窗口"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"更改宽高比"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"关闭"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"关闭菜单"</string> - <!-- no translation found for desktop_mode_app_header_chip_text (7617377295944971651) --> - <skip /> + <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g>(桌面设备窗口化)"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"最大化屏幕"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="5673738963174074006">"调整大小"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"无法将应用移至此处"</string> diff --git a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml index fda5c744eccf..103ee600ff60 100644 --- a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml @@ -100,7 +100,7 @@ <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"未能修正問題?\n輕按即可還原"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"相機冇問題?㩒一下就可以即可閂咗佢。"</string> <string name="windowing_app_handle_education_tooltip" msgid="2929643449849791854">"你可在這裡找到應用程式選單"</string> - <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="7171915734817051666">"進入電腦分割視窗模式可同時開啟多個應用程式"</string> + <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="7171915734817051666">"進入桌面電腦視窗模式以同時開啟多個應用程式"</string> <string name="windowing_desktop_mode_exit_education_tooltip" msgid="5225660258192054132">"你可隨時從應用程式選單返回全螢幕"</string> <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"瀏覽更多內容及執行更多操作"</string> <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"拖入另一個應用程式即可分割螢幕"</string> @@ -121,7 +121,7 @@ <string name="handle_text" msgid="4419667835599523257">"應用程式控點"</string> <string name="app_icon_text" msgid="2823268023931811747">"應用程式圖示"</string> <string name="fullscreen_text" msgid="1162316685217676079">"全螢幕"</string> - <string name="desktop_text" msgid="9058641752519570266">"電腦分割視窗"</string> + <string name="desktop_text" msgid="9058641752519570266">"桌面電腦視窗模式"</string> <string name="split_screen_text" msgid="1396336058129570886">"分割螢幕"</string> <string name="more_button_text" msgid="3655388105592893530">"更多"</string> <string name="float_button_text" msgid="9221657008391364581">"浮動"</string> @@ -132,9 +132,11 @@ <string name="new_window_text" msgid="6318648868380652280">"新視窗"</string> <string name="manage_windows_text" msgid="5567366688493093920">"管理視窗"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"變更長寬比"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"關閉"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"關閉選單"</string> - <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (電腦分割視窗)"</string> + <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (桌面電腦視窗模式)"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"畫面最大化"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="5673738963174074006">"調整大小"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"應用程式無法移至這裡"</string> diff --git a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml index e83c647b59bb..a3d81fc6f4f0 100644 --- a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml @@ -100,7 +100,7 @@ <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"未修正問題嗎?\n輕觸即可還原"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"相機沒問題嗎?輕觸即可關閉。"</string> <string name="windowing_app_handle_education_tooltip" msgid="2929643449849791854">"你可以在這裡查看應用程式選單"</string> - <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="7171915734817051666">"進入電腦分割視窗模式可同時開啟多個應用程式"</string> + <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="7171915734817051666">"進入電腦分割視窗模式後,可同時開啟多個應用程式"</string> <string name="windowing_desktop_mode_exit_education_tooltip" msgid="5225660258192054132">"你隨時可以從應用程式選單返回全螢幕模式"</string> <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"瀏覽更多內容及執行更多操作"</string> <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"拖進另一個應用程式即可使用分割畫面模式"</string> @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"新視窗"</string> <string name="manage_windows_text" msgid="5567366688493093920">"管理視窗"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"變更顯示比例"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"關閉"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"關閉選單"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (電腦分割視窗)"</string> diff --git a/libs/WindowManager/Shell/res/values-zu/strings.xml b/libs/WindowManager/Shell/res/values-zu/strings.xml index 4d658f291e02..81d9200d6938 100644 --- a/libs/WindowManager/Shell/res/values-zu/strings.xml +++ b/libs/WindowManager/Shell/res/values-zu/strings.xml @@ -132,6 +132,8 @@ <string name="new_window_text" msgid="6318648868380652280">"Iwindi Elisha"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Phatha Amawindi"</string> <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Shintsha ukubukeka kwesilinganiselo"</string> + <!-- no translation found for handle_menu_restart_text (3907767216238298098) --> + <skip /> <string name="close_text" msgid="4986518933445178928">"Vala"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Vala Imenyu"</string> <string name="desktop_mode_app_header_chip_text" msgid="7617377295944971651">"<xliff:g id="APP_NAME">%1$s</xliff:g> (Ukwenziwa kwamawindi amaningi kwedeskithophu)"</string> diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml index 08cda7b94a78..086c8a5651c3 100644 --- a/libs/WindowManager/Shell/res/values/styles.xml +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -51,7 +51,6 @@ <item name="android:clickable">true</item> <item name="android:focusable">true</item> <item name="android:orientation">horizontal</item> - <item name="android:background">?android:attr/selectableItemBackground</item> </style> <style name="DesktopModeHandleMenuActionButtonImage"> diff --git a/libs/WindowManager/Shell/shared/res/values/dimen.xml b/libs/WindowManager/Shell/shared/res/values/dimen.xml index 74b6023bde36..c3987caa87e5 100644 --- a/libs/WindowManager/Shell/shared/res/values/dimen.xml +++ b/libs/WindowManager/Shell/shared/res/values/dimen.xml @@ -45,7 +45,7 @@ <dimen name="drop_target_full_screen_padding">20dp</dimen> <dimen name="drop_target_desktop_window_padding_small">100dp</dimen> <dimen name="drop_target_desktop_window_padding_large">130dp</dimen> - <dimen name="drop_target_expanded_view_width">364</dimen> + <dimen name="drop_target_expanded_view_width">330</dimen> <dimen name="drop_target_expanded_view_height">578</dimen> <dimen name="drop_target_expanded_view_padding_bottom">108</dimen> <dimen name="drop_target_expanded_view_padding_horizontal">24</dimen> diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt index afeaf70c9d62..ffd1f5f3a39e 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt @@ -133,7 +133,7 @@ class DragZoneFactory( fullScreenDropTargetPadding = 20.dpToPx() desktopWindowDropTargetPaddingSmall = 100.dpToPx() desktopWindowDropTargetPaddingLarge = 130.dpToPx() - expandedViewDropTargetWidth = 364.dpToPx() + expandedViewDropTargetWidth = 330.dpToPx() expandedViewDropTargetHeight = 578.dpToPx() expandedViewDropTargetPaddingBottom = 108.dpToPx() expandedViewDropTargetPaddingHorizontal = 24.dpToPx() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java index 8e3dc4c36c1d..711667760314 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java @@ -34,6 +34,8 @@ import android.view.animation.ScaleAnimation; import android.view.animation.Transformation; import android.view.animation.TranslateAnimation; +import com.android.wm.shell.shared.animation.Interpolators; + import java.util.function.Consumer; /** @@ -196,6 +198,8 @@ public class SizeChangeAnimation { float startScaleY = scaleFactor * ((float) startBounds.height()) / endBounds.height() + (1.f - scaleFactor); final AnimationSet animSet = new AnimationSet(true); + // Use a linear interpolator so the driving ValueAnimator sets the interpolation + animSet.setInterpolator(Interpolators.LINEAR); final Animation scaleAnim = new ScaleAnimation(startScaleX, 1, startScaleY, 1); scaleAnim.setDuration(scalePeriod); @@ -244,6 +248,8 @@ public class SizeChangeAnimation { + (1.f - scaleFactor)); AnimationSet snapAnimSet = new AnimationSet(true); + // Use a linear interpolator so the driving ValueAnimator sets the interpolation + snapAnimSet.setInterpolator(Interpolators.LINEAR); // Animation for the "old-state" snapshot that is atop the task. final Animation snapAlphaAnim = new AlphaAnimation(1.f, 0.f); snapAlphaAnim.setDuration(scalePeriod); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java index 26f7b360e0fc..98cae5ae9296 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java @@ -19,6 +19,8 @@ package com.android.wm.shell.back; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; +import static com.android.window.flags.Flags.enableMultidisplayTrackpadBackGesture; + import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Color; @@ -59,9 +61,9 @@ public class BackAnimationBackground { * @param statusbarHeight The height of the statusbar (in px). */ public void ensureBackground(Rect startRect, int color, - @NonNull SurfaceControl.Transaction transaction, int statusbarHeight) { + @NonNull SurfaceControl.Transaction transaction, int statusbarHeight, int displayId) { ensureBackground(startRect, color, transaction, statusbarHeight, - null /* cropBounds */, 0 /* cornerRadius */); + null /* cropBounds */, 0 /* cornerRadius */, displayId); } /** @@ -76,7 +78,7 @@ public class BackAnimationBackground { */ public void ensureBackground(Rect startRect, int color, @NonNull SurfaceControl.Transaction transaction, int statusbarHeight, - @Nullable Rect cropBounds, float cornerRadius) { + @Nullable Rect cropBounds, float cornerRadius, int displayId) { if (mBackgroundSurface != null) { return; } @@ -91,7 +93,11 @@ public class BackAnimationBackground { .setCallsite("BackAnimationBackground") .setColorLayer(); - mRootTaskDisplayAreaOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, colorLayerBuilder); + if (enableMultidisplayTrackpadBackGesture()) { + mRootTaskDisplayAreaOrganizer.attachToDisplayArea(displayId, colorLayerBuilder); + } else { + mRootTaskDisplayAreaOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, colorLayerBuilder); + } mBackgroundSurface = colorLayerBuilder.build(); transaction.setColor(mBackgroundSurface, colorComponents) .setLayer(mBackgroundSurface, BACKGROUND_LAYER) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index 746632f67725..f91154c7a362 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -1050,7 +1050,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont () -> mShellExecutor.execute(this::onBackAnimationFinished)); if (mApps.length >= 1) { - BackMotionEvent startEvent = mCurrentTracker.createStartEvent(mApps[0]); + BackMotionEvent startEvent = mCurrentTracker.createStartEvent( + Flags.removeDepartTargetFromMotion() ? null : mApps[0]); dispatchOnBackStarted(mActiveCallback, startEvent); if (startEvent.getSwipeEdge() == EDGE_NONE) { // TODO(b/373544911): onBackStarted is dispatched here so that diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt index 7e5a82e640cc..6c41c975cf4d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt @@ -52,6 +52,7 @@ import com.android.internal.jank.Cuj import com.android.internal.policy.ScreenDecorationsUtils import com.android.internal.policy.SystemBarUtils import com.android.internal.protolog.ProtoLog +import com.android.window.flags.Flags.enableMultidisplayTrackpadBackGesture import com.android.window.flags.Flags.predictiveBackTimestampApi import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer @@ -210,7 +211,8 @@ abstract class CrossActivityBackAnimation( statusbarHeight, if (closingTarget!!.windowConfiguration.tasksAreFloating()) closingTarget!!.localBounds else null, - cornerRadius + cornerRadius, + closingTarget!!.taskInfo.getDisplayId() ) ensureScrimLayer() if (isLetterboxed && enteringHasSameLetterbox) { @@ -409,7 +411,12 @@ abstract class CrossActivityBackAnimation( .setOpaque(false) .setHidden(false) - rootTaskDisplayAreaOrganizer.attachToDisplayArea(Display.DEFAULT_DISPLAY, scrimBuilder) + if (enableMultidisplayTrackpadBackGesture()) { + rootTaskDisplayAreaOrganizer.attachToDisplayArea( + closingTarget!!.taskInfo.getDisplayId(), scrimBuilder) + } else { + rootTaskDisplayAreaOrganizer.attachToDisplayArea(Display.DEFAULT_DISPLAY, scrimBuilder) + } scrimLayer = scrimBuilder.build() val colorComponents = floatArrayOf(0f, 0f, 0f) maxScrimAlpha = if (isDarkTheme) MAX_SCRIM_ALPHA_DARK else MAX_SCRIM_ALPHA_LIGHT @@ -473,7 +480,13 @@ abstract class CrossActivityBackAnimation( .setOpaque(true) .setHidden(false) - rootTaskDisplayAreaOrganizer.attachToDisplayArea(Display.DEFAULT_DISPLAY, letterboxBuilder) + if (enableMultidisplayTrackpadBackGesture()) { + rootTaskDisplayAreaOrganizer.attachToDisplayArea( + closingTarget!!.taskInfo.getDisplayId(), letterboxBuilder) + } else { + rootTaskDisplayAreaOrganizer.attachToDisplayArea( + Display.DEFAULT_DISPLAY, letterboxBuilder) + } val layer = letterboxBuilder.build() val colorComponents = floatArrayOf( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java index f48b3ffcd598..f5b0e359e019 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java @@ -175,7 +175,8 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { // Draw background. mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(), - BACKGROUNDCOLOR, mTransaction, mStatusbarHeight); + BACKGROUNDCOLOR, mTransaction, mStatusbarHeight, + mClosingTarget.taskInfo.getDisplayId()); mInterWindowMargin = mContext.getResources() .getDimension(R.dimen.cross_task_back_inter_window_margin); mVerticalMargin = mContext.getResources() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 70fa48cca0b0..6a7b5cc0e1ba 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -81,6 +81,7 @@ import android.view.WindowInsets; import android.view.WindowManager; import android.window.ScreenCapture; import android.window.ScreenCapture.SynchronousScreenCaptureListener; +import android.window.TransitionInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -156,7 +157,7 @@ import java.util.function.IntConsumer; */ public class BubbleController implements ConfigurationChangeListener, RemoteCallable<BubbleController>, Bubbles.SysuiProxy.Provider, - BubbleBarDragListener { + BubbleBarDragListener, BubbleTaskUnfoldTransitionMerger { private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES; @@ -2175,6 +2176,32 @@ public class BubbleController implements ConfigurationChangeListener, }); } + @Override + public boolean mergeTaskWithUnfold(@NonNull ActivityManager.RunningTaskInfo taskInfo, + @NonNull TransitionInfo.Change change, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT) { + if (!mBubbleTransitions.mTaskViewTransitions.isTaskViewTask(taskInfo)) { + // if this task isn't managed by bubble transitions just bail. + return false; + } + if (isShowingAsBubbleBar()) { + // if bubble bar is enabled, the task view will switch to a new surface on unfold, so we + // should not merge the transition. + return false; + } + + boolean merged = mBubbleTransitions.mTaskViewTransitions.updateBoundsForUnfold( + change.getEndAbsBounds(), startT, finishT, change.getTaskInfo(), change.getLeash()); + if (merged) { + BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble(); + if (selectedBubble != null && selectedBubble.getExpandedView() != null) { + selectedBubble.getExpandedView().onContainerClipUpdate(); + } + } + return merged; + } + /** When bubbles are floating, this will be used to notify the floating views. */ private final BubbleViewCallback mBubbleStackViewCallback = new BubbleViewCallback() { @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index 290ef1633819..ac8393576477 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -843,7 +843,8 @@ public class BubbleExpandedView extends LinearLayout { onContainerClipUpdate(); } - private void onContainerClipUpdate() { + /** Updates the clip bounds. */ + public void onContainerClipUpdate() { if (mTopClip == 0 && mBottomClip == 0 && mRightClip == 0 && mLeftClip == 0) { if (mIsClipping) { mIsClipping = false; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index 4900b6fc77ea..7ae9de8ee65d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -2959,6 +2959,9 @@ public class BubbleStackView extends FrameLayout if (mIsExpanded) { mExpandedViewAnimationController.animateForImeVisibilityChange(visible); BubbleExpandedView expandedView = getExpandedView(); + if (expandedView != null) { + expandedView.setImeVisible(visible); + } if (mPositioner.showBubblesVertically() && expandedView != null) { float selectedY = mPositioner.getExpandedBubbleXY(getState().selectedIndex, getState()).y; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskUnfoldTransitionMerger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskUnfoldTransitionMerger.kt new file mode 100644 index 000000000000..13fabc8b1d91 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskUnfoldTransitionMerger.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles + +import android.app.ActivityManager +import android.view.SurfaceControl +import android.window.TransitionInfo + +/** Merges a bubble task transition with the unfold transition. */ +interface BubbleTaskUnfoldTransitionMerger { + + /** Attempts to merge the transition. Returns `true` if the change was merged. */ + fun mergeTaskWithUnfold( + taskInfo: ActivityManager.RunningTaskInfo, + change: TransitionInfo.Change, + startT: SurfaceControl.Transaction, + finishT: SurfaceControl.Transaction + ): Boolean +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java index b89bfd5c969e..ea365efcb400 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java @@ -615,7 +615,7 @@ public class BubbleBarAnimationHelper { bbev.setSurfaceZOrderedOnTop(true); a.setDuration(EXPANDED_VIEW_ANIMATE_TO_REST_DURATION); - a.setInterpolator(Interpolators.EMPHASIZED); + a.setInterpolator(EMPHASIZED); a.start(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java index 46f6e40ec5e8..06d734c71f6a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java @@ -355,6 +355,19 @@ public class DisplayController { } } + private void onDesktopModeEligibleChanged(int displayId) { + synchronized (mDisplays) { + if (mDisplays.get(displayId) == null || getDisplay(displayId) == null) { + Slog.w(TAG, "Skipping onDesktopModeEligibleChanged on unknown" + + " display, displayId=" + displayId); + return; + } + for (int i = mDisplayChangedListeners.size() - 1; i >= 0; --i) { + mDisplayChangedListeners.get(i).onDesktopModeEligibleChanged(displayId); + } + } + } + private static class DisplayRecord { private int mDisplayId; private Context mContext; @@ -422,6 +435,13 @@ public class DisplayController { new ArraySet<>(restricted), new ArraySet<>(unrestricted)); }); } + + @Override + public void onDesktopModeEligibleChanged(int displayId) { + mMainExecutor.execute(() -> { + DisplayController.this.onDesktopModeEligibleChanged(displayId); + }); + } } /** @@ -467,5 +487,10 @@ public class DisplayController { * Called when the display topology has changed. */ default void onTopologyChanged(DisplayTopology topology) {} + + /** + * Called when the eligibility of the desktop mode for a display have changed. + */ + default void onDesktopModeEligibleChanged(int displayId) {} } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index b3c25d495002..ad509bcc1ceb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -766,6 +766,7 @@ public abstract class WMShellBaseModule { @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler, @ShellAnimationThread ShellExecutor animExecutor, + @ShellAnimationThread Handler animHandler, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, HomeTransitionObserver homeTransitionObserver, FocusTransitionObserver focusTransitionObserver) { @@ -775,7 +776,7 @@ public abstract class WMShellBaseModule { } return new Transitions(context, shellInit, shellCommandHandler, shellController, organizer, pool, displayController, displayInsetsController, mainExecutor, mainHandler, - animExecutor, rootTaskDisplayAreaOrganizer, homeTransitionObserver, + animExecutor, animHandler, rootTaskDisplayAreaOrganizer, homeTransitionObserver, focusTransitionObserver); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 5fbbb0bf1e78..46c9b07fb802 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -62,6 +62,7 @@ import com.android.wm.shell.bubbles.BubbleEducationController; import com.android.wm.shell.bubbles.BubbleLogger; import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleResizabilityChecker; +import com.android.wm.shell.bubbles.BubbleTaskUnfoldTransitionMerger; import com.android.wm.shell.bubbles.bar.BubbleBarDragListener; import com.android.wm.shell.bubbles.storage.BubblePersistentRepository; import com.android.wm.shell.common.DisplayController; @@ -100,7 +101,6 @@ import com.android.wm.shell.desktopmode.DesktopModeKeyGestureHandler; import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver; import com.android.wm.shell.desktopmode.DesktopModeMoveToDisplayTransitionHandler; import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger; -import com.android.wm.shell.desktopmode.DesktopPipTransitionObserver; import com.android.wm.shell.desktopmode.DesktopTaskChangeListener; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.desktopmode.DesktopTasksLimiter; @@ -244,6 +244,13 @@ public abstract class WMShellModule { context, logger, positioner, educationController, mainExecutor, bgExecutor); } + @WMSingleton + @Provides + static Optional<BubbleTaskUnfoldTransitionMerger> provideBubbleTaskUnfoldTransitionMerger( + Optional<BubbleController> bubbleController) { + return bubbleController.map(controller -> controller); + } + // Note: Handler needed for LauncherApps.register @WMSingleton @Provides @@ -705,7 +712,8 @@ public abstract class WMShellModule { Transitions transitions, @ShellMainThread ShellExecutor executor, @ShellMainThread Handler handler, - ShellInit shellInit) { + ShellInit shellInit, + Optional<BubbleTaskUnfoldTransitionMerger> bubbleTaskUnfoldTransitionMerger) { return new UnfoldTransitionHandler( shellInit, progressProvider.get(), @@ -714,7 +722,8 @@ public abstract class WMShellModule { transactionPool, executor, handler, - transitions); + transitions, + bubbleTaskUnfoldTransitionMerger); } @WMSingleton @@ -783,7 +792,6 @@ public abstract class WMShellModule { OverviewToDesktopTransitionObserver overviewToDesktopTransitionObserver, DesksOrganizer desksOrganizer, Optional<DesksTransitionObserver> desksTransitionObserver, - Optional<DesktopPipTransitionObserver> desktopPipTransitionObserver, UserProfileContexts userProfileContexts, DesktopModeCompatPolicy desktopModeCompatPolicy, DragToDisplayTransitionHandler dragToDisplayTransitionHandler, @@ -827,7 +835,6 @@ public abstract class WMShellModule { overviewToDesktopTransitionObserver, desksOrganizer, desksTransitionObserver.get(), - desktopPipTransitionObserver, userProfileContexts, desktopModeCompatPolicy, dragToDisplayTransitionHandler, @@ -1047,7 +1054,8 @@ public abstract class WMShellModule { DesktopModeCompatPolicy desktopModeCompatPolicy, DesktopTilingDecorViewModel desktopTilingDecorViewModel, MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController, - Optional<CompatUIHandler> compatUI + Optional<CompatUIHandler> compatUI, + DesksOrganizer desksOrganizer ) { if (!DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) { return Optional.empty(); @@ -1065,7 +1073,8 @@ public abstract class WMShellModule { activityOrientationChangeHandler, focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger, taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy, desktopTilingDecorViewModel, - multiDisplayDragMoveIndicatorController, compatUI.orElse(null))); + multiDisplayDragMoveIndicatorController, compatUI.orElse(null), + desksOrganizer)); } @WMSingleton @@ -1240,7 +1249,6 @@ public abstract class WMShellModule { Transitions transitions, ShellTaskOrganizer shellTaskOrganizer, Optional<DesktopMixedTransitionHandler> desktopMixedTransitionHandler, - Optional<DesktopPipTransitionObserver> desktopPipTransitionObserver, Optional<BackAnimationController> backAnimationController, DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider, ShellInit shellInit) { @@ -1253,7 +1261,6 @@ public abstract class WMShellModule { transitions, shellTaskOrganizer, desktopMixedTransitionHandler.get(), - desktopPipTransitionObserver, backAnimationController.get(), desktopWallpaperActivityTokenProvider, shellInit))); @@ -1275,19 +1282,6 @@ public abstract class WMShellModule { @WMSingleton @Provides - static Optional<DesktopPipTransitionObserver> provideDesktopPipTransitionObserver( - Context context - ) { - if (DesktopModeStatus.canEnterDesktopMode(context) - && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue()) { - return Optional.of( - new DesktopPipTransitionObserver()); - } - return Optional.empty(); - } - - @WMSingleton - @Provides static Optional<DesktopMixedTransitionHandler> provideDesktopMixedTransitionHandler( Context context, Transitions transitions, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index 6f0919e1d045..c5f956a80702 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -19,6 +19,7 @@ package com.android.wm.shell.dagger.pip; import android.annotation.NonNull; import android.content.Context; import android.os.Handler; +import android.window.DesktopModeFlags; import com.android.internal.jank.InteractionJankMonitor; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; @@ -42,6 +43,8 @@ import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.common.pip.SizeSpecSource; import com.android.wm.shell.dagger.WMShellBaseModule; import com.android.wm.shell.dagger.WMSingleton; +import com.android.wm.shell.desktopmode.DesktopPipTransitionController; +import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.desktopmode.DesktopUserRepositories; import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler; import com.android.wm.shell.pip2.phone.PhonePipMenuController; @@ -55,6 +58,7 @@ import com.android.wm.shell.pip2.phone.PipTransition; import com.android.wm.shell.pip2.phone.PipTransitionState; import com.android.wm.shell.pip2.phone.PipUiStateChangeController; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; @@ -91,12 +95,13 @@ public abstract class Pip2Module { DisplayController displayController, Optional<SplitScreenController> splitScreenControllerOptional, PipDesktopState pipDesktopState, + Optional<DesktopPipTransitionController> desktopPipTransitionController, PipInteractionHandler pipInteractionHandler) { return new PipTransition(context, shellInit, shellTaskOrganizer, transitions, pipBoundsState, null, pipBoundsAlgorithm, pipTaskListener, pipScheduler, pipStackListenerController, pipDisplayLayoutState, pipUiStateChangeController, displayController, splitScreenControllerOptional, - pipDesktopState, pipInteractionHandler); + pipDesktopState, desktopPipTransitionController, pipInteractionHandler); } @WMSingleton @@ -250,6 +255,22 @@ public abstract class Pip2Module { dragToDesktopTransitionHandlerOptional, rootTaskDisplayAreaOrganizer); } + @WMSingleton + @Provides + static Optional<DesktopPipTransitionController> provideDesktopPipTransitionController( + Context context, Optional<DesktopTasksController> desktopTasksControllerOptional, + Optional<DesktopUserRepositories> desktopUserRepositoriesOptional, + PipDesktopState pipDesktopState + ) { + if (DesktopModeStatus.canEnterDesktopMode(context) + && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue()) { + return Optional.of( + new DesktopPipTransitionController(desktopTasksControllerOptional.get(), + desktopUserRepositoriesOptional.get(), pipDesktopState)); + } + return Optional.empty(); + } + @BindsOptionalOf abstract DragToDesktopTransitionHandler optionalDragToDesktopTransitionHandler(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt index 3b98f8123b46..80c6f2e5ff33 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt @@ -49,9 +49,6 @@ class DesktopDisplayEventHandler( private val desktopDisplayModeController: DesktopDisplayModeController, ) : OnDisplaysChangedListener, OnDeskRemovedListener { - private val desktopRepository: DesktopRepository - get() = desktopUserRepositories.current - init { shellInit.addInitCallback({ onInit() }, this) } @@ -66,7 +63,7 @@ class DesktopDisplayEventHandler( object : UserChangeListener { override fun onUserChanged(newUserId: Int, userContext: Context) { val displayIds = rootTaskDisplayAreaOrganizer.displayIds - createDefaultDesksIfNeeded(displayIds.toSet()) + createDefaultDesksIfNeeded(displayIds.toSet(), newUserId) } } ) @@ -75,38 +72,52 @@ class DesktopDisplayEventHandler( override fun onDisplayAdded(displayId: Int) { if (displayId != DEFAULT_DISPLAY) { - desktopDisplayModeController.refreshDisplayWindowingMode() + desktopDisplayModeController.updateExternalDisplayWindowingMode(displayId) + // The default display's windowing mode depends on the availability of the external + // display. So updating the default display's windowing mode here. + desktopDisplayModeController.updateDefaultDisplayWindowingMode() } - createDefaultDesksIfNeeded(displayIds = setOf(displayId)) + createDefaultDesksIfNeeded(displayIds = setOf(displayId), userId = null) } override fun onDisplayRemoved(displayId: Int) { if (displayId != DEFAULT_DISPLAY) { - desktopDisplayModeController.refreshDisplayWindowingMode() + desktopDisplayModeController.updateDefaultDisplayWindowingMode() } // TODO: b/362720497 - move desks in closing display to the remaining desk. } - override fun onDeskRemoved(lastDisplayId: Int, deskId: Int) { - val remainingDesks = desktopRepository.getNumberOfDesks(lastDisplayId) - if (remainingDesks == 0) { - logV("All desks removed from display#$lastDisplayId") - createDefaultDesksIfNeeded(setOf(lastDisplayId)) + override fun onDesktopModeEligibleChanged(displayId: Int) { + if ( + DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue && + displayId != DEFAULT_DISPLAY + ) { + desktopDisplayModeController.updateExternalDisplayWindowingMode(displayId) + // The default display's windowing mode depends on the desktop eligibility of the + // external display. So updating the default display's windowing mode here. + desktopDisplayModeController.updateDefaultDisplayWindowingMode() } } - private fun createDefaultDesksIfNeeded(displayIds: Set<Int>) { + override fun onDeskRemoved(lastDisplayId: Int, deskId: Int) { + createDefaultDesksIfNeeded(setOf(lastDisplayId), userId = null) + } + + private fun createDefaultDesksIfNeeded(displayIds: Set<Int>, userId: Int?) { if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return logV("createDefaultDesksIfNeeded displays=%s", displayIds) mainScope.launch { desktopRepositoryInitializer.isInitialized.collect { initialized -> if (!initialized) return@collect + val repository = + userId?.let { desktopUserRepositories.getProfile(userId) } + ?: desktopUserRepositories.current displayIds .filter { displayId -> displayId != Display.INVALID_DISPLAY } .filter { displayId -> supportsDesks(displayId) } - .filter { displayId -> desktopRepository.getNumberOfDesks(displayId) == 0 } + .filter { displayId -> repository.getNumberOfDesks(displayId) == 0 } .also { displaysNeedingDesk -> logV( "createDefaultDesksIfNeeded creating default desks in displays=%s", @@ -116,7 +127,7 @@ class DesktopDisplayEventHandler( .forEach { displayId -> // TODO: b/393978539 - consider activating the desk on creation when // applicable, such as for connected displays. - desktopTasksController.createDesk(displayId) + desktopTasksController.createDesk(displayId, repository.userId) } cancel() } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt index ea2fdc0ee8ed..dec489e8fc63 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt @@ -56,44 +56,51 @@ class DesktopDisplayModeController( @ShellMainThread private val mainHandler: Handler, ) { - private val onTabletModeChangedListener = - object : InputManager.OnTabletModeChangedListener { - override fun onTabletModeChanged(whenNanos: Long, inTabletMode: Boolean) { - refreshDisplayWindowingMode() - } - } - private val inputDeviceListener = object : InputManager.InputDeviceListener { override fun onInputDeviceAdded(deviceId: Int) { - refreshDisplayWindowingMode() + updateDefaultDisplayWindowingMode() } override fun onInputDeviceChanged(deviceId: Int) { - refreshDisplayWindowingMode() + updateDefaultDisplayWindowingMode() } override fun onInputDeviceRemoved(deviceId: Int) { - refreshDisplayWindowingMode() + updateDefaultDisplayWindowingMode() } } init { if (DesktopExperienceFlags.FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH.isTrue) { - inputManager.registerOnTabletModeChangedListener( - onTabletModeChangedListener, - mainHandler, - ) inputManager.registerInputDeviceListener(inputDeviceListener, mainHandler) } } - fun refreshDisplayWindowingMode() { + fun updateExternalDisplayWindowingMode(displayId: Int) { + if (!DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue) return + + val desktopModeSupported = + displayController.getDisplay(displayId)?.let { display -> + DesktopModeStatus.isDesktopModeSupportedOnDisplay(context, display) + } ?: false + if (!desktopModeSupported) return + + // An external display should always be a freeform display when desktop mode is enabled. + updateDisplayWindowingMode(displayId, WINDOWING_MODE_FREEFORM) + } + + fun updateDefaultDisplayWindowingMode() { if (!DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue) return - val targetDisplayWindowingMode = getTargetWindowingModeForDefaultDisplay() - val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY) - requireNotNull(tdaInfo) { "DisplayAreaInfo of DEFAULT_DISPLAY must be non-null." } + updateDisplayWindowingMode(DEFAULT_DISPLAY, getTargetWindowingModeForDefaultDisplay()) + } + + private fun updateDisplayWindowingMode(displayId: Int, targetDisplayWindowingMode: Int) { + val tdaInfo = + requireNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)) { + "DisplayAreaInfo of display#$displayId must be non-null." + } val currentDisplayWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode if (currentDisplayWindowingMode == targetDisplayWindowingMode) { // Already in the target mode. @@ -101,15 +108,16 @@ class DesktopDisplayModeController( } logV( - "As an external display is connected, changing default display's windowing mode from" + - " ${windowingModeToString(currentDisplayWindowingMode)}" + - " to ${windowingModeToString(targetDisplayWindowingMode)}" + "Changing display#%d's windowing mode from %s to %s", + displayId, + windowingModeToString(currentDisplayWindowingMode), + windowingModeToString(targetDisplayWindowingMode), ) val wct = WindowContainerTransaction() wct.setWindowingMode(tdaInfo.token, targetDisplayWindowingMode) shellTaskOrganizer - .getRunningTasks(DEFAULT_DISPLAY) + .getRunningTasks(displayId) .filter { it.activityType == ACTIVITY_TYPE_STANDARD } .forEach { // TODO: b/391965153 - Reconsider the logic under multi-desk window hierarchy @@ -125,7 +133,7 @@ class DesktopDisplayModeController( // The override windowing mode of DesktopWallpaper can be UNDEFINED on fullscreen-display // right after the first launch while its resolved windowing mode is FULLSCREEN. We here // it has the FULLSCREEN override windowing mode. - desktopWallpaperActivityTokenProvider.getToken(DEFAULT_DISPLAY)?.let { token -> + desktopWallpaperActivityTokenProvider.getToken(displayId)?.let { token -> wct.setWindowingMode(token, WINDOWING_MODE_FULLSCREEN) } transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) @@ -139,7 +147,7 @@ class DesktopDisplayModeController( return true } if (DesktopExperienceFlags.FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH.isTrue) { - if (isInClamshellMode() || hasAnyMouseDevice()) { + if (hasAnyTouchpadDevice() && hasAnyPhysicalKeyboardDevice()) { return true } } @@ -186,17 +194,25 @@ class DesktopDisplayModeController( private fun hasExternalDisplay() = rootTaskDisplayAreaOrganizer.getDisplayIds().any { it != DEFAULT_DISPLAY } - private fun hasAnyMouseDevice() = - inputManager.inputDeviceIds.any { - inputManager.getInputDevice(it)?.supportsSource(InputDevice.SOURCE_MOUSE) == true + private fun hasAnyTouchpadDevice() = + inputManager.inputDeviceIds.any { deviceId -> + inputManager.getInputDevice(deviceId)?.let { device -> + device.supportsSource(InputDevice.SOURCE_TOUCHPAD) && device.isEnabled() + } ?: false } - private fun isInClamshellMode() = inputManager.isInTabletMode() == InputManager.SWITCH_STATE_OFF + private fun hasAnyPhysicalKeyboardDevice() = + inputManager.inputDeviceIds.any { deviceId -> + inputManager.getInputDevice(deviceId)?.let { device -> + !device.isVirtual() && device.isFullKeyboard() && device.isEnabled() + } ?: false + } private fun isDefaultDisplayDesktopEligible(): Boolean { - val display = requireNotNull(displayController.getDisplay(DEFAULT_DISPLAY)) { - "Display object of DEFAULT_DISPLAY must be non-null." - } + val display = + requireNotNull(displayController.getDisplay(DEFAULT_DISPLAY)) { + "Display object of DEFAULT_DISPLAY must be non-null." + } return DesktopModeStatus.isDesktopModeSupportedOnDisplay(context, display) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt index 3c44fe8061aa..55179511af6e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt @@ -22,7 +22,6 @@ import android.app.ActivityManager.RunningTaskInfo import android.app.TaskInfo import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.pm.ActivityInfo.LAUNCH_MULTIPLE import android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE import android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE_PER_TASK @@ -303,21 +302,19 @@ fun getInheritedExistingTaskBounds( // Top task is an instance of launching activity. Activity will be launching in a new // task with the existing task also being closed. Inherit existing task bounds to // prevent new task jumping. - (isLaunchingNewTask(launchMode, intentFlags) && isClosingExitingInstance(intentFlags)) -> + (isLaunchingNewSingleTask(launchMode) && isClosingExitingInstance(intentFlags)) -> lastTask.configuration.windowConfiguration.bounds else -> null } } /** - * Returns true if the launch mode or intent will result in a new task being created for the - * activity. + * Returns true if the launch mode will result in a single new task being created for the activity. */ -private fun isLaunchingNewTask(launchMode: Int, intentFlags: Int) = +private fun isLaunchingNewSingleTask(launchMode: Int) = launchMode == LAUNCH_SINGLE_TASK || launchMode == LAUNCH_SINGLE_INSTANCE || - launchMode == LAUNCH_SINGLE_INSTANCE_PER_TASK || - (intentFlags and FLAG_ACTIVITY_NEW_TASK) != 0 + launchMode == LAUNCH_SINGLE_INSTANCE_PER_TASK /** * Returns true if the intent will result in an existing task instance being closed if a new one diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java index 1c5138f486e4..8bbe36dd6644 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java @@ -57,7 +57,7 @@ import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider; import com.android.wm.shell.windowdecor.tiling.SnapEventHandler; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -307,7 +307,8 @@ public class DesktopModeVisualIndicator { if (splitRightRegion.contains(x, y)) { result = IndicatorType.TO_SPLIT_RIGHT_INDICATOR; } - if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen() + && mDragStartState == DragStartState.FROM_FULLSCREEN) { if (calculateBubbleLeftRegion(layout).contains(x, y)) { result = IndicatorType.TO_BUBBLE_LEFT_INDICATOR; } else if (calculateBubbleRightRegion(layout).contains(x, y)) { @@ -415,30 +416,59 @@ public class DesktopModeVisualIndicator { private List<Pair<Rect, IndicatorType>> initSmallTabletRegions(DisplayLayout layout, boolean isLeftRightSplit) { - boolean dragFromFullscreen = mDragStartState == DragStartState.FROM_FULLSCREEN; - boolean dragFromSplit = mDragStartState == DragStartState.FROM_SPLIT; - if (isLeftRightSplit && (dragFromFullscreen || dragFromSplit)) { + return switch (mDragStartState) { + case DragStartState.FROM_FULLSCREEN -> initSmallTabletRegionsFromFullscreen(layout, + isLeftRightSplit); + case DragStartState.FROM_SPLIT -> initSmallTabletRegionsFromSplit(layout, + isLeftRightSplit); + default -> Collections.emptyList(); + }; + } + + private List<Pair<Rect, IndicatorType>> initSmallTabletRegionsFromFullscreen( + DisplayLayout layout, boolean isLeftRightSplit) { + + List<Pair<Rect, IndicatorType>> result = new ArrayList<>(); + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + result.add(new Pair<>(calculateBubbleLeftRegion(layout), TO_BUBBLE_LEFT_INDICATOR)); + result.add(new Pair<>(calculateBubbleRightRegion(layout), TO_BUBBLE_RIGHT_INDICATOR)); + } + + if (isLeftRightSplit) { int splitRegionWidth = mContext.getResources().getDimensionPixelSize( com.android.wm.shell.shared.R.dimen.drag_zone_h_split_from_app_width_fold); - return Arrays.asList( - new Pair<>(calculateBubbleLeftRegion(layout), TO_BUBBLE_LEFT_INDICATOR), - new Pair<>(calculateBubbleRightRegion(layout), TO_BUBBLE_RIGHT_INDICATOR), - new Pair<>(calculateSplitLeftRegion(layout, splitRegionWidth, - /* captionHeight= */ 0), TO_SPLIT_LEFT_INDICATOR), - new Pair<>(calculateSplitRightRegion(layout, splitRegionWidth, - /* captionHeight= */ 0), TO_SPLIT_RIGHT_INDICATOR), - new Pair<>(new Rect(), TO_FULLSCREEN_INDICATOR) // default to fullscreen - ); + result.add(new Pair<>(calculateSplitLeftRegion(layout, splitRegionWidth, + /* captionHeight= */ 0), TO_SPLIT_LEFT_INDICATOR)); + result.add(new Pair<>(calculateSplitRightRegion(layout, splitRegionWidth, + /* captionHeight= */ 0), TO_SPLIT_RIGHT_INDICATOR)); } - if (dragFromFullscreen) { - // If left/right split is not available, we can only drag fullscreen tasks - // TODO(b/401352409): add support for top/bottom split zones - return Arrays.asList( - new Pair<>(calculateBubbleLeftRegion(layout), TO_BUBBLE_LEFT_INDICATOR), - new Pair<>(calculateBubbleRightRegion(layout), TO_BUBBLE_RIGHT_INDICATOR), - new Pair<>(new Rect(), TO_FULLSCREEN_INDICATOR) // default to fullscreen - ); + // TODO(b/401352409): add support for top/bottom split zones + // default to fullscreen + result.add(new Pair<>(new Rect(), TO_FULLSCREEN_INDICATOR)); + return result; + } + + private List<Pair<Rect, IndicatorType>> initSmallTabletRegionsFromSplit(DisplayLayout layout, + boolean isLeftRightSplit) { + if (!isLeftRightSplit) { + // Dragging a top/bottom split is not supported on small tablets + return Collections.emptyList(); } - return Collections.emptyList(); + + List<Pair<Rect, IndicatorType>> result = new ArrayList<>(); + if (BubbleAnythingFlagHelper.enableBubbleAnything()) { + result.add(new Pair<>(calculateBubbleLeftRegion(layout), TO_BUBBLE_LEFT_INDICATOR)); + result.add(new Pair<>(calculateBubbleRightRegion(layout), TO_BUBBLE_RIGHT_INDICATOR)); + } + + int splitRegionWidth = mContext.getResources().getDimensionPixelSize( + com.android.wm.shell.shared.R.dimen.drag_zone_h_split_from_app_width_fold); + result.add(new Pair<>(calculateSplitLeftRegion(layout, splitRegionWidth, + /* captionHeight= */ 0), TO_SPLIT_LEFT_INDICATOR)); + result.add(new Pair<>(calculateSplitRightRegion(layout, splitRegionWidth, + /* captionHeight= */ 0), TO_SPLIT_RIGHT_INDICATOR)); + // default to fullscreen + result.add(new Pair<>(new Rect(), TO_FULLSCREEN_INDICATOR)); + return result; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopPipTransitionController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopPipTransitionController.kt new file mode 100644 index 000000000000..88468531cc47 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopPipTransitionController.kt @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode + +import android.app.ActivityManager +import android.os.IBinder +import android.window.DesktopExperienceFlags +import android.window.WindowContainerTransaction +import com.android.internal.protolog.ProtoLog +import com.android.wm.shell.common.pip.PipDesktopState +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE + +/** + * Controller to perform extra handling to PiP transitions that are entering while in Desktop mode. + */ +class DesktopPipTransitionController( + private val desktopTasksController: DesktopTasksController, + private val desktopUserRepositories: DesktopUserRepositories, + private val pipDesktopState: PipDesktopState, +) { + + /** + * This is called by [PipTransition#handleRequest] when a request for entering PiP is received. + * + * @param wct WindowContainerTransaction that will apply these changes + * @param transition that will apply this transaction + * @param taskInfo of the task that is entering PiP + */ + fun handlePipTransition( + wct: WindowContainerTransaction, + transition: IBinder, + taskInfo: ActivityManager.RunningTaskInfo, + ) { + if (!pipDesktopState.isDesktopWindowingPipEnabled()) { + return + } + + // Early return if the transition is a synthetic transition that is not backed by a true + // system transition. + if (transition == DesktopTasksController.SYNTHETIC_TRANSITION) { + logD("handlePipTransitionIfInDesktop: SYNTHETIC_TRANSITION, not a true transition") + return + } + + val taskId = taskInfo.taskId + val displayId = taskInfo.displayId + val desktopRepository = desktopUserRepositories.getProfile(taskInfo.userId) + if (!desktopRepository.isAnyDeskActive(displayId)) { + logD("handlePipTransitionIfInDesktop: PiP transition is not in Desktop session") + return + } + + val deskId = + desktopRepository.getActiveDeskId(displayId) + ?: if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + logW( + "handlePipTransitionIfInDesktop: " + + "Active desk not found for display id %d", + displayId, + ) + return + } else { + checkNotNull(desktopRepository.getDefaultDeskId(displayId)) { + "$TAG: handlePipTransitionIfInDesktop: " + + "Expected a default desk to exist in display with id $displayId" + } + } + + val isLastTask = + desktopRepository.isOnlyVisibleNonClosingTaskInDesk( + taskId = taskId, + deskId = deskId, + displayId = displayId, + ) + if (!isLastTask) { + logD("handlePipTransitionIfInDesktop: PiP task is not last visible task in Desk") + return + } + + val desktopExitRunnable = + desktopTasksController.performDesktopExitCleanUp( + wct = wct, + deskId = deskId, + displayId = displayId, + willExitDesktop = true, + ) + desktopExitRunnable?.invoke(transition) + } + + private fun logW(msg: String, vararg arguments: Any?) { + ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + private fun logD(msg: String, vararg arguments: Any?) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + private companion object { + private const val TAG = "DesktopPipTransitionController" + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopPipTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopPipTransitionObserver.kt deleted file mode 100644 index efd3866e1bc4..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopPipTransitionObserver.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.wm.shell.desktopmode - -import android.app.WindowConfiguration.WINDOWING_MODE_PINNED -import android.os.IBinder -import android.window.DesktopModeFlags -import android.window.TransitionInfo -import com.android.internal.protolog.ProtoLog -import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE - -/** - * Observer of PiP in Desktop Mode transitions. At the moment, this is specifically tracking a PiP - * transition for a task that is entering PiP via the minimize button on the caption bar. - */ -class DesktopPipTransitionObserver { - private val pendingPipTransitions = mutableMapOf<IBinder, PendingPipTransition>() - - /** Adds a pending PiP transition to be tracked. */ - fun addPendingPipTransition(transition: PendingPipTransition) { - if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue) return - pendingPipTransitions[transition.token] = transition - } - - /** - * Called when any transition is ready, which may include transitions not tracked by this - * observer. - */ - fun onTransitionReady(transition: IBinder, info: TransitionInfo) { - if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue) return - val pipTransition = pendingPipTransitions.remove(transition) ?: return - - logD("Desktop PiP transition ready: %s", transition) - for (change in info.changes) { - val taskInfo = change.taskInfo - if (taskInfo == null || taskInfo.taskId == -1) { - continue - } - - if ( - taskInfo.taskId == pipTransition.taskId && - taskInfo.windowingMode == WINDOWING_MODE_PINNED - ) { - logD("Desktop PiP transition was successful") - pipTransition.onSuccess() - return - } - } - logD("Change with PiP task not found in Desktop PiP transition; likely failed") - } - - /** - * Data tracked for a pending PiP transition. - * - * @property token the PiP transition that is started. - * @property taskId task id of the task entering PiP. - * @property onSuccess callback to be invoked if the PiP transition is successful. - */ - data class PendingPipTransition(val token: IBinder, val taskId: Int, val onSuccess: () -> Unit) - - private fun logD(msg: String, vararg arguments: Any?) { - ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) - } - - private companion object { - private const val TAG = "DesktopPipTransitionObserver" - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 5849d4af4e7e..8b1d3fa65ac6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -42,6 +42,7 @@ import android.os.Handler import android.os.IBinder import android.os.SystemProperties import android.os.UserHandle +import android.os.UserManager import android.util.Slog import android.view.Display import android.view.Display.DEFAULT_DISPLAY @@ -115,7 +116,6 @@ import com.android.wm.shell.desktopmode.multidesks.DeskTransition import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener -import com.android.wm.shell.desktopmode.multidesks.createDesk import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer.DeskRecreationFactory import com.android.wm.shell.draganddrop.DragAndDropController @@ -163,6 +163,7 @@ import java.util.Optional import java.util.concurrent.Executor import java.util.concurrent.TimeUnit import java.util.function.Consumer +import kotlin.coroutines.suspendCoroutine import kotlin.jvm.optionals.getOrNull /** @@ -214,7 +215,6 @@ class DesktopTasksController( private val overviewToDesktopTransitionObserver: OverviewToDesktopTransitionObserver, private val desksOrganizer: DesksOrganizer, private val desksTransitionObserver: DesksTransitionObserver, - private val desktopPipTransitionObserver: Optional<DesktopPipTransitionObserver>, private val userProfileContexts: UserProfileContexts, private val desktopModeCompatPolicy: DesktopModeCompatPolicy, private val dragToDisplayTransitionHandler: DragToDisplayTransitionHandler, @@ -284,14 +284,8 @@ class DesktopTasksController( if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { desktopRepositoryInitializer.deskRecreationFactory = - DeskRecreationFactory { deskUserId, destinationDisplayId, deskId -> - if (deskUserId != userId) { - // TODO: b/400984250 - add multi-user support for multi-desk restoration. - logW("Tried to re-create desk of another user.") - null - } else { - desksOrganizer.createDesk(destinationDisplayId) - } + DeskRecreationFactory { deskUserId, destinationDisplayId, _ -> + createDeskSuspending(displayId = destinationDisplayId, userId = deskUserId) } } } @@ -449,6 +443,11 @@ class DesktopTasksController( return false } + // Secondary displays are always desktop-first + if (displayId != DEFAULT_DISPLAY) { + return true + } + val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId) // A non-organized display (e.g., non-trusted virtual displays used in CTS) doesn't have // TDA. @@ -489,22 +488,55 @@ class DesktopTasksController( runOnTransitStart?.invoke(transition) } - /** Creates a new desk in the given display. */ - fun createDesk(displayId: Int) { + /** Adds a new desk to the given display for the given user. */ + fun createDesk(displayId: Int, userId: Int = this.userId) { + logV("addDesk displayId=%d, userId=%d", displayId, userId) + val repository = userRepositories.getProfile(userId) + createDesk(displayId, userId) { deskId -> + if (deskId == null) { + logW("Failed to add desk in displayId=%d for userId=%d", displayId, userId) + } else { + repository.addDesk(displayId = displayId, deskId = deskId) + } + } + } + + private fun createDesk(displayId: Int, userId: Int = this.userId, onResult: (Int?) -> Unit) { if (displayId == Display.INVALID_DISPLAY) { logW("createDesk attempt with invalid displayId", displayId) + onResult(null) return } - if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { - desksOrganizer.createDesk(displayId) { deskId -> - taskRepository.addDesk(displayId = displayId, deskId = deskId) - } - } else { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { // In single-desk, the desk reuses the display id. - taskRepository.addDesk(displayId = displayId, deskId = displayId) + logD("createDesk reusing displayId=%d for single-desk", displayId) + onResult(displayId) + return + } + if ( + DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_HSUM.isTrue && + UserManager.isHeadlessSystemUserMode() && + UserHandle.USER_SYSTEM == userId + ) { + logW("createDesk ignoring attempt for system user") + return + } + desksOrganizer.createDesk(displayId, userId) { deskId -> + logD( + "createDesk obtained deskId=%d for displayId=%d and userId=%d", + deskId, + displayId, + userId, + ) + onResult(deskId) } } + private suspend fun createDeskSuspending(displayId: Int, userId: Int = this.userId): Int? = + suspendCoroutine { cont -> + createDesk(displayId, userId) { deskId -> cont.resumeWith(Result.success(deskId)) } + } + /** Moves task to desktop mode if task is running, else launches it in desktop mode. */ @JvmOverloads fun moveTaskToDefaultDeskAndActivate( @@ -842,7 +874,6 @@ class DesktopTasksController( } val isMinimizingToPip = DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue && - desktopPipTransitionObserver.isPresent && (taskInfo.pictureInPictureParams?.isAutoEnterEnabled ?: false) // If task is going to PiP, start a PiP transition instead of a minimize transition @@ -856,25 +887,23 @@ class DesktopTasksController( /* displayChange= */ null, /* flags= */ 0, ) - val requestRes = transitions.dispatchRequest(Binder(), requestInfo, /* skip= */ null) + val requestRes = + transitions.dispatchRequest(SYNTHETIC_TRANSITION, requestInfo, /* skip= */ null) wct.merge(requestRes.second, true) - desktopPipTransitionObserver - .get() - .addPendingPipTransition( - DesktopPipTransitionObserver.PendingPipTransition( - token = freeformTaskTransitionStarter.startPipTransition(wct), - taskId = taskInfo.taskId, - onSuccess = { - onDesktopTaskEnteredPip( - taskId = taskId, - deskId = deskId, - displayId = taskInfo.displayId, - taskIsLastVisibleTaskBeforePip = isLastTask, - ) - }, + // If the task minimizing to PiP is the last task, modify wct to perform Desktop cleanup + var desktopExitRunnable: RunOnTransitStart? = null + if (isLastTask) { + desktopExitRunnable = + performDesktopExitCleanUp( + wct = wct, + deskId = deskId, + displayId = displayId, + willExitDesktop = true, ) - ) + } + val transition = freeformTaskTransitionStarter.startPipTransition(wct) + desktopExitRunnable?.invoke(transition) } else { snapEventHandler.removeTaskIfTiled(displayId, taskId) val willExitDesktop = willExitDesktop(taskId, displayId, forceExitDesktop = false) @@ -1140,6 +1169,7 @@ class DesktopTasksController( } val t = if (remoteTransition == null) { + logV("startLaunchTransition -- no remoteTransition -- wct = $launchTransaction") desktopMixedTransitionHandler.startLaunchTransition( transitionType = transitionType, wct = launchTransaction, @@ -1887,11 +1917,7 @@ class DesktopTasksController( displayId: Int, forceExitDesktop: Boolean, ): Boolean { - if ( - forceExitDesktop && - (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue || - DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue) - ) { + if (forceExitDesktop && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { // |forceExitDesktop| is true when the callers knows we'll exit desktop, such as when // explicitly going fullscreen, so there's no point in checking the desktop state. return true @@ -1908,33 +1934,6 @@ class DesktopTasksController( return true } - /** Potentially perform Desktop cleanup after a task successfully enters PiP. */ - @VisibleForTesting - fun onDesktopTaskEnteredPip( - taskId: Int, - deskId: Int, - displayId: Int, - taskIsLastVisibleTaskBeforePip: Boolean, - ) { - if ( - !willExitDesktop(taskId, displayId, forceExitDesktop = taskIsLastVisibleTaskBeforePip) - ) { - return - } - - val wct = WindowContainerTransaction() - val desktopExitRunnable = - performDesktopExitCleanUp( - wct = wct, - deskId = deskId, - displayId = displayId, - willExitDesktop = true, - ) - - val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) - desktopExitRunnable?.invoke(transition) - } - private fun performDesktopExitCleanupIfNeeded( taskId: Int, deskId: Int? = null, @@ -1958,7 +1957,7 @@ class DesktopTasksController( } /** TODO: b/394268248 - update [deskId] to be non-null. */ - private fun performDesktopExitCleanUp( + fun performDesktopExitCleanUp( wct: WindowContainerTransaction, deskId: Int?, displayId: Int, @@ -2793,11 +2792,14 @@ class DesktopTasksController( taskInfo: RunningTaskInfo, deskId: Int?, ): RunOnTransitStart? { - // This windowing mode is to get the transition animation started; once we complete - // split select, we will change windowing mode to undefined and inherit from split stage. - // Going to undefined here causes task to flicker to the top left. - // Cancelling the split select flow will revert it to fullscreen. - wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_MULTI_WINDOW) + if (!DesktopModeFlags.ENABLE_INPUT_LAYER_TRANSITION_FIX.isTrue) { + // This windowing mode is to get the transition animation started; once we complete + // split select, we will change windowing mode to undefined and inherit from split + // stage. + // Going to undefined here causes task to flicker to the top left. + // Cancelling the split select flow will revert it to fullscreen. + wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_MULTI_WINDOW) + } // The task's density may have been overridden in freeform; revert it here as we don't // want it overridden in multi-window. wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) @@ -2892,7 +2894,7 @@ class DesktopTasksController( * null and may be used to run other desktop policies, such as minimizing another task if the * task limit has been exceeded. */ - fun addDeskActivationChanges( + private fun addDeskActivationChanges( deskId: Int, wct: WindowContainerTransaction, newTask: TaskInfo? = null, @@ -2950,6 +2952,8 @@ class DesktopTasksController( } } } + val deactivatingDesk = taskRepository.getActiveDeskId(displayId)?.takeIf { it != deskId } + val deactivationRunnable = prepareDeskDeactivationIfNeeded(wct, deactivatingDesk) return { transition -> val activateDeskTransition = if (newTaskIdInFront != null) { @@ -2970,6 +2974,7 @@ class DesktopTasksController( taskIdToMinimize?.let { minimizingTask -> addPendingMinimizeTransition(transition, minimizingTask, MinimizeReason.TASK_LIMIT) } + deactivationRunnable?.invoke(transition) } } @@ -3047,18 +3052,17 @@ class DesktopTasksController( } val wct = WindowContainerTransaction() - if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { - tasksToRemove.forEach { - val task = shellTaskOrganizer.getRunningTaskInfo(it) - if (task != null) { - wct.removeTask(task.token) - } else { - recentTasksController?.removeBackgroundTask(it) - } + tasksToRemove.forEach { + // TODO: b/404595635 - consider moving this block into [DesksOrganizer]. + val task = shellTaskOrganizer.getRunningTaskInfo(it) + if (task != null) { + wct.removeTask(task.token) + } else { + recentTasksController?.removeBackgroundTask(it) } - } else { - // TODO: 362720497 - double check background tasks are also removed. - desksOrganizer.removeDesk(wct, deskId) + } + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + desksOrganizer.removeDesk(wct, deskId, userId) } if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue && wct.isEmpty) return val transition = transitions.startTransition(TRANSIT_CLOSE, wct, /* handler= */ null) @@ -3624,6 +3628,7 @@ class DesktopTasksController( pw.println("${prefix}DesktopTasksController") DesktopModeStatus.dump(pw, innerPrefix, context) userRepositories.dump(pw, innerPrefix) + focusTransitionObserver.dump(pw, innerPrefix) } /** The interface for calls from outside the shell, within the host process. */ @@ -3955,6 +3960,12 @@ class DesktopTasksController( DesktopTaskToFrontReason.TASKBAR_MANAGE_WINDOW -> UnminimizeReason.TASKBAR_MANAGE_WINDOW } + + @JvmField + /** + * A placeholder for a synthetic transition that isn't backed by a true system transition. + */ + val SYNTHETIC_TRANSITION: IBinder = Binder() } /** Defines interface for classes that can listen to changes for task resize. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt index df4d18f8c803..3fd955d112f0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt @@ -42,7 +42,6 @@ import com.android.wm.shell.shared.TransitionUtil.isOpeningMode import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions -import java.util.Optional /** * A [Transitions.TransitionObserver] that observes shell transitions and updates the @@ -55,7 +54,6 @@ class DesktopTasksTransitionObserver( private val transitions: Transitions, private val shellTaskOrganizer: ShellTaskOrganizer, private val desktopMixedTransitionHandler: DesktopMixedTransitionHandler, - private val desktopPipTransitionObserver: Optional<DesktopPipTransitionObserver>, private val backAnimationController: BackAnimationController, private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider, shellInit: ShellInit, @@ -97,7 +95,6 @@ class DesktopTasksTransitionObserver( removeTaskIfNeeded(info) } removeWallpaperOnLastTaskClosingIfNeeded(transition, info) - desktopPipTransitionObserver.ifPresent { it.onTransitionReady(transition, info) } } private fun removeTaskIfNeeded(info: TransitionInfo) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt index 5a988fcd1b77..605465b15468 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt @@ -18,13 +18,11 @@ package com.android.wm.shell.desktopmode.multidesks import android.app.ActivityManager import android.window.TransitionInfo import android.window.WindowContainerTransaction -import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer.OnCreateCallback -import kotlin.coroutines.suspendCoroutine /** An organizer of desk containers in which to host child desktop windows. */ interface DesksOrganizer { - /** Creates a new desk container in the given display. */ - fun createDesk(displayId: Int, callback: OnCreateCallback) + /** Creates a new desk container to use in the given display for the given user. */ + fun createDesk(displayId: Int, userId: Int, callback: OnCreateCallback) /** Activates the given desk, making it visible in its display. */ fun activateDesk(wct: WindowContainerTransaction, deskId: Int) @@ -32,8 +30,8 @@ interface DesksOrganizer { /** Deactivates the given desk, removing it as the default launch container for new tasks. */ fun deactivateDesk(wct: WindowContainerTransaction, deskId: Int) - /** Removes the given desk and its desktop windows. */ - fun removeDesk(wct: WindowContainerTransaction, deskId: Int) + /** Removes the given desk of the given user. */ + fun removeDesk(wct: WindowContainerTransaction, deskId: Int, userId: Int) /** Moves the given task to the given desk. */ fun moveTaskToDesk( @@ -78,15 +76,12 @@ interface DesksOrganizer { /** Whether the desk is activate according to the given change at the end of a transition. */ fun isDeskActiveAtEnd(change: TransitionInfo.Change, deskId: Int): Boolean + /** Allows for other classes to respond to task changes this organizer receives. */ + fun setOnDesktopTaskInfoChangedListener(listener: (ActivityManager.RunningTaskInfo) -> Unit) + /** A callback that is invoked when the desk container is created. */ fun interface OnCreateCallback { /** Calls back when the [deskId] has been created. */ fun onCreated(deskId: Int) } } - -/** Creates a new desk container in the given display. */ -suspend fun DesksOrganizer.createDesk(displayId: Int): Int = suspendCoroutine { cont -> - val onCreateCallback = OnCreateCallback { deskId -> cont.resumeWith(Result.success(deskId)) } - createDesk(displayId, onCreateCallback) -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt index 49ca58e7b32a..e4edeb95be6d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt @@ -30,6 +30,7 @@ import android.window.TransitionInfo import android.window.WindowContainerToken import android.window.WindowContainerTransaction import androidx.core.util.forEach +import androidx.core.util.valueIterator import com.android.internal.annotations.VisibleForTesting import com.android.internal.protolog.ProtoLog import com.android.wm.shell.ShellTaskOrganizer @@ -40,7 +41,13 @@ import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellInit import java.io.PrintWriter -/** A [DesksOrganizer] that uses root tasks as the container of each desk. */ +/** + * A [DesksOrganizer] that uses root tasks as the container of each desk. + * + * Note that root tasks are reusable between multiple users at the same time, and may also be + * pre-created to have one ready for the first entry to the default desk, so root-task existence + * does not imply a formal desk exists to the user. + */ class RootTaskDesksOrganizer( shellInit: ShellInit, shellCommandHandler: ShellCommandHandler, @@ -54,6 +61,7 @@ class RootTaskDesksOrganizer( mutableListOf<CreateDeskMinimizationRootRequest>() @VisibleForTesting val deskMinimizationRootsByDeskId: MutableMap<Int, DeskMinimizationRoot> = mutableMapOf() + private var onTaskInfoChangedListener: ((RunningTaskInfo) -> Unit)? = null init { if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { @@ -64,9 +72,26 @@ class RootTaskDesksOrganizer( } } - override fun createDesk(displayId: Int, callback: OnCreateCallback) { - logV("createDesk in display: %d", displayId) - createDeskRootRequests += CreateDeskRequest(displayId, callback) + override fun createDesk(displayId: Int, userId: Int, callback: OnCreateCallback) { + logV("createDesk in displayId=%d userId=%s", displayId, userId) + // Find an existing desk that is not yet used by this user. + val unassignedDesk = + deskRootsByDeskId + .valueIterator() + .asSequence() + .filterNot { desk -> userId in desk.users } + .firstOrNull() + if (unassignedDesk != null) { + unassignedDesk.users.add(userId) + callback.onCreated(unassignedDesk.deskId) + return + } + createDeskRoot(displayId, userId, callback) + } + + private fun createDeskRoot(displayId: Int, userId: Int, callback: OnCreateCallback) { + logV("createDeskRoot in display: %d for user: %d", displayId, userId) + createDeskRootRequests += CreateDeskRequest(displayId, userId, callback) shellTaskOrganizer.createRootTask( displayId, WINDOWING_MODE_FREEFORM, @@ -75,31 +100,52 @@ class RootTaskDesksOrganizer( ) } - override fun removeDesk(wct: WindowContainerTransaction, deskId: Int) { - logV("removeDesk %d", deskId) - deskRootsByDeskId[deskId]?.let { root -> wct.removeRootTask(root.token) } - deskMinimizationRootsByDeskId[deskId]?.let { root -> wct.removeRootTask(root.token) } + override fun removeDesk(wct: WindowContainerTransaction, deskId: Int, userId: Int) { + logV("removeDesk %d for userId=%d", deskId, userId) + val deskRoot = deskRootsByDeskId[deskId] + if (deskRoot == null) { + logW("removeDesk attempted to remove non-existent desk=%d", deskId) + return + } + updateLaunchRoot(wct, deskId, enabled = false) + deskRoot.users.remove(userId) + if (deskRoot.users.isEmpty()) { + // No longer in use by any users, remove it completely. + logD("removeDesk %d is no longer used by any users, removing it completely", deskId) + wct.removeRootTask(deskRoot.token) + deskMinimizationRootsByDeskId[deskId]?.let { root -> wct.removeRootTask(root.token) } + } } override fun activateDesk(wct: WindowContainerTransaction, deskId: Int) { logV("activateDesk %d", deskId) val root = checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" } wct.reorder(root.token, /* onTop= */ true) - wct.setLaunchRoot( - /* container= */ root.taskInfo.token, - /* windowingModes= */ intArrayOf(WINDOWING_MODE_FREEFORM, WINDOWING_MODE_UNDEFINED), - /* activityTypes= */ intArrayOf(ACTIVITY_TYPE_UNDEFINED, ACTIVITY_TYPE_STANDARD), - ) + updateLaunchRoot(wct, deskId, enabled = true) } override fun deactivateDesk(wct: WindowContainerTransaction, deskId: Int) { logV("deactivateDesk %d", deskId) + updateLaunchRoot(wct, deskId, enabled = false) + } + + private fun updateLaunchRoot(wct: WindowContainerTransaction, deskId: Int, enabled: Boolean) { val root = checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" } - wct.setLaunchRoot( - /* container= */ root.taskInfo.token, - /* windowingModes= */ null, - /* activityTypes= */ null, - ) + root.isLaunchRootRequested = enabled + logD("updateLaunchRoot deskId=%d enabled=%b", deskId, enabled) + if (enabled) { + wct.setLaunchRoot( + /* container= */ root.taskInfo.token, + /* windowingModes= */ intArrayOf(WINDOWING_MODE_FREEFORM, WINDOWING_MODE_UNDEFINED), + /* activityTypes= */ intArrayOf(ACTIVITY_TYPE_UNDEFINED, ACTIVITY_TYPE_STANDARD), + ) + } else { + wct.setLaunchRoot( + /* container= */ root.taskInfo.token, + /* windowingModes= */ null, + /* activityTypes= */ null, + ) + } } override fun moveTaskToDesk( @@ -213,6 +259,10 @@ class RootTaskDesksOrganizer( change.taskInfo?.isVisibleRequested == true && change.mode == TRANSIT_TO_FRONT + override fun setOnDesktopTaskInfoChangedListener(listener: (RunningTaskInfo) -> Unit) { + onTaskInfoChangedListener = listener + } + override fun onTaskAppeared(taskInfo: RunningTaskInfo, leash: SurfaceControl) { handleTaskAppeared(taskInfo, leash) updateLaunchAdjacentController() @@ -220,6 +270,12 @@ class RootTaskDesksOrganizer( override fun onTaskInfoChanged(taskInfo: RunningTaskInfo) { handleTaskInfoChanged(taskInfo) + if ( + taskInfo.taskId !in deskRootsByDeskId && + deskMinimizationRootsByDeskId.values.none { it.rootId == taskInfo.taskId } + ) { + onTaskInfoChangedListener?.invoke(taskInfo) + } updateLaunchAdjacentController() } @@ -264,7 +320,13 @@ class RootTaskDesksOrganizer( // Appearing root matches desk request. val deskId = taskInfo.taskId logV("Desk #$deskId appeared") - deskRootsByDeskId[deskId] = DeskRoot(deskId, taskInfo, leash) + deskRootsByDeskId[deskId] = + DeskRoot( + deskId = deskId, + taskInfo = taskInfo, + leash = leash, + users = mutableSetOf(deskRequest.userId), + ) createDeskRootRequests.remove(deskRequest) deskRequest.onCreateCallback.onCreated(deskId) createDeskMinimizationRoot(displayId = appearingInDisplayId, deskId = deskId) @@ -419,6 +481,8 @@ class RootTaskDesksOrganizer( val taskInfo: RunningTaskInfo, val leash: SurfaceControl, val children: MutableSet<Int> = mutableSetOf(), + val users: MutableSet<Int> = mutableSetOf(), + var isLaunchRootRequested: Boolean = false, ) { val token: WindowContainerToken = taskInfo.token } @@ -438,15 +502,24 @@ class RootTaskDesksOrganizer( private data class CreateDeskRequest( val displayId: Int, + val userId: Int, val onCreateCallback: OnCreateCallback, ) private data class CreateDeskMinimizationRootRequest(val displayId: Int, val deskId: Int) + private fun logD(msg: String, vararg arguments: Any?) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + private fun logV(msg: String, vararg arguments: Any?) { ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) } + private fun logW(msg: String, vararg arguments: Any?) { + ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + private fun logE(msg: String, vararg arguments: Any?) { ProtoLog.e(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) } @@ -462,7 +535,9 @@ class RootTaskDesksOrganizer( val minimizationRoot = deskMinimizationRootsByDeskId[deskId] pw.println("$innerPrefix #$deskId visible=${root.taskInfo.isVisible}") pw.println("$innerPrefix displayId=${root.taskInfo.displayId}") + pw.println("$innerPrefix isLaunchRootRequested=${root.isLaunchRootRequested}") pw.println("$innerPrefix children=${root.children}") + pw.println("$innerPrefix users=${root.users}") pw.println("$innerPrefix minimization root:") pw.println("$innerPrefix rootId=${minimizationRoot?.rootId}") if (minimizationRoot != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java index e7492f17835a..2476ee1a4090 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java @@ -174,7 +174,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs SurfaceControl.Transaction finishT) { mTaskChangeListener.ifPresent(listener -> listener.onTaskChanging(change.getTaskInfo())); mWindowDecorViewModel.onTaskChanging( - change.getTaskInfo(), change.getLeash(), startT, finishT, change.getMode()); + change.getTaskInfo(), change.getLeash(), startT, finishT); } private void onToFrontTransitionReady( @@ -184,7 +184,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs mTaskChangeListener.ifPresent( listener -> listener.onTaskMovingToFront(change.getTaskInfo())); mWindowDecorViewModel.onTaskChanging( - change.getTaskInfo(), change.getLeash(), startT, finishT, change.getMode()); + change.getTaskInfo(), change.getLeash(), startT, finishT); } private void onToBackTransitionReady( @@ -194,7 +194,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs mTaskChangeListener.ifPresent( listener -> listener.onTaskMovingToBack(change.getTaskInfo())); mWindowDecorViewModel.onTaskChanging( - change.getTaskInfo(), change.getLeash(), startT, finishT, change.getMode()); + change.getTaskInfo(), change.getLeash(), startT, finishT); } @Override 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 5d8d8b685a23..9ec1c7d65a6e 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 @@ -50,7 +50,9 @@ import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.os.Bundle; +import android.os.Debug; import android.os.IBinder; +import android.util.Log; import android.view.SurfaceControl; import android.view.WindowManager; import android.window.TransitionInfo; @@ -71,6 +73,7 @@ import com.android.wm.shell.common.pip.PipDesktopState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipUtils; +import com.android.wm.shell.desktopmode.DesktopPipTransitionController; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; @@ -115,6 +118,7 @@ public class PipTransition extends PipTransitionController implements private final DisplayController mDisplayController; private final PipSurfaceTransactionHelper mPipSurfaceTransactionHelper; private final PipDesktopState mPipDesktopState; + private final Optional<DesktopPipTransitionController> mDesktopPipTransitionController; private final PipInteractionHandler mPipInteractionHandler; // @@ -158,6 +162,7 @@ public class PipTransition extends PipTransitionController implements DisplayController displayController, Optional<SplitScreenController> splitScreenControllerOptional, PipDesktopState pipDesktopState, + Optional<DesktopPipTransitionController> desktopPipTransitionController, PipInteractionHandler pipInteractionHandler) { super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController, pipBoundsAlgorithm); @@ -172,6 +177,7 @@ public class PipTransition extends PipTransitionController implements mDisplayController = displayController; mPipSurfaceTransactionHelper = new PipSurfaceTransactionHelper(mContext); mPipDesktopState = pipDesktopState; + mDesktopPipTransitionController = desktopPipTransitionController; mPipInteractionHandler = pipInteractionHandler; mExpandHandler = new PipExpandHandler(mContext, pipBoundsState, pipBoundsAlgorithm, @@ -227,7 +233,18 @@ public class PipTransition extends PipTransitionController implements @NonNull TransitionRequestInfo request) { if (isAutoEnterInButtonNavigation(request) || isEnterPictureInPictureModeRequest(request)) { mEnterTransition = transition; - return getEnterPipTransaction(transition, request.getPipChange()); + final WindowContainerTransaction wct = getEnterPipTransaction(transition, + request.getPipChange()); + + mDesktopPipTransitionController.ifPresent( + desktopPipTransitionController -> + desktopPipTransitionController.handlePipTransition( + wct, + transition, + request.getPipChange().getTaskInfo() + ) + ); + return wct; } return null; } @@ -300,6 +317,20 @@ public class PipTransition extends PipTransitionController implements return startAlphaTypeEnterAnimation(info, startTransaction, finishTransaction, finishCallback); } + + TransitionInfo.Change pipActivityChange = PipTransitionUtils + .getDeferConfigActivityChange(info, pipChange.getTaskInfo().getToken()); + if (pipActivityChange == null) { + // Legacy-enter and swipe-pip-to-home filters did not resolve a scheduled PiP entry. + // Bounds-type enter animation is the last resort, and it requires a config-at-end + // activity amongst the list of changes. If no such change, something went wrong. + Log.wtf(TAG, String.format(""" + PipTransition.startAnimation didn't handle a scheduled PiP entry + transitionInfo=%s, + callers=%s""", info, Debug.getCallers(4))); + return false; + } + return startBoundsTypeEnterAnimation(info, startTransaction, finishTransaction, finishCallback); } else if (transition == mExitViaExpandTransition) { @@ -824,26 +855,15 @@ public class PipTransition extends PipTransitionController implements return true; } - // Sometimes root PiP task can have TF children. These child containers can be collected - // even if they can promote to their parents: e.g. if they are marked as "organized". - // So we count the chain of containers under PiP task as one "real" changing target; - // iterate through changes bottom-to-top to properly identify parents. - int expectedTargetCount = 1; - WindowContainerToken lastPipChildToken = pipChange.getContainer(); - for (int i = info.getChanges().size() - 1; i >= 0; --i) { - TransitionInfo.Change change = info.getChanges().get(i); - if (change == pipChange || change.getContainer() == null) continue; - if (change.getParent() != null && change.getParent().equals(lastPipChildToken)) { - // Allow an extra change since our pinned root task has a child. - ++expectedTargetCount; - lastPipChildToken = change.getContainer(); - } - } - - // If the only root task change in the changes list is a opening type PiP task, - // then this is legacy-enter PiP. - return info.getChanges().size() == expectedTargetCount - && TransitionUtil.isOpeningMode(pipChange.getMode()); + // #getEnterPipTransaction() always attempts to mark PiP activity as config-at-end one. + // However, the activity will only actually be marked config-at-end by Core if it is + // both isVisible and isVisibleRequested, which is when we can't run bounds animation. + // + // So we can use the absence of a config-at-end activity as a signal that we should run + // a legacy-enter PiP animation instead. + return TransitionUtil.isOpeningMode(pipChange.getMode()) + && PipTransitionUtils.getDeferConfigActivityChange( + info, pipChange.getContainer()) == null; } return false; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 277842477a4c..d240aca522bb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -124,7 +124,6 @@ import android.view.SurfaceControl; import android.view.WindowManager; import android.widget.Toast; import android.window.DesktopExperienceFlags; -import android.window.DesktopModeFlags; import android.window.DisplayAreaInfo; import android.window.RemoteTransition; import android.window.TransitionInfo; @@ -684,8 +683,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (!enteredSplitSelect) { return null; } - if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue() - && !DesktopModeFlags.ENABLE_INPUT_LAYER_TRANSITION_FIX.isTrue()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()) { mTaskOrganizer.applyTransaction(wct); return null; } 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 a6f872634ee9..22848c38bb1c 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 @@ -728,6 +728,48 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV taskView.notifyAppeared(newTask); } + /** + * Updates bounds for the task view during an unfold transition. + * + * @return true if the task was found and a transition for this task is pending. false + * otherwise. + */ + public boolean updateBoundsForUnfold(Rect bounds, SurfaceControl.Transaction startTransaction, + SurfaceControl.Transaction finishTransaction, + ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + final TaskViewTaskController taskView = findTaskView(taskInfo); + if (taskView == null) { + return false; + } + + final PendingTransition pendingTransition = findPending(taskView, TRANSIT_CHANGE); + if (pendingTransition == null) { + return false; + } + + mPending.remove(pendingTransition); + + // reparent the task under the task view surface and set the bounds on it + startTransaction.reparent(leash, taskView.getSurfaceControl()) + .setPosition(leash, 0, 0) + .setWindowCrop(leash, bounds.width(), bounds.height()) + .show(leash); + // the finish transaction would reparent the task back to the transition root, so reparent + // it again to the task view surface + finishTransaction.reparent(leash, taskView.getSurfaceControl()) + .setPosition(leash, 0, 0) + .setWindowCrop(leash, bounds.width(), bounds.height()); + if (useRepo()) { + final TaskViewRepository.TaskViewState state = mTaskViewRepo.byTaskView(taskView); + if (state != null) { + state.mBounds.set(bounds); + } + } else { + updateBoundsState(taskView, bounds); + } + return true; + } + private void updateBounds(TaskViewTaskController taskView, Rect boundsOnScreen, SurfaceControl.Transaction startTransaction, SurfaceControl.Transaction finishTransaction, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java index bff08ba6d88f..3240cbb779c6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java @@ -169,7 +169,7 @@ public class DefaultSurfaceAnimator { needCrop = true; } if (needCrop) { - t.setCrop(leash, mAnimClipRect); + t.setWindowCrop(leash, mAnimClipRect); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index e9200834c5dd..5b6993863c5d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -133,6 +133,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { private final DisplayController mDisplayController; private final Context mContext; private final Handler mMainHandler; + private final Handler mAnimHandler; private final ShellExecutor mMainExecutor; private final ShellExecutor mAnimExecutor; private final TransitionAnimation mTransitionAnimation; @@ -171,6 +172,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { @NonNull TransactionPool transactionPool, @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler, @NonNull ShellExecutor animExecutor, + @NonNull Handler animHandler, @NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer, @NonNull InteractionJankMonitor interactionJankMonitor) { mDisplayController = displayController; @@ -179,6 +181,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { mMainHandler = mainHandler; mMainExecutor = mainExecutor; mAnimExecutor = animExecutor; + mAnimHandler = animHandler; mTransitionAnimation = new TransitionAnimation(context, false /* debug */, Transitions.TAG); mCurrentUserId = UserHandle.myUserId(); mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class); @@ -349,10 +352,6 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { mAnimations.put(transition, animations); final boolean isTaskTransition = isTaskTransition(info); - if (isTaskTransition) { - mInteractionJankMonitor.begin(info.getRoot(0).getLeash(), mContext, - mMainHandler, CUJ_DEFAULT_TASK_TO_TASK_ANIMATION); - } final Runnable onAnimFinish = () -> { if (!animations.isEmpty()) return; @@ -642,6 +641,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { // now start animations. they are started on another thread, so we have to post them // *after* applying the startTransaction mAnimExecutor.execute(() -> { + if (isTaskTransition) { + mInteractionJankMonitor.begin(info.getRoot(0).getLeash(), mContext, + mAnimHandler, CUJ_DEFAULT_TASK_TO_TASK_ANIMATION); + } for (int i = 0; i < animations.size(); ++i) { animations.get(i).start(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java index f0f1ad05008b..b91fb048dc09 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java @@ -31,6 +31,7 @@ import android.annotation.NonNull; import android.app.ActivityManager.RunningTaskInfo; import android.os.RemoteException; import android.util.ArraySet; +import android.util.IndentingPrintWriter; import android.util.Slog; import android.util.SparseArray; import android.window.TransitionInfo; @@ -38,6 +39,7 @@ import android.window.TransitionInfo; import com.android.wm.shell.shared.FocusTransitionListener; import com.android.wm.shell.shared.IFocusTransitionListener; +import java.io.PrintWriter; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -237,4 +239,21 @@ public class FocusTransitionObserver { } return task.displayId == mFocusedDisplayId && isFocusedOnDisplay(task); } + + /** Dumps focused display and tasks. */ + public void dump(PrintWriter originalWriter, String prefix) { + final IndentingPrintWriter writer = + new IndentingPrintWriter(originalWriter, " ", prefix); + writer.println("FocusTransitionObserver:"); + writer.increaseIndent(); + writer.printf("currentFocusedDisplayId=%d\n", mFocusedDisplayId); + writer.println("currentFocusedTaskOnDisplay:"); + writer.increaseIndent(); + for (int i = 0; i < mFocusedTaskOnDisplay.size(); i++) { + writer.printf("Display #%d: taskId=%d topActivity=%s\n", + mFocusedTaskOnDisplay.keyAt(i), + mFocusedTaskOnDisplay.valueAt(i).taskId, + mFocusedTaskOnDisplay.valueAt(i).topActivity); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 3dc8733c879d..84724268cfc2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -215,6 +215,7 @@ public class Transitions implements RemoteCallable<Transitions>, private final Context mContext; private final ShellExecutor mMainExecutor; private final ShellExecutor mAnimExecutor; + private final Handler mAnimHandler; private final TransitionPlayerImpl mPlayerImpl; private final DefaultTransitionHandler mDefaultTransitionHandler; private final RemoteTransitionHandler mRemoteTransitionHandler; @@ -319,11 +320,12 @@ public class Transitions implements RemoteCallable<Transitions>, @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler, @NonNull ShellExecutor animExecutor, + @NonNull Handler animHandler, @NonNull HomeTransitionObserver homeTransitionObserver, @NonNull FocusTransitionObserver focusTransitionObserver) { this(context, shellInit, new ShellCommandHandler(), shellController, organizer, pool, displayController, displayInsetsController, mainExecutor, mainHandler, animExecutor, - new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit), + animHandler, new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit), homeTransitionObserver, focusTransitionObserver); } @@ -338,6 +340,7 @@ public class Transitions implements RemoteCallable<Transitions>, @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler, @NonNull ShellExecutor animExecutor, + @NonNull Handler animHandler, @NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer, @NonNull HomeTransitionObserver homeTransitionObserver, @NonNull FocusTransitionObserver focusTransitionObserver) { @@ -345,11 +348,12 @@ public class Transitions implements RemoteCallable<Transitions>, mContext = context; mMainExecutor = mainExecutor; mAnimExecutor = animExecutor; + mAnimHandler = animHandler; mDisplayController = displayController; mPlayerImpl = new TransitionPlayerImpl(); mDefaultTransitionHandler = new DefaultTransitionHandler(context, shellInit, displayController, displayInsetsController, pool, mainExecutor, mainHandler, - animExecutor, rootTDAOrganizer, InteractionJankMonitor.getInstance()); + animExecutor, mAnimHandler, rootTDAOrganizer, InteractionJankMonitor.getInstance()); mRemoteTransitionHandler = new RemoteTransitionHandler(mMainExecutor); mShellCommandHandler = shellCommandHandler; mShellController = shellController; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java index 7fd19a7d2a88..706a366441cd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java @@ -38,6 +38,7 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.internal.protolog.ProtoLog; +import com.android.wm.shell.bubbles.BubbleTaskUnfoldTransitionMerger; import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.shared.TransitionUtil; import com.android.wm.shell.sysui.ShellInit; @@ -53,6 +54,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.concurrent.Executor; /** @@ -80,6 +82,7 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene private final ShellUnfoldProgressProvider mUnfoldProgressProvider; private final Transitions mTransitions; + private final Optional<BubbleTaskUnfoldTransitionMerger> mBubbleTaskUnfoldTransitionMerger; private final Executor mExecutor; private final TransactionPool mTransactionPool; private final Handler mHandler; @@ -108,12 +111,14 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene TransactionPool transactionPool, Executor executor, Handler handler, - Transitions transitions) { + Transitions transitions, + Optional<BubbleTaskUnfoldTransitionMerger> bubbleTaskUnfoldTransitionMerger) { mUnfoldProgressProvider = unfoldProgressProvider; mTransitions = transitions; mTransactionPool = transactionPool; mExecutor = executor; mHandler = handler; + mBubbleTaskUnfoldTransitionMerger = bubbleTaskUnfoldTransitionMerger; mAnimators.add(splitUnfoldTaskAnimator); mAnimators.add(fullscreenUnfoldAnimator); @@ -237,14 +242,26 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene } // TODO (b/286928742) unfold transition handler should be part of mixed handler to // handle merges better. + for (int i = 0; i < info.getChanges().size(); ++i) { final TransitionInfo.Change change = info.getChanges().get(i); final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); if (taskInfo != null && taskInfo.configuration.windowConfiguration.isAlwaysOnTop()) { - // Tasks that are always on top (e.g. bubbles), will handle their own transition - // as they are on top of everything else. So skip merging transitions here. - return; + // Tasks that are always on top, excluding bubbles, will handle their own transition + // as they are on top of everything else. If this is a transition for a bubble task, + // attempt to merge it. Otherwise skip merging transitions. + if (mBubbleTaskUnfoldTransitionMerger.isPresent()) { + boolean merged = + mBubbleTaskUnfoldTransitionMerger + .get() + .mergeTaskWithUnfold(taskInfo, change, startT, finishT); + if (!merged) { + return; + } + } else { + return; + } } } // Apply changes happening during the unfold animation immediately diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index 42321e56e72b..7871179a50de 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -49,7 +49,6 @@ import android.view.SurfaceControl; import android.view.View; import android.view.ViewConfiguration; import android.window.DisplayAreaInfo; -import android.window.TransitionInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -234,8 +233,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusT RunningTaskInfo taskInfo, SurfaceControl taskSurface, SurfaceControl.Transaction startT, - SurfaceControl.Transaction finishT, - @TransitionInfo.TransitionMode int changeMode) { + SurfaceControl.Transaction finishT) { final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); if (!shouldShowWindowDecor(taskInfo)) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecorViewModel.java index 4511fbe10764..2b2cdf84005c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecorViewModel.java @@ -31,7 +31,6 @@ import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.SurfaceControl; import android.view.View; -import android.window.TransitionInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -160,8 +159,7 @@ public abstract class CarWindowDecorViewModel RunningTaskInfo taskInfo, SurfaceControl taskSurface, SurfaceControl.Transaction startT, - SurfaceControl.Transaction finishT, - @TransitionInfo.TransitionMode int changeMode) { + SurfaceControl.Transaction finishT) { final CarWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); if (!shouldShowWindowDecor(taskInfo)) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecoration.java index f6acca95916f..dc1b94e80ed7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecoration.java @@ -89,9 +89,6 @@ public class CarWindowDecoration extends WindowDecoration<WindowDecorLinearLayou updateRelayoutParams(mRelayoutParams, taskInfo, isCaptionVisible); relayout(mRelayoutParams, startT, finishT, wct, mRootView, mResult); - if (DesktopModeFlags.ENABLE_DESKTOP_APP_HANDLE_ANIMATION.isTrue()) { - setCaptionVisibility(isCaptionVisible); - } // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo mBgExecutor.execute(() -> mTaskOrganizer.applyTransaction(wct)); @@ -100,19 +97,23 @@ public class CarWindowDecoration extends WindowDecoration<WindowDecorLinearLayou // Nothing is set up in this case including the decoration surface. return; } + if (mRootView != mResult.mRootView) { mRootView = mResult.mRootView; setupRootView(mResult.mRootView, mClickListener); } - } - private void setCaptionVisibility(boolean visible) { - if (mRootView == null) { - return; + if (DesktopModeFlags.ENABLE_DESKTOP_APP_HANDLE_ANIMATION.isTrue()) { + setCaptionVisibility(mRootView, mRelayoutParams.mIsCaptionVisible); } + } + + private void setCaptionVisibility(@NonNull View rootView, boolean visible) { final int v = visible ? View.VISIBLE : View.GONE; - final View captionView = mRootView.findViewById(getCaptionViewId()); - captionView.setVisibility(v); + final View captionView = rootView.findViewById(getCaptionViewId()); + if (captionView != null) { + captionView.setVisibility(v); + } } @Override 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 69e1f36dec0b..16fa5120d64b 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 @@ -27,7 +27,6 @@ import static android.view.MotionEvent.ACTION_HOVER_EXIT; import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_UP; import static android.view.WindowInsets.Type.statusBars; -import static android.view.WindowManager.TRANSIT_TO_BACK; import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU; import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions; @@ -80,7 +79,6 @@ import android.view.ViewConfiguration; import android.view.ViewRootImpl; import android.window.DesktopModeFlags; import android.window.TaskSnapshot; -import android.window.TransitionInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -127,6 +125,7 @@ import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction; import com.android.wm.shell.desktopmode.common.ToggleTaskSizeUtilsKt; import com.android.wm.shell.desktopmode.education.AppHandleEducationController; import com.android.wm.shell.desktopmode.education.AppToWebEducationController; +import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; import com.android.wm.shell.recents.RecentsTransitionHandler; import com.android.wm.shell.recents.RecentsTransitionStateListener; @@ -212,6 +211,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, private final AppHandleAndHeaderVisibilityHelper mAppHandleAndHeaderVisibilityHelper; private final AppHeaderViewHolder.Factory mAppHeaderViewHolderFactory; private final AppHandleViewHolder.Factory mAppHandleViewHolderFactory; + private final DesksOrganizer mDesksOrganizer; private boolean mTransitionDragActive; private SparseArray<EventReceiver> mEventReceiversByDisplay = new SparseArray<>(); @@ -310,7 +310,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, DesktopModeCompatPolicy desktopModeCompatPolicy, DesktopTilingDecorViewModel desktopTilingDecorViewModel, MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController, - CompatUIHandler compatUI) { + CompatUIHandler compatUI, + DesksOrganizer desksOrganizer) { this( context, shellExecutor, @@ -358,7 +359,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, desktopModeCompatPolicy, desktopTilingDecorViewModel, multiDisplayDragMoveIndicatorController, - compatUI); + compatUI, + desksOrganizer); } @VisibleForTesting @@ -409,7 +411,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, DesktopModeCompatPolicy desktopModeCompatPolicy, DesktopTilingDecorViewModel desktopTilingDecorViewModel, MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController, - CompatUIHandler compatUI) { + CompatUIHandler compatUI, + DesksOrganizer desksOrganizer) { mContext = context; mMainExecutor = shellExecutor; mMainHandler = mainHandler; @@ -487,6 +490,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mDesktopTasksController.setSnapEventHandler(this); mMultiDisplayDragMoveIndicatorController = multiDisplayDragMoveIndicatorController; mLatencyTracker = LatencyTracker.getInstance(mContext); + mDesksOrganizer = desksOrganizer; shellInit.addInitCallback(this::onInit, this); } @@ -525,6 +529,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, }); } mFocusTransitionObserver.setLocalFocusTransitionListener(this, mMainExecutor); + mDesksOrganizer.setOnDesktopTaskInfoChangedListener((taskInfo) -> { + onTaskInfoChanged(taskInfo); + return Unit.INSTANCE; + }); } @Override @@ -602,8 +610,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, RunningTaskInfo taskInfo, SurfaceControl taskSurface, SurfaceControl.Transaction startT, - SurfaceControl.Transaction finishT, - @TransitionInfo.TransitionMode int changeMode) { + SurfaceControl.Transaction finishT) { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); if (!shouldShowWindowDecor(taskInfo)) { if (decoration != null) { @@ -617,8 +624,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, } else { decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */, - mFocusTransitionObserver.hasGlobalFocus(taskInfo), mExclusionRegion, - /*isMovingToBack= */ changeMode == TRANSIT_TO_BACK); + mFocusTransitionObserver.hasGlobalFocus(taskInfo), mExclusionRegion); } } @@ -633,7 +639,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */, mFocusTransitionObserver.hasGlobalFocus(taskInfo), - mExclusionRegion, /* isMovingToBack= */ false); + mExclusionRegion); } @Override @@ -1892,7 +1898,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, windowDecoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */, mFocusTransitionObserver.hasGlobalFocus(taskInfo), - mExclusionRegion, /* isMovingToBack= */ false); + mExclusionRegion); if (!DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) { incrementEventReceiverTasks(taskInfo.displayId); } 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 50bc7b5e865b..d24308137936 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 @@ -217,7 +217,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private boolean mIsDragging = false; private Runnable mLoadAppInfoRunnable; private Runnable mSetAppInfoRunnable; - private boolean mIsMovingToBack; public DesktopModeWindowDecoration( Context context, @@ -479,7 +478,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // causes flickering. See b/270202228. final boolean applyTransactionOnDraw = taskInfo.isFreeform(); relayout(taskInfo, t, t, applyTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop, - hasGlobalFocus, displayExclusionRegion, mIsMovingToBack); + hasGlobalFocus, displayExclusionRegion); if (!applyTransactionOnDraw) { t.apply(); } @@ -506,8 +505,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin void relayout(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, - boolean hasGlobalFocus, @NonNull Region displayExclusionRegion, - boolean isMovingToBack) { + boolean hasGlobalFocus, @NonNull Region displayExclusionRegion) { Trace.beginSection("DesktopModeWindowDecoration#relayout"); if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_APP_TO_WEB.isTrue()) { @@ -530,7 +528,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final boolean inFullImmersive = mDesktopUserRepositories.getProfile(taskInfo.userId) .isTaskInFullImmersiveState(taskInfo.taskId); - mIsMovingToBack = isMovingToBack; updateRelayoutParams(mRelayoutParams, mContext, taskInfo, mSplitScreenController, applyStartTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop, mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded, inFullImmersive, @@ -539,8 +536,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin /* shouldIgnoreCornerRadius= */ mIsRecentsTransitionRunning && DesktopModeFlags .ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX.isTrue(), - mDesktopModeCompatPolicy.shouldExcludeCaptionFromAppBounds(taskInfo), - mIsRecentsTransitionRunning, mIsMovingToBack); + mDesktopModeCompatPolicy.shouldExcludeCaptionFromAppBounds(taskInfo)); final WindowDecorLinearLayout oldRootView = mResult.mRootView; final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; @@ -633,6 +629,16 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mBgExecutor.execute(mLoadAppInfoRunnable); } + private boolean showInputLayer() { + if (!DesktopModeFlags.ENABLE_INPUT_LAYER_TRANSITION_FIX.isTrue()) { + return isCaptionVisible(); + } + // Don't show the input layer during the recents transition, otherwise it could become + // touchable while in overview, during quick-switch or even for a short moment after going + // Home. + return isCaptionVisible() && !mIsRecentsTransitionRunning; + } + private boolean isCaptionVisible() { return mTaskInfo.isVisible && mIsCaptionVisible; } @@ -874,7 +880,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin if (!isAppHandle(mWindowDecorViewHolder)) return; asAppHandle(mWindowDecorViewHolder).bindData(new AppHandleViewHolder.HandleData( mTaskInfo, determineHandlePosition(), mResult.mCaptionWidth, - mResult.mCaptionHeight, /* showInputLayer= */ isCaptionVisible(), + mResult.mCaptionHeight, /* showInputLayer= */ showInputLayer(), /* isCaptionVisible= */ isCaptionVisible() )); } @@ -959,9 +965,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin boolean hasGlobalFocus, @NonNull Region displayExclusionRegion, boolean shouldIgnoreCornerRadius, - boolean shouldExcludeCaptionFromAppBounds, - boolean isRecentsTransitionRunning, - boolean isMovingToBack) { + boolean shouldExcludeCaptionFromAppBounds) { final int captionLayoutId = getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode()); final boolean isAppHeader = captionLayoutId == R.layout.desktop_mode_app_header; @@ -979,19 +983,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin relayoutParams.mAsyncViewHost = isAppHandle; boolean showCaption; - // If this relayout is occurring from an observed TRANSIT_TO_BACK transition, do not - // show caption (this includes split select transition). - if (DesktopModeFlags.ENABLE_INPUT_LAYER_TRANSITION_FIX.isTrue() - && isMovingToBack && !isDragging) { - showCaption = false; - } else if (DesktopModeFlags.ENABLE_DESKTOP_IMMERSIVE_DRAG_BUGFIX.isTrue() && isDragging) { + if (DesktopModeFlags.ENABLE_DESKTOP_IMMERSIVE_DRAG_BUGFIX.isTrue() && isDragging) { // If the task is being dragged, the caption should not be hidden so that it continues // receiving input showCaption = true; - } else if (DesktopModeFlags.ENABLE_INPUT_LAYER_TRANSITION_FIX.isTrue() - && isRecentsTransitionRunning) { - // Caption should not be visible in recents. - showCaption = false; } else if (DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()) { if (inFullImmersiveMode) { showCaption = (isStatusBarVisible && !isKeyguardVisibleAndOccluded); @@ -1895,18 +1890,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * <p> When a Recents transition is active we allow that transition to take ownership of the * corner radius of its task surfaces, so each window decoration should stop updating the corner * radius of its task surface during that time. - * - * We should not allow input to reach the input layer during a Recents transition, so - * update the handle view holder accordingly if transition status changes. */ void setIsRecentsTransitionRunning(boolean isRecentsTransitionRunning) { - if (mIsRecentsTransitionRunning != isRecentsTransitionRunning) { - mIsRecentsTransitionRunning = isRecentsTransitionRunning; - if (DesktopModeFlags.ENABLE_INPUT_LAYER_TRANSITION_FIX.isTrue()) { - // We don't relayout decor on recents transition, so we need to call it directly. - relayout(mTaskInfo, mHasGlobalFocus, mRelayoutParams.mDisplayExclusionRegion); - } - } + mIsRecentsTransitionRunning = isRecentsTransitionRunning; } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java index 8a8bdcadd67a..97a47c602bcd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java @@ -435,7 +435,9 @@ class DragResizeInputListener implements AutoCloseable { } // Removing this surface on the background thread to ensure that mInitInputChannels has // already been finished. - mSurfaceControlTransactionSupplier.get().remove(mDecorationSurface).apply(); + // Do not |remove| the surface, the decoration might still be needed even if + // drag-resizing isn't. + mDecorationSurface.release(); }); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt index cdadce57d610..71bb153e4b1e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -508,6 +508,9 @@ class HandleMenu( private val iconButtonRippleRadius = context.resources.getDimensionPixelSize( R.dimen.desktop_mode_handle_menu_icon_button_ripple_radius ) + private val handleMenuCornerRadius = context.resources.getDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_corner_radius + ) private val iconButtonDrawableInsetsBase = DrawableInsets( t = iconButtondrawableBaseInset, b = iconButtondrawableBaseInset, l = iconButtondrawableBaseInset, @@ -866,14 +869,21 @@ class HandleMenu( private fun bindMoreActionsPill(style: MenuStyle) { moreActionsPill.background.setTint(style.backgroundColor) - - arrayOf( + val buttons = arrayOf( screenshotBtn to SHOULD_SHOW_SCREENSHOT_BUTTON, newWindowBtn to shouldShowNewWindowButton, manageWindowBtn to shouldShowManageWindowsButton, changeAspectRatioBtn to shouldShowChangeAspectRatioButton, restartBtn to shouldShowRestartButton, - ).forEach { (button, shouldShow) -> + ) + val firstVisible = buttons.find { it.second }?.first + val lastVisible = buttons.findLast { it.second }?.first + + buttons.forEach { (button, shouldShow) -> + val topRadius = + if (button == firstVisible) handleMenuCornerRadius.toFloat() else 0f + val bottomRadius = + if (button == lastVisible) handleMenuCornerRadius.toFloat() else 0f button.apply { isGone = !shouldShow textView.apply { @@ -881,6 +891,13 @@ class HandleMenu( startMarquee() } iconView.imageTintList = ColorStateList.valueOf(style.textColor) + background = createBackgroundDrawable( + color = style.textColor, + cornerRadius = floatArrayOf( + topRadius, topRadius, topRadius, topRadius, + bottomRadius, bottomRadius, bottomRadius, bottomRadius + ), + drawableInsets = DrawableInsets()) } } } @@ -899,6 +916,10 @@ class HandleMenu( openInAppOrBrowserBtn.apply { contentDescription = btnText + background = createBackgroundDrawable( + color = style.textColor, + cornerRadius = handleMenuCornerRadius, + drawableInsets = DrawableInsets()) textView.apply { text = btnText setTextColor(style.textColor) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java index 5e4a0a5860f0..1563259f4a1a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java @@ -18,7 +18,6 @@ package com.android.wm.shell.windowdecor; import android.app.ActivityManager; import android.view.SurfaceControl; -import android.window.TransitionInfo; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; import com.android.wm.shell.splitscreen.SplitScreenController; @@ -84,14 +83,12 @@ public interface WindowDecorViewModel { * @param taskSurface the surface of the task * @param startT the start transaction to be applied before the transition * @param finishT the finish transaction to restore states after the transition - * @param changeMode the type of change to the task */ void onTaskChanging( ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, SurfaceControl.Transaction startT, - SurfaceControl.Transaction finishT, - @TransitionInfo.TransitionMode int changeMode); + SurfaceControl.Transaction finishT); /** * Notifies that the given task is about to close to give the window decoration a chance to diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/AppHandleAndHeaderVisibilityHelper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/AppHandleAndHeaderVisibilityHelper.kt index 39ccf5bd03a7..950eeccf6a4a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/AppHandleAndHeaderVisibilityHelper.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/AppHandleAndHeaderVisibilityHelper.kt @@ -23,6 +23,7 @@ import android.view.WindowManager import android.window.DesktopExperienceFlags.ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY import com.android.wm.shell.common.DisplayController import com.android.wm.shell.desktopmode.DesktopWallpaperActivity.Companion.isWallpaperTask +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.splitscreen.SplitScreenController @@ -52,7 +53,8 @@ class AppHandleAndHeaderVisibilityHelper ( private fun allowedForTask(taskInfo: ActivityManager.RunningTaskInfo): Boolean { // TODO (b/382023296): Remove once we no longer rely on // Flags.enableBugFixesForSecondaryDisplay as it is taken care of in #allowedForDisplay - if (displayController.getDisplay(taskInfo.displayId) == null) { + val display = displayController.getDisplay(taskInfo.displayId) + if (display == null) { // If DisplayController doesn't have it tracked, it could be a private/managed display. return false } @@ -68,8 +70,7 @@ class AppHandleAndHeaderVisibilityHelper ( // TODO (b/382023296): Remove once we no longer rely on // Flags.enableBugFixesForSecondaryDisplay as it is taken care of in #allowedForDisplay val isOnLargeScreen = - displayController.getDisplay(taskInfo.displayId).minSizeDimensionDp >= - WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP + display.minSizeDimensionDp >= WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP if (!DesktopModeStatus.canEnterDesktopMode(context) && DesktopModeStatus.overridesShowAppHandle(context) && !isOnLargeScreen @@ -78,6 +79,14 @@ class AppHandleAndHeaderVisibilityHelper ( // small screens return false } + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen() + && !DesktopModeStatus.isDesktopModeSupportedOnDisplay(context, display) + ) { + // TODO(b/388853233): enable handles for split tasks once drag to bubble is enabled + if (taskInfo.windowingMode != WindowConfiguration.WINDOWING_MODE_FULLSCREEN) { + return false + } + } return DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context) && !isWallpaperTask(taskInfo) && taskInfo.windowingMode != WindowConfiguration.WINDOWING_MODE_PINNED diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt index f08cfa987cc7..33e743016d0d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt @@ -51,10 +51,20 @@ fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int { */ fun createBackgroundDrawable( @ColorInt color: Int, cornerRadius: Int, drawableInsets: DrawableInsets +): Drawable = createBackgroundDrawable( + color, + FloatArray(8) { cornerRadius.toFloat() }, + drawableInsets) + +/** + * Creates a background drawable with specified color, corner radius, and insets. + */ +fun createBackgroundDrawable( + @ColorInt color: Int, cornerRadius: FloatArray, drawableInsets: DrawableInsets ): Drawable = LayerDrawable(arrayOf( ShapeDrawable().apply { shape = RoundRectShape( - FloatArray(8) { cornerRadius.toFloat() }, + cornerRadius, /* inset= */ null, /* innerRadii= */ null ) diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/Android.bp b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/Android.bp index 7585c977809e..50581f7e01f3 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/Android.bp +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/Android.bp @@ -33,7 +33,6 @@ android_test { "WMShellFlickerTestsBase", "WMShellScenariosDesktopMode", "WMShellTestUtils", - "ui-trace-collector", ], data: ["trace_config/*"], } diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt index 7d9f2bf8fdf6..05ddb4043b82 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt @@ -19,7 +19,7 @@ package com.android.wm.shell.scenarios import android.app.Instrumentation import android.tools.NavBar import android.tools.Rotation -import android.tools.device.apphelpers.GmailAppHelper +import android.tools.device.apphelpers.CalculatorAppHelper import android.tools.flicker.rules.ChangeDisplayOrientationRule import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry @@ -44,8 +44,9 @@ abstract class UnminimizeAppFromTaskbar(val rotation: Rotation = Rotation.ROTATI private val wmHelper = WindowManagerStateHelper(instrumentation) private val device = UiDevice.getInstance(instrumentation) private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) - private val gmailHelper = GmailAppHelper(instrumentation) - private val gmailApp = DesktopModeAppHelper(gmailHelper) + private val calculatorHelper = CalculatorAppHelper(instrumentation) + private val calculatorApp = DesktopModeAppHelper(calculatorHelper) + @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) @@ -59,20 +60,20 @@ abstract class UnminimizeAppFromTaskbar(val rotation: Rotation = Rotation.ROTATI ChangeDisplayOrientationRule.setRotation(rotation) testApp.enterDesktopMode(wmHelper, device) tapl.showTaskbarIfHidden() - gmailApp.launchViaIntent(wmHelper) - gmailApp.minimizeDesktopApp(wmHelper, device) + calculatorApp.launchViaIntent(wmHelper) + calculatorApp.minimizeDesktopApp(wmHelper, device) } @Test open fun unminimizeApp() { tapl.launchedAppState.taskbar - .getAppIcon(gmailHelper.appName) - .launch(gmailHelper.packageName) + .getAppIcon(calculatorHelper.appName) + .launch(calculatorApp.packageName) } @After fun teardown() { testApp.exit(wmHelper) - gmailApp.exit(wmHelper) + calculatorApp.exit(wmHelper) } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt index fa9864b539ee..7cd2bcc8efd4 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt @@ -52,7 +52,7 @@ abstract class SwitchAppByDoubleTapDividerBenchmark(override val flicker: Legacy ) } transitions { - SplitScreenUtils.doubleTapDividerToSwitch(device) + SplitScreenUtils.doubleTapDividerToSwitch(device, instrumentation.uiAutomation) wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() waitForLayersToSwitch(wmHelper) diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml index 7659ec903480..8cbec687d8d8 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml @@ -74,10 +74,6 @@ <option name="shell-timeout" value="6600s"/> <option name="test-timeout" value="6000s"/> <option name="hidden-api-checks" value="false"/> - <option name="device-listeners" value="android.tools.collectors.DefaultUITraceListener"/> - <!-- DefaultUITraceListener args --> - <option name="instrumentation-arg" key="per_run" value="true"/> - <option name="instrumentation-arg" key="perfetto_persist_pid_track" value="true"/> </test> <!-- Needed for pulling the collected trace config on to the host --> <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/SwitchAppByDoubleTapDivider.kt index 3fd93d3eaf59..dfc737174a4f 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/SwitchAppByDoubleTapDivider.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/SwitchAppByDoubleTapDivider.kt @@ -61,7 +61,7 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { @Test open fun switchAppByDoubleTapDivider() { - SplitScreenUtils.doubleTapDividerToSwitch(device) + SplitScreenUtils.doubleTapDividerToSwitch(device, instrumentation.uiAutomation) wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() waitForLayersToSwitch(wmHelper) diff --git a/libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/SimulatedConnectedDisplayTestRule.kt b/libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/SimulatedConnectedDisplayTestRule.kt index 68f7ef09ee70..f9b69d3f5f7e 100644 --- a/libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/SimulatedConnectedDisplayTestRule.kt +++ b/libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/SimulatedConnectedDisplayTestRule.kt @@ -41,7 +41,6 @@ import org.junit.runners.model.Statement class SimulatedConnectedDisplayTestRule : TestRule { private val context = InstrumentationRegistry.getInstrumentation().targetContext - private val uiAutomation = InstrumentationRegistry.getInstrumentation().uiAutomation private val displayManager = context.getSystemService(DisplayManager::class.java) private val addedDisplays = mutableListOf<Int>() @@ -102,7 +101,8 @@ class SimulatedConnectedDisplayTestRule : TestRule { // Add the overlay displays Settings.Global.putString( InstrumentationRegistry.getInstrumentation().context.contentResolver, - Settings.Global.OVERLAY_DISPLAY_DEVICES, displaySettings + Settings.Global.OVERLAY_DISPLAY_DEVICES, + displaySettings ) withTimeoutOrNull(TIMEOUT) { displayAddedFlow.take(displays.size).collect { displayId -> @@ -125,10 +125,6 @@ class SimulatedConnectedDisplayTestRule : TestRule { } private fun cleanupTestDisplays() = runBlocking { - if (addedDisplays.isEmpty()) { - return@runBlocking - } - val displayRemovedFlow: Flow<Int> = callbackFlow { val listener = object : DisplayListener { override fun onDisplayAdded(displayId: Int) {} @@ -146,16 +142,24 @@ class SimulatedConnectedDisplayTestRule : TestRule { } } - // Remove overlay displays + // Remove overlay displays. We'll execute this regardless of addedDisplays just to + // ensure all overlay displays are removed before and after the test. + // Note: If we want to restore the original overlay display added before this test (and its + // topology), it will be complicated as re-adding overlay display would lead to different + // displayId and topology could not be restored easily. Settings.Global.putString( InstrumentationRegistry.getInstrumentation().context.contentResolver, - Settings.Global.OVERLAY_DISPLAY_DEVICES, null) + Settings.Global.OVERLAY_DISPLAY_DEVICES, + null + ) - withTimeoutOrNull(TIMEOUT) { - displayRemovedFlow.take(addedDisplays.size).collect { displayId -> - addedDisplays.remove(displayId) - } - } ?: error("Timed out waiting for displays to be removed.") + if (!addedDisplays.isEmpty()) { + withTimeoutOrNull(TIMEOUT) { + displayRemovedFlow.take(addedDisplays.size).collect { displayId -> + addedDisplays.remove(displayId) + } + } ?: error("Timed out waiting for displays to be removed: $addedDisplays") + } } private companion object { diff --git a/libs/WindowManager/Shell/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp index 7b6cfe3f9f8a..98b0bd0b589d 100644 --- a/libs/WindowManager/Shell/tests/flicker/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/Android.bp @@ -38,15 +38,15 @@ java_library { ], static_libs: [ "androidx.test.ext.junit", - "flickertestapplib", + "com_android_wm_shell_flags_lib", "flickerlib", "flickerlib-helpers", + "flickertestapplib", + "launcher-aosp-tapl", + "launcher-helper-lib", "platform-test-annotations", "wm-flicker-common-app-helpers", "wm-flicker-common-assertions", - "launcher-helper-lib", - "launcher-aosp-tapl", - "com_android_wm_shell_flags_lib", ], } @@ -60,18 +60,18 @@ java_defaults { test_suites: ["device-tests"], libs: ["android.test.runner.stubs.system"], static_libs: [ - "wm-shell-flicker-utils", "androidx.test.ext.junit", - "flickertestapplib", "flickerlib", "flickerlib-helpers", "flickerlib-trace_processor_shell", + "flickertestapplib", + "launcher-aosp-tapl", + "launcher-helper-lib", "platform-test-annotations", "platform-test-rules", "wm-flicker-common-app-helpers", "wm-flicker-common-assertions", - "launcher-helper-lib", - "launcher-aosp-tapl", + "wm-shell-flicker-utils", ], data: [ ":FlickerTestApp", diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt index e4183f16ba14..e54930d730f3 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.flicker.utils import android.app.Instrumentation +import android.app.UiAutomation import android.content.Context import android.graphics.Point import android.os.SystemClock @@ -355,13 +356,40 @@ object SplitScreenUtils { ) } - fun doubleTapDividerToSwitch(device: UiDevice) { + fun doubleTapDividerToSwitch(device: UiDevice, uiAutomation: UiAutomation) { val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS) - val interval = - (ViewConfiguration.getDoubleTapTimeout() + ViewConfiguration.getDoubleTapMinTime()) / 2 - dividerBar.click() - SystemClock.sleep(interval.toLong()) - dividerBar.click() + val x = dividerBar.visibleCenter.x.toFloat() + val y = dividerBar.visibleCenter.y.toFloat() + + // To send a double-tap action, we set a DOWN event, then UP, then DOWN, then, UP. + val startTime = SystemClock.uptimeMillis() + val timeOfFirstUp = startTime + ViewConfiguration.getTapTimeout() + // Between the two taps, we wait an arbitrary amount of time between the min and max times + // for a double-tap. + val timeOfSecondDown = timeOfFirstUp + ViewConfiguration.getDoubleTapMinTime() + + ((ViewConfiguration.getDoubleTapTimeout() - + ViewConfiguration.getDoubleTapMinTime()) / 4) + val timeOfSecondUp = timeOfSecondDown + ViewConfiguration.getTapTimeout() + + val downEvent = MotionEvent.obtain(startTime, startTime, MotionEvent.ACTION_DOWN, x, y, + 0 /* metaState */) + downEvent.setSource(InputDevice.SOURCE_TOUCHSCREEN) + uiAutomation.injectInputEvent(downEvent, true) + + val upEvent = MotionEvent.obtain(startTime, timeOfFirstUp, MotionEvent.ACTION_UP, x, y, + 0 /* metaState */) + upEvent.setSource(InputDevice.SOURCE_TOUCHSCREEN) + uiAutomation.injectInputEvent(upEvent, true) + + val downEvent2 = MotionEvent.obtain(timeOfSecondDown, timeOfSecondDown, + MotionEvent.ACTION_DOWN, x, y, 0 /* metaState */) + downEvent2.setSource(InputDevice.SOURCE_TOUCHSCREEN) + uiAutomation.injectInputEvent(downEvent2, true) + + val upEvent2 = MotionEvent.obtain(timeOfSecondDown, timeOfSecondUp, MotionEvent.ACTION_UP, + x, y, 0 /* metaState */) + upEvent2.setSource(InputDevice.SOURCE_TOUCHSCREEN) + uiAutomation.injectInputEvent(upEvent2, true) } fun copyContentInSplit( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt index 9268db60aa51..42dcaf4b4f33 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt @@ -23,6 +23,7 @@ import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.ExtendedMockito.never import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.server.display.feature.flags.Flags as DisplayFlags import com.android.window.flags.Flags import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTestCase @@ -50,6 +51,7 @@ import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import org.mockito.quality.Strictness @@ -212,12 +214,15 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun testUserChanged_createsDeskWhenNeeded() = testScope.runTest { + val userId = 11 whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) val userChangeListenerCaptor = argumentCaptor<UserChangeListener>() verify(mockShellController).addUserChangeListener(userChangeListenerCaptor.capture()) - whenever(mockDesktopRepository.getNumberOfDesks(displayId = 2)).thenReturn(0) - whenever(mockDesktopRepository.getNumberOfDesks(displayId = 3)).thenReturn(0) - whenever(mockDesktopRepository.getNumberOfDesks(displayId = 4)).thenReturn(1) + val mockRepository = mock<DesktopRepository>() + whenever(mockDesktopUserRepositories.getProfile(userId)).thenReturn(mockRepository) + whenever(mockRepository.getNumberOfDesks(displayId = 2)).thenReturn(0) + whenever(mockRepository.getNumberOfDesks(displayId = 3)).thenReturn(0) + whenever(mockRepository.getNumberOfDesks(displayId = 4)).thenReturn(1) whenever(mockRootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(2, 3, 4)) desktopRepositoryInitializer.initialize(mockDesktopUserRepositories) handler.onDisplayAdded(displayId = 2) @@ -226,7 +231,7 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { runCurrent() clearInvocations(mockDesktopTasksController) - userChangeListenerCaptor.lastValue.onUserChanged(1, context) + userChangeListenerCaptor.lastValue.onUserChanged(userId, context) runCurrent() verify(mockDesktopTasksController).createDesk(displayId = 2) @@ -237,13 +242,22 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { @Test fun testConnectExternalDisplay() { onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(externalDisplayId) - verify(desktopDisplayModeController).refreshDisplayWindowingMode() + verify(desktopDisplayModeController).updateExternalDisplayWindowingMode(externalDisplayId) + verify(desktopDisplayModeController).updateDefaultDisplayWindowingMode() } @Test fun testDisconnectExternalDisplay() { onDisplaysChangedListenerCaptor.lastValue.onDisplayRemoved(externalDisplayId) - verify(desktopDisplayModeController).refreshDisplayWindowingMode() + verify(desktopDisplayModeController).updateDefaultDisplayWindowingMode() + } + + @Test + @EnableFlags(DisplayFlags.FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT) + fun testDesktopModeEligibleChanged() { + onDisplaysChangedListenerCaptor.lastValue.onDesktopModeEligibleChanged(externalDisplayId) + verify(desktopDisplayModeController).updateExternalDisplayWindowingMode(externalDisplayId) + verify(desktopDisplayModeController).updateDefaultDisplayWindowingMode() } private class FakeDesktopRepositoryInitializer : DesktopRepositoryInitializer { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt index 96b826f93aae..7e9ee34c8f68 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt @@ -101,10 +101,13 @@ class DesktopDisplayModeControllerTest( private val fullscreenTask = TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FULLSCREEN).build() private val defaultTDA = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) + private val externalTDA = DisplayAreaInfo(MockToken().token(), EXTERNAL_DISPLAY_ID, 0) private val wallpaperToken = MockToken().token() private val defaultDisplay = mock<Display>() private val externalDisplay = mock<Display>() - private val mouseDevice = mock<InputDevice>() + private val touchpadDevice = mock<InputDevice>() + private val keyboardDevice = mock<InputDevice>() + private val connectedDeviceIds = mutableListOf<Int>() private lateinit var extendedDisplaySettingsRestoreSession: ExtendedDisplaySettingsRestoreSession @@ -127,6 +130,8 @@ class DesktopDisplayModeControllerTest( whenever(transitions.startTransition(anyInt(), any(), isNull())).thenReturn(Binder()) whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) .thenReturn(defaultTDA) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(EXTERNAL_DISPLAY_ID)) + .thenReturn(externalTDA) controller = DesktopDisplayModeController( context, @@ -145,16 +150,18 @@ class DesktopDisplayModeControllerTest( whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(wallpaperToken) whenever(displayController.getDisplay(DEFAULT_DISPLAY)).thenReturn(defaultDisplay) whenever(displayController.getDisplay(EXTERNAL_DISPLAY_ID)).thenReturn(externalDisplay) - setTabletModeStatus(SwitchState.UNKNOWN) - whenever( - DesktopModeStatus.isDesktopModeSupportedOnDisplay( - context, - defaultDisplay - ) - ).thenReturn(true) - whenever(mouseDevice.supportsSource(InputDevice.SOURCE_MOUSE)).thenReturn(true) - whenever(inputManager.getInputDevice(EXTERNAL_DEVICE_ID)).thenReturn(mouseDevice) - setMouseConnected(false) + whenever(DesktopModeStatus.isDesktopModeSupportedOnDisplay(context, defaultDisplay)) + .thenReturn(true) + whenever(touchpadDevice.supportsSource(InputDevice.SOURCE_TOUCHPAD)).thenReturn(true) + whenever(touchpadDevice.isEnabled()).thenReturn(true) + whenever(inputManager.getInputDevice(TOUCHPAD_DEVICE_ID)).thenReturn(touchpadDevice) + whenever(keyboardDevice.isFullKeyboard()).thenReturn(true) + whenever(keyboardDevice.isVirtual()).thenReturn(false) + whenever(keyboardDevice.isEnabled()).thenReturn(true) + whenever(inputManager.getInputDevice(KEYBOARD_DEVICE_ID)).thenReturn(keyboardDevice) + whenever(inputManager.inputDeviceIds).thenAnswer { connectedDeviceIds.toIntArray() } + setTouchpadConnected(false) + setKeyboardConnected(false) } @After @@ -211,8 +218,8 @@ class DesktopDisplayModeControllerTest( @DisableFlags(Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH) fun testTargetWindowingMode_formfactorDisabled( @TestParameter param: ExternalDisplayBasedTargetModeTestCase, - @TestParameter tabletModeStatus: SwitchState, - @TestParameter hasAnyMouseDevice: Boolean, + @TestParameter hasAnyTouchpadDevice: Boolean, + @TestParameter hasAnyKeyboardDevice: Boolean, ) { whenever(mockWindowManager.getWindowingMode(anyInt())) .thenReturn(param.defaultWindowingMode) @@ -221,15 +228,11 @@ class DesktopDisplayModeControllerTest( } else { disconnectExternalDisplay() } - setTabletModeStatus(tabletModeStatus) - setMouseConnected(hasAnyMouseDevice) + setTouchpadConnected(hasAnyTouchpadDevice) + setKeyboardConnected(hasAnyKeyboardDevice) setExtendedMode(param.extendedDisplayEnabled) - whenever( - DesktopModeStatus.isDesktopModeSupportedOnDisplay( - context, - defaultDisplay - ) - ).thenReturn(param.isDefaultDisplayDesktopEligible) + whenever(DesktopModeStatus.isDesktopModeSupportedOnDisplay(context, defaultDisplay)) + .thenReturn(param.isDefaultDisplayDesktopEligible) assertThat(controller.getTargetWindowingModeForDefaultDisplay()) .isEqualTo(param.expectedWindowingMode) @@ -246,15 +249,11 @@ class DesktopDisplayModeControllerTest( } else { disconnectExternalDisplay() } - setTabletModeStatus(param.tabletModeStatus) setExtendedMode(param.extendedDisplayEnabled) - whenever( - DesktopModeStatus.isDesktopModeSupportedOnDisplay( - context, - defaultDisplay - ) - ).thenReturn(param.isDefaultDisplayDesktopEligible) - setMouseConnected(param.hasAnyMouseDevice) + whenever(DesktopModeStatus.isDesktopModeSupportedOnDisplay(context, defaultDisplay)) + .thenReturn(param.isDefaultDisplayDesktopEligible) + setTouchpadConnected(param.hasAnyTouchpadDevice) + setKeyboardConnected(param.hasAnyKeyboardDevice) assertThat(controller.getTargetWindowingModeForDefaultDisplay()) .isEqualTo(param.expectedWindowingMode) @@ -296,30 +295,36 @@ class DesktopDisplayModeControllerTest( .isEqualTo(WINDOWING_MODE_UNDEFINED) } + @Test + @EnableFlags(DisplayFlags.FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT) + fun externalDisplayWindowingMode() { + externalTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + setExtendedMode(true) + + controller.updateExternalDisplayWindowingMode(EXTERNAL_DISPLAY_ID) + + val arg = argumentCaptor<WindowContainerTransaction>() + verify(transitions, times(1)).startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull()) + assertThat(arg.firstValue.changes[externalTDA.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + private fun connectExternalDisplay() { whenever(rootTaskDisplayAreaOrganizer.getDisplayIds()) .thenReturn(intArrayOf(DEFAULT_DISPLAY, EXTERNAL_DISPLAY_ID)) - controller.refreshDisplayWindowingMode() + controller.updateDefaultDisplayWindowingMode() } private fun disconnectExternalDisplay() { whenever(rootTaskDisplayAreaOrganizer.getDisplayIds()) .thenReturn(intArrayOf(DEFAULT_DISPLAY)) - controller.refreshDisplayWindowingMode() - } - - private fun setTabletModeStatus(status: SwitchState) { - whenever(inputManager.isInTabletMode()).thenReturn(status.value) + controller.updateDefaultDisplayWindowingMode() } private fun setExtendedMode(enabled: Boolean) { if (DisplayFlags.enableDisplayContentModeManagement()) { - whenever( - DesktopModeStatus.isDesktopModeSupportedOnDisplay( - context, - externalDisplay - ) - ).thenReturn(enabled) + whenever(DesktopModeStatus.isDesktopModeSupportedOnDisplay(context, externalDisplay)) + .thenReturn(enabled) } else { Settings.Global.putInt( context.contentResolver, @@ -329,9 +334,20 @@ class DesktopDisplayModeControllerTest( } } - private fun setMouseConnected(connected: Boolean) { - whenever(inputManager.inputDeviceIds) - .thenReturn(if (connected) intArrayOf(EXTERNAL_DEVICE_ID) else intArrayOf()) + private fun setTouchpadConnected(connected: Boolean) { + if (connected) { + connectedDeviceIds.add(TOUCHPAD_DEVICE_ID) + } else { + connectedDeviceIds.remove(TOUCHPAD_DEVICE_ID) + } + } + + private fun setKeyboardConnected(connected: Boolean) { + if (connected) { + connectedDeviceIds.add(KEYBOARD_DEVICE_ID) + } else { + connectedDeviceIds.remove(KEYBOARD_DEVICE_ID) + } } private class ExtendedDisplaySettingsRestoreSession( @@ -358,13 +374,8 @@ class DesktopDisplayModeControllerTest( companion object { const val EXTERNAL_DISPLAY_ID = 100 - const val EXTERNAL_DEVICE_ID = 10 - - enum class SwitchState(val value: Int) { - UNKNOWN(InputManager.SWITCH_STATE_UNKNOWN), - ON(InputManager.SWITCH_STATE_ON), - OFF(InputManager.SWITCH_STATE_OFF), - } + const val TOUCHPAD_DEVICE_ID = 10 + const val KEYBOARD_DEVICE_ID = 11 enum class ExternalDisplayBasedTargetModeTestCase( val defaultWindowingMode: Int, @@ -490,393 +501,265 @@ class DesktopDisplayModeControllerTest( enum class FormFactorBasedTargetModeTestCase( val hasExternalDisplay: Boolean, val extendedDisplayEnabled: Boolean, - val tabletModeStatus: SwitchState, val isDefaultDisplayDesktopEligible: Boolean, - val hasAnyMouseDevice: Boolean, + val hasAnyTouchpadDevice: Boolean, + val hasAnyKeyboardDevice: Boolean, val expectedWindowingMode: Int, ) { - EXTERNAL_EXTENDED_TABLET_NO_PROJECTED_NO_MOUSE( - hasExternalDisplay = true, - extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.ON, - isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = false, - expectedWindowingMode = WINDOWING_MODE_FREEFORM, - ), - NO_EXTERNAL_EXTENDED_TABLET_NO_PROJECTED_NO_MOUSE( - hasExternalDisplay = false, - extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.ON, - isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = false, - expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, - ), - EXTERNAL_MIRROR_TABLET_NO_PROJECTED_NO_MOUSE( - hasExternalDisplay = true, - extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.ON, - isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = false, - expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, - ), - NO_EXTERNAL_MIRROR_TABLET_NO_PROJECTED_NO_MOUSE( - hasExternalDisplay = false, - extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.ON, - isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = false, - expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, - ), - EXTERNAL_EXTENDED_CLAMSHELL_NO_PROJECTED_NO_MOUSE( + EXTERNAL_EXTENDED_NO_PROJECTED_TOUCHPAD_KEYBOARD( hasExternalDisplay = true, extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.OFF, isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = false, + hasAnyTouchpadDevice = true, + hasAnyKeyboardDevice = true, expectedWindowingMode = WINDOWING_MODE_FREEFORM, ), - NO_EXTERNAL_EXTENDED_CLAMSHELL_NO_PROJECTED_NO_MOUSE( + NO_EXTERNAL_EXTENDED_NO_PROJECTED_TOUCHPAD_KEYBOARD( hasExternalDisplay = false, extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.OFF, isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = false, + hasAnyTouchpadDevice = true, + hasAnyKeyboardDevice = true, expectedWindowingMode = WINDOWING_MODE_FREEFORM, ), - EXTERNAL_MIRROR_CLAMSHELL_NO_PROJECTED_NO_MOUSE( + EXTERNAL_MIRROR_NO_PROJECTED_TOUCHPAD_KEYBOARD( hasExternalDisplay = true, extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.OFF, isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = false, + hasAnyTouchpadDevice = true, + hasAnyKeyboardDevice = true, expectedWindowingMode = WINDOWING_MODE_FREEFORM, ), - NO_EXTERNAL_MIRROR_CLAMSHELL_NO_PROJECTED_NO_MOUSE( + NO_EXTERNAL_MIRROR_NO_PROJECTED_TOUCHPAD_KEYBOARD( hasExternalDisplay = false, extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.OFF, isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = false, + hasAnyTouchpadDevice = true, + hasAnyKeyboardDevice = true, expectedWindowingMode = WINDOWING_MODE_FREEFORM, ), - EXTERNAL_EXTENDED_UNKNOWN_NO_PROJECTED_NO_MOUSE( + EXTERNAL_EXTENDED_PROJECTED_TOUCHPAD_KEYBOARD( hasExternalDisplay = true, extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.UNKNOWN, - isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = false, - expectedWindowingMode = WINDOWING_MODE_FREEFORM, - ), - NO_EXTERNAL_EXTENDED_UNKNOWN_NO_PROJECTED_NO_MOUSE( - hasExternalDisplay = false, - extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.UNKNOWN, - isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = false, - expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, - ), - EXTERNAL_MIRROR_UNKNOWN_NO_PROJECTED_NO_MOUSE( - hasExternalDisplay = true, - extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.UNKNOWN, - isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = false, - expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, - ), - NO_EXTERNAL_MIRROR_UNKNOWN_NO_PROJECTED_NO_MOUSE( - hasExternalDisplay = false, - extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.UNKNOWN, - isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = false, - expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, - ), - EXTERNAL_EXTENDED_TABLET_PROJECTED_NO_MOUSE( - hasExternalDisplay = true, - extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.ON, isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = false, + hasAnyTouchpadDevice = true, + hasAnyKeyboardDevice = true, expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - NO_EXTERNAL_EXTENDED_TABLET_PROJECTED_NO_MOUSE( + NO_EXTERNAL_EXTENDED_PROJECTED_TOUCHPAD_KEYBOARD( hasExternalDisplay = false, extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.ON, isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = false, + hasAnyTouchpadDevice = true, + hasAnyKeyboardDevice = true, expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - EXTERNAL_MIRROR_TABLET_PROJECTED_NO_MOUSE( + EXTERNAL_MIRROR_PROJECTED_TOUCHPAD_KEYBOARD( hasExternalDisplay = true, extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.ON, isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = false, + hasAnyTouchpadDevice = true, + hasAnyKeyboardDevice = true, expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - NO_EXTERNAL_MIRROR_TABLET_PROJECTED_NO_MOUSE( + NO_EXTERNAL_MIRROR_PROJECTED_TOUCHPAD_KEYBOARD( hasExternalDisplay = false, extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.ON, isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = false, + hasAnyTouchpadDevice = true, + hasAnyKeyboardDevice = true, expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - EXTERNAL_EXTENDED_CLAMSHELL_PROJECTED_NO_MOUSE( + EXTERNAL_EXTENDED_NO_PROJECTED_NO_TOUCHPAD_KEYBOARD( hasExternalDisplay = true, extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.OFF, - isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = false, - expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, + isDefaultDisplayDesktopEligible = true, + hasAnyTouchpadDevice = false, + hasAnyKeyboardDevice = true, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, ), - NO_EXTERNAL_EXTENDED_CLAMSHELL_PROJECTED_NO_MOUSE( + NO_EXTERNAL_EXTENDED_NO_PROJECTED_NO_TOUCHPAD_KEYBOARD( hasExternalDisplay = false, extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.OFF, - isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = false, + isDefaultDisplayDesktopEligible = true, + hasAnyTouchpadDevice = false, + hasAnyKeyboardDevice = true, expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - EXTERNAL_MIRROR_CLAMSHELL_PROJECTED_NO_MOUSE( + EXTERNAL_MIRROR_NO_PROJECTED_NO_TOUCHPAD_KEYBOARD( hasExternalDisplay = true, extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.OFF, - isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = false, + isDefaultDisplayDesktopEligible = true, + hasAnyTouchpadDevice = false, + hasAnyKeyboardDevice = true, expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - NO_EXTERNAL_MIRROR_CLAMSHELL_PROJECTED_NO_MOUSE( + NO_EXTERNAL_MIRROR_NO_PROJECTED_NO_TOUCHPAD_KEYBOARD( hasExternalDisplay = false, extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.OFF, - isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = false, + isDefaultDisplayDesktopEligible = true, + hasAnyTouchpadDevice = false, + hasAnyKeyboardDevice = true, expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - EXTERNAL_EXTENDED_UNKNOWN_PROJECTED_NO_MOUSE( + EXTERNAL_EXTENDED_PROJECTED_NO_TOUCHPAD_KEYBOARD( hasExternalDisplay = true, extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.UNKNOWN, isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = false, + hasAnyTouchpadDevice = false, + hasAnyKeyboardDevice = true, expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - NO_EXTERNAL_EXTENDED_UNKNOWN_PROJECTED_NO_MOUSE( + NO_EXTERNAL_EXTENDED_PROJECTED_NO_TOUCHPAD_KEYBOARD( hasExternalDisplay = false, extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.UNKNOWN, isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = false, + hasAnyTouchpadDevice = false, + hasAnyKeyboardDevice = true, expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - EXTERNAL_MIRROR_UNKNOWN_PROJECTED_NO_MOUSE( + EXTERNAL_MIRROR_PROJECTED_NO_TOUCHPAD_KEYBOARD( hasExternalDisplay = true, extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.UNKNOWN, isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = false, + hasAnyTouchpadDevice = false, + hasAnyKeyboardDevice = true, expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - NO_EXTERNAL_MIRROR_UNKNOWN_PROJECTED_NO_MOUSE( + NO_EXTERNAL_MIRROR_PROJECTED_NO_TOUCHPAD_KEYBOARD( hasExternalDisplay = false, extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.UNKNOWN, isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = false, + hasAnyTouchpadDevice = false, + hasAnyKeyboardDevice = true, expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - EXTERNAL_EXTENDED_TABLET_NO_PROJECTED_MOUSE( + EXTERNAL_EXTENDED_NO_PROJECTED_TOUCHPAD_NO_KEYBOARD( hasExternalDisplay = true, extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.ON, isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = true, + hasAnyTouchpadDevice = true, + hasAnyKeyboardDevice = false, expectedWindowingMode = WINDOWING_MODE_FREEFORM, ), - NO_EXTERNAL_EXTENDED_TABLET_NO_PROJECTED_MOUSE( + NO_EXTERNAL_EXTENDED_NO_PROJECTED_TOUCHPAD_NO_KEYBOARD( hasExternalDisplay = false, extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.ON, isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = true, - expectedWindowingMode = WINDOWING_MODE_FREEFORM, - ), - EXTERNAL_MIRROR_TABLET_NO_PROJECTED_MOUSE( - hasExternalDisplay = true, - extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.ON, - isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = true, - expectedWindowingMode = WINDOWING_MODE_FREEFORM, - ), - NO_EXTERNAL_MIRROR_TABLET_NO_PROJECTED_MOUSE( - hasExternalDisplay = false, - extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.ON, - isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = true, - expectedWindowingMode = WINDOWING_MODE_FREEFORM, - ), - EXTERNAL_EXTENDED_CLAMSHELL_NO_PROJECTED_MOUSE( - hasExternalDisplay = true, - extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.OFF, - isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = true, - expectedWindowingMode = WINDOWING_MODE_FREEFORM, - ), - NO_EXTERNAL_EXTENDED_CLAMSHELL_NO_PROJECTED_MOUSE( - hasExternalDisplay = false, - extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.OFF, - isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = true, - expectedWindowingMode = WINDOWING_MODE_FREEFORM, - ), - EXTERNAL_MIRROR_CLAMSHELL_NO_PROJECTED_MOUSE( - hasExternalDisplay = true, - extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.OFF, - isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = true, - expectedWindowingMode = WINDOWING_MODE_FREEFORM, - ), - NO_EXTERNAL_MIRROR_CLAMSHELL_NO_PROJECTED_MOUSE( - hasExternalDisplay = false, - extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.OFF, - isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = true, - expectedWindowingMode = WINDOWING_MODE_FREEFORM, - ), - EXTERNAL_EXTENDED_UNKNOWN_NO_PROJECTED_MOUSE( - hasExternalDisplay = true, - extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.UNKNOWN, - isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = true, - expectedWindowingMode = WINDOWING_MODE_FREEFORM, - ), - NO_EXTERNAL_EXTENDED_UNKNOWN_NO_PROJECTED_MOUSE( - hasExternalDisplay = false, - extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.UNKNOWN, - isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = true, - expectedWindowingMode = WINDOWING_MODE_FREEFORM, + hasAnyTouchpadDevice = true, + hasAnyKeyboardDevice = false, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - EXTERNAL_MIRROR_UNKNOWN_NO_PROJECTED_MOUSE( + EXTERNAL_MIRROR_NO_PROJECTED_TOUCHPAD_NO_KEYBOARD( hasExternalDisplay = true, extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.UNKNOWN, isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = true, - expectedWindowingMode = WINDOWING_MODE_FREEFORM, + hasAnyTouchpadDevice = true, + hasAnyKeyboardDevice = false, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - NO_EXTERNAL_MIRROR_UNKNOWN_NO_PROJECTED_MOUSE( + NO_EXTERNAL_MIRROR_NO_PROJECTED_TOUCHPAD_NO_KEYBOARD( hasExternalDisplay = false, extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.UNKNOWN, isDefaultDisplayDesktopEligible = true, - hasAnyMouseDevice = true, - expectedWindowingMode = WINDOWING_MODE_FREEFORM, + hasAnyTouchpadDevice = true, + hasAnyKeyboardDevice = false, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - EXTERNAL_EXTENDED_TABLET_PROJECTED_MOUSE( + EXTERNAL_EXTENDED_PROJECTED_TOUCHPAD_NO_KEYBOARD( hasExternalDisplay = true, extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.ON, isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = true, + hasAnyTouchpadDevice = true, + hasAnyKeyboardDevice = false, expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - NO_EXTERNAL_EXTENDED_TABLET_PROJECTED_MOUSE( + NO_EXTERNAL_EXTENDED_PROJECTED_TOUCHPAD_NO_KEYBOARD( hasExternalDisplay = false, extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.ON, isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = true, + hasAnyTouchpadDevice = true, + hasAnyKeyboardDevice = false, expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - EXTERNAL_MIRROR_TABLET_PROJECTED_MOUSE( + EXTERNAL_MIRROR_PROJECTED_TOUCHPAD_NO_KEYBOARD( hasExternalDisplay = true, extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.ON, isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = true, + hasAnyTouchpadDevice = true, + hasAnyKeyboardDevice = false, expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - NO_EXTERNAL_MIRROR_TABLET_PROJECTED_MOUSE( + NO_EXTERNAL_MIRROR_PROJECTED_TOUCHPAD_NO_KEYBOARD( hasExternalDisplay = false, extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.ON, isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = true, + hasAnyTouchpadDevice = true, + hasAnyKeyboardDevice = false, expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - EXTERNAL_EXTENDED_CLAMSHELL_PROJECTED_MOUSE( + EXTERNAL_EXTENDED_NO_PROJECTED_NO_TOUCHPAD_NO_KEYBOARD( hasExternalDisplay = true, extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.OFF, - isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = true, - expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, + isDefaultDisplayDesktopEligible = true, + hasAnyTouchpadDevice = false, + hasAnyKeyboardDevice = false, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, ), - NO_EXTERNAL_EXTENDED_CLAMSHELL_PROJECTED_MOUSE( + NO_EXTERNAL_EXTENDED_NO_PROJECTED_NO_TOUCHPAD_NO_KEYBOARD( hasExternalDisplay = false, extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.OFF, - isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = true, + isDefaultDisplayDesktopEligible = true, + hasAnyTouchpadDevice = false, + hasAnyKeyboardDevice = false, expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - EXTERNAL_MIRROR_CLAMSHELL_PROJECTED_MOUSE( + EXTERNAL_MIRROR_NO_PROJECTED_NO_TOUCHPAD_NO_KEYBOARD( hasExternalDisplay = true, extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.OFF, - isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = true, + isDefaultDisplayDesktopEligible = true, + hasAnyTouchpadDevice = false, + hasAnyKeyboardDevice = false, expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - NO_EXTERNAL_MIRROR_CLAMSHELL_PROJECTED_MOUSE( + NO_EXTERNAL_MIRROR_NO_PROJECTED_NO_TOUCHPAD_NO_KEYBOARD( hasExternalDisplay = false, extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.OFF, - isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = true, + isDefaultDisplayDesktopEligible = true, + hasAnyTouchpadDevice = false, + hasAnyKeyboardDevice = false, expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - EXTERNAL_EXTENDED_UNKNOWN_PROJECTED_MOUSE( + EXTERNAL_EXTENDED_PROJECTED_NO_TOUCHPAD_NO_KEYBOARD( hasExternalDisplay = true, extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.UNKNOWN, isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = true, + hasAnyTouchpadDevice = false, + hasAnyKeyboardDevice = false, expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - NO_EXTERNAL_EXTENDED_UNKNOWN_PROJECTED_MOUSE( + NO_EXTERNAL_EXTENDED_PROJECTED_NO_TOUCHPAD_NO_KEYBOARD( hasExternalDisplay = false, extendedDisplayEnabled = true, - tabletModeStatus = SwitchState.UNKNOWN, isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = true, + hasAnyTouchpadDevice = false, + hasAnyKeyboardDevice = false, expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - EXTERNAL_MIRROR_UNKNOWN_PROJECTED_MOUSE( + EXTERNAL_MIRROR_PROJECTED_NO_TOUCHPAD_NO_KEYBOARD( hasExternalDisplay = true, extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.UNKNOWN, isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = true, + hasAnyTouchpadDevice = false, + hasAnyKeyboardDevice = false, expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - NO_EXTERNAL_MIRROR_UNKNOWN_PROJECTED_MOUSE( + NO_EXTERNAL_MIRROR_PROJECTED_NO_TOUCHPAD_NO_KEYBOARD( hasExternalDisplay = false, extendedDisplayEnabled = false, - tabletModeStatus = SwitchState.UNKNOWN, isDefaultDisplayDesktopEligible = false, - hasAnyMouseDevice = true, + hasAnyTouchpadDevice = false, + hasAnyKeyboardDevice = false, expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt index 652fae01c1b2..a4052890f08a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt @@ -283,14 +283,32 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, ) - fun testDefaultIndicators_bubblesEnabled() { + fun testDefaultIndicators_enableBubbleToFullscreen() { createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) var result = visualIndicator.updateIndicatorType(PointF(10f, 1500f)) assertThat(result) .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_LEFT_INDICATOR) - result = visualIndicator.updateIndicatorType(PointF(2300f, 1500f)) + result = visualIndicator.updateIndicatorType(PointF(2390f, 1500f)) assertThat(result) .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR) + + // Check that bubble zones are not available from split + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT) + result = visualIndicator.updateIndicatorType(PointF(10f, 1500f)) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR) + result = visualIndicator.updateIndicatorType(PointF(2390f, 1500f)) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR) + + // Check that bubble zones are not available from desktop + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FREEFORM) + result = visualIndicator.updateIndicatorType(PointF(10f, 1500f)) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR) + result = visualIndicator.updateIndicatorType(PointF(2390f, 1500f)) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR) } @Test @@ -298,7 +316,7 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, ) - fun testDefaultIndicators_foldable_leftRightSplit() { + fun testDefaultIndicators_foldable_enableBubbleToFullscreen_dragFromFullscreen() { setUpFoldable() createVisualIndicator( @@ -325,13 +343,47 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { result = visualIndicator.updateIndicatorType(foldRightBottom()) assertThat(result) .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR) + } + + @Test + @EnableFlags( + com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, + com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, + ) + @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_ANYTHING) + fun testDefaultIndicators_foldable_enableBubbleToFullscreen_dragFromSplit() { + setUpFoldable() createVisualIndicator( DesktopModeVisualIndicator.DragStartState.FROM_SPLIT, isSmallTablet = true, isLeftRightSplit = true, ) - result = visualIndicator.updateIndicatorType(foldCenter()) + var result = visualIndicator.updateIndicatorType(foldCenter()) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) + + // Check that bubbles are not available from split + result = visualIndicator.updateIndicatorType(foldLeftBottom()) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR) + + result = visualIndicator.updateIndicatorType(foldRightBottom()) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR) + } + + @Test + @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_ANYTHING) + fun testDefaultIndicators_foldable_enableBubbleAnything_dragFromSplit() { + setUpFoldable() + + createVisualIndicator( + DesktopModeVisualIndicator.DragStartState.FROM_SPLIT, + isSmallTablet = true, + isLeftRightSplit = true, + ) + var result = visualIndicator.updateIndicatorType(foldCenter()) assertThat(result) .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopPipTransitionControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopPipTransitionControllerTest.kt new file mode 100644 index 000000000000..47a9a6c8d840 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopPipTransitionControllerTest.kt @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode + +import android.os.Binder +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization +import android.view.Display.DEFAULT_DISPLAY +import android.window.WindowContainerTransaction +import androidx.test.filters.SmallTest +import com.android.window.flags.Flags +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP +import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.pip.PipDesktopState +import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters + +/** + * Tests for [DesktopPipTransitionController]. + * + * Build/Install/Run: atest WMShellUnitTests:DesktopPipTransitionControllerTest + */ +@SmallTest +@RunWith(ParameterizedAndroidJunit4::class) +@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) +class DesktopPipTransitionControllerTest(flags: FlagsParameterization) : ShellTestCase() { + private val mockDesktopTasksController = mock<DesktopTasksController>() + private val mockDesktopUserRepositories = mock<DesktopUserRepositories>() + private val mockDesktopRepository = mock<DesktopRepository>() + private val mockPipDesktopState = mock<PipDesktopState>() + + private lateinit var controller: DesktopPipTransitionController + + private val transition = Binder() + private val wct = WindowContainerTransaction() + private val taskInfo = createFreeformTask() + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + + @Before + fun setUp() { + whenever(mockPipDesktopState.isDesktopWindowingPipEnabled()).thenReturn(true) + whenever(mockDesktopUserRepositories.getProfile(anyInt())).thenReturn(mockDesktopRepository) + whenever(mockDesktopRepository.isAnyDeskActive(anyInt())).thenReturn(true) + whenever(mockDesktopRepository.getActiveDeskId(anyInt())).thenReturn(DESK_ID) + + controller = + DesktopPipTransitionController( + mockDesktopTasksController, + mockDesktopUserRepositories, + mockPipDesktopState, + ) + } + + @Test + fun handlePipTransition_noDeskActive_doesntPerformDesktopExitCleanup() { + whenever(mockDesktopRepository.isAnyDeskActive(eq(taskInfo.displayId))).thenReturn(false) + + controller.handlePipTransition(wct, transition, taskInfo) + + verifyPerformDesktopExitCleanupAfterPip(isCalled = false) + } + + @Test + fun handlePipTransition_notLastTask_doesntPerformDesktopExitCleanup() { + whenever( + mockDesktopRepository.isOnlyVisibleNonClosingTaskInDesk( + taskId = eq(taskInfo.taskId), + deskId = eq(DESK_ID), + displayId = eq(taskInfo.displayId), + ) + ) + .thenReturn(false) + + controller.handlePipTransition(wct, transition, taskInfo) + + verifyPerformDesktopExitCleanupAfterPip(isCalled = false) + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handlePipTransition_noActiveDeskId_multiDesk_doesntPerformDesktopExitCleanup() { + whenever(mockDesktopRepository.getActiveDeskId(eq(taskInfo.displayId))).thenReturn(null) + + controller.handlePipTransition(wct, transition, taskInfo) + + verifyPerformDesktopExitCleanupAfterPip(isCalled = false) + } + + @Test + fun handlePipTransition_isLastTask_performDesktopExitCleanup() { + whenever( + mockDesktopRepository.isOnlyVisibleNonClosingTaskInDesk( + taskId = eq(taskInfo.taskId), + deskId = eq(DESK_ID), + displayId = eq(taskInfo.displayId), + ) + ) + .thenReturn(true) + + controller.handlePipTransition(wct, transition, taskInfo) + + verifyPerformDesktopExitCleanupAfterPip(isCalled = true) + } + + private fun verifyPerformDesktopExitCleanupAfterPip(isCalled: Boolean) { + if (isCalled) { + verify(mockDesktopTasksController) + .performDesktopExitCleanUp( + wct = wct, + deskId = DESK_ID, + displayId = DEFAULT_DISPLAY, + willExitDesktop = true, + ) + } else { + verify(mockDesktopTasksController, never()) + .performDesktopExitCleanUp( + any(), + anyInt(), + anyInt(), + anyBoolean(), + anyBoolean(), + anyBoolean(), + ) + } + } + + private companion object { + const val DESK_ID = 1 + + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> = + FlagsParameterization.allCombinationsOf(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopPipTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopPipTransitionObserverTest.kt deleted file mode 100644 index ef394d81cc57..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopPipTransitionObserverTest.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (C) 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.desktopmode - -import android.app.WindowConfiguration.WINDOWING_MODE_PINNED -import android.os.Binder -import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule -import android.testing.AndroidTestingRunner -import android.view.WindowManager.TRANSIT_PIP -import android.window.TransitionInfo -import androidx.test.filters.SmallTest -import com.android.window.flags.Flags -import com.android.wm.shell.ShellTestCase -import com.android.wm.shell.TestRunningTaskInfoBuilder -import com.google.common.truth.Truth.assertThat -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.kotlin.mock - -/** - * Tests for [DesktopPipTransitionObserver]. - * - * Build/Install/Run: atest WMShellUnitTests:DesktopPipTransitionObserverTest - */ -@SmallTest -@RunWith(AndroidTestingRunner::class) -class DesktopPipTransitionObserverTest : ShellTestCase() { - - @JvmField @Rule val setFlagsRule = SetFlagsRule() - - private lateinit var observer: DesktopPipTransitionObserver - - private val transition = Binder() - private var onSuccessInvokedCount = 0 - - @Before - fun setUp() { - observer = DesktopPipTransitionObserver() - - onSuccessInvokedCount = 0 - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP) - fun onTransitionReady_taskInPinnedWindowingMode_onSuccessInvoked() { - val taskId = 1 - val pipTransition = createPendingPipTransition(taskId) - val successfulChange = createChange(taskId, WINDOWING_MODE_PINNED) - observer.addPendingPipTransition(pipTransition) - - observer.onTransitionReady( - transition = transition, - info = TransitionInfo( - TRANSIT_PIP, /* flags= */ - 0 - ).apply { addChange(successfulChange) }, - ) - - assertThat(onSuccessInvokedCount).isEqualTo(1) - } - - private fun createPendingPipTransition( - taskId: Int - ): DesktopPipTransitionObserver.PendingPipTransition { - return DesktopPipTransitionObserver.PendingPipTransition( - token = transition, - taskId = taskId, - onSuccess = { onSuccessInvokedCount += 1 }, - ) - } - - private fun createChange(taskId: Int, windowingMode: Int): TransitionInfo.Change { - return TransitionInfo.Change(mock(), mock()).apply { - taskInfo = - TestRunningTaskInfoBuilder() - .setTaskId(taskId) - .setWindowingMode(windowingMode) - .build() - } - } -} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index b577667d8279..d81786b5e6a5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -32,9 +32,9 @@ import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.content.ComponentName import android.content.Context import android.content.Intent -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo.CONFIG_DENSITY +import android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED @@ -50,6 +50,7 @@ import android.os.Binder import android.os.Bundle import android.os.Handler import android.os.IBinder +import android.os.UserHandle import android.os.UserManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags @@ -265,7 +266,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Mock private lateinit var desksOrganizer: DesksOrganizer @Mock private lateinit var userProfileContexts: UserProfileContexts @Mock private lateinit var desksTransitionsObserver: DesksTransitionObserver - @Mock private lateinit var desktopPipTransitionObserver: DesktopPipTransitionObserver @Mock private lateinit var packageManager: PackageManager @Mock private lateinit var mockDisplayContext: Context @Mock private lateinit var dragToDisplayTransitionHandler: DragToDisplayTransitionHandler @@ -460,7 +460,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() overviewToDesktopTransitionObserver, desksOrganizer, desksTransitionsObserver, - Optional.of(desktopPipTransitionObserver), userProfileContexts, desktopModeCompatPolicy, dragToDisplayTransitionHandler, @@ -1200,9 +1199,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() existingTask.topActivity = testComponent existingTask.configuration.windowConfiguration.setBounds(Rect(0, 0, 500, 500)) // Set up new instance of already existing task. - val launchingTask = setUpFullscreenTask() + val launchingTask = + setUpFullscreenTask().apply { + topActivityInfo = ActivityInfo().apply { launchMode = LAUNCH_SINGLE_INSTANCE } + } launchingTask.topActivity = testComponent - launchingTask.baseIntent.addFlags(FLAG_ACTIVITY_NEW_TASK) // Move new instance to desktop. By default multi instance is not supported so first // instance will close. @@ -1224,10 +1225,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() existingTask.topActivity = testComponent existingTask.configuration.windowConfiguration.setBounds(Rect(0, 0, 500, 500)) // Set up new instance of already existing task. - val launchingTask = setUpFreeformTask(active = false) + val launchingTask = + setUpFreeformTask(active = false).apply { + topActivityInfo = ActivityInfo().apply { launchMode = LAUNCH_SINGLE_INSTANCE } + } taskRepository.removeTask(launchingTask.displayId, launchingTask.taskId) launchingTask.topActivity = testComponent - launchingTask.baseIntent.addFlags(FLAG_ACTIVITY_NEW_TASK) // Move new instance to desktop. By default multi instance is not supported so first // instance will close. @@ -2378,9 +2381,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity() { + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)) + .thenReturn(false) val homeTask = setUpHomeTask() val task = setUpFreeformTask() - assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) .configuration .windowConfiguration @@ -2432,9 +2436,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, ) fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_homeBehindFullscreen_multiDesksEnabled() { + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)) + .thenReturn(false) val homeTask = setUpHomeTask() val task = setUpFreeformTask() - assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) .configuration .windowConfiguration @@ -3536,15 +3541,20 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() .addPendingTransition(DeskTransition.DeactivateDesk(token = transition, deskId = 0)) } - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP) - fun onPipTaskMinimize_autoEnterEnabled_startPipTransition() { - val task = setUpPipTask(autoEnterEnabled = true) + private fun minimizePipTask(task: RunningTaskInfo) { val handler = mock(TransitionHandler::class.java) whenever(transitions.dispatchRequest(any(), any(), anyOrNull())) .thenReturn(android.util.Pair(handler, WindowContainerTransaction())) controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun onPipTaskMinimize_autoEnterEnabled_startPipTransition() { + val task = setUpPipTask(autoEnterEnabled = true) + + minimizePipTask(task) verify(freeformTaskTransitionStarter).startPipTransition(any()) verify(freeformTaskTransitionStarter, never()) @@ -3564,7 +3574,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() ) .thenReturn(Binder()) - controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) + minimizePipTask(task) verify(freeformTaskTransitionStarter) .startMinimizedModeTransition(any(), eq(task.taskId), anyBoolean()) @@ -3575,52 +3585,24 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP) fun onPipTaskMinimize_autoEnterEnabled_sendsTaskbarRoundingUpdate() { val task = setUpPipTask(autoEnterEnabled = true) - val handler = mock(TransitionHandler::class.java) - whenever(transitions.dispatchRequest(any(), any(), anyOrNull())) - .thenReturn(android.util.Pair(handler, WindowContainerTransaction())) - controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) + minimizePipTask(task) verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(anyBoolean()) } @Test @EnableFlags( - Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP, - ) - fun onDesktopTaskEnteredPip_pipIsLastTask_removesWallpaper() { - val task = setUpPipTask(autoEnterEnabled = true) - - controller.onDesktopTaskEnteredPip( - taskId = task.taskId, - deskId = DEFAULT_DISPLAY, - displayId = task.displayId, - taskIsLastVisibleTaskBeforePip = true, - ) - - // Wallpaper is moved to the back - val wct = getLatestTransition() - wct.assertReorder(wallpaperToken, /* toTop= */ false) - } - - @Test - @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP, Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, ) - fun onDesktopTaskEnteredPip_pipIsLastTask_deactivatesDesk() { + fun onPipTaskMinimize_isLastTask_deactivatesDesk() { val deskId = DEFAULT_DISPLAY val task = setUpPipTask(autoEnterEnabled = true, deskId = deskId) val transition = Binder() - whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition) + whenever(freeformTaskTransitionStarter.startPipTransition(any())).thenReturn(transition) - controller.onDesktopTaskEnteredPip( - taskId = task.taskId, - deskId = deskId, - displayId = task.displayId, - taskIsLastVisibleTaskBeforePip = true, - ) + minimizePipTask(task) verify(desksOrganizer).deactivateDesk(any(), eq(deskId)) verify(desksTransitionsObserver) @@ -3628,44 +3610,31 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP) - fun onDesktopTaskEnteredPip_pipIsLastTask_launchesHome() { + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + ) + fun onPipTaskMinimize_isLastTask_removesWallpaper() { val task = setUpPipTask(autoEnterEnabled = true) - controller.onDesktopTaskEnteredPip( - taskId = task.taskId, - deskId = DEFAULT_DISPLAY, - displayId = task.displayId, - taskIsLastVisibleTaskBeforePip = true, - ) + minimizePipTask(task) - val wct = getLatestTransition() - wct.assertPendingIntent(launchHomeIntent(DEFAULT_DISPLAY)) + val arg = argumentCaptor<WindowContainerTransaction>() + verify(freeformTaskTransitionStarter).startPipTransition(arg.capture()) + // Wallpaper is moved to the back + arg.lastValue.assertReorder(wallpaperToken, /* toTop= */ false) } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP) - fun onDesktopTaskEnteredPip_pipIsNotLastTask_doesntExitDesktopMode() { + fun onPipTaskMinimize_isLastTask_launchesHome() { val task = setUpPipTask(autoEnterEnabled = true) - val deskId = DEFAULT_DISPLAY - setUpFreeformTask(deskId = deskId) // launch another freeform task - val transition = Binder() - whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition) - controller.onDesktopTaskEnteredPip( - taskId = task.taskId, - deskId = deskId, - displayId = task.displayId, - taskIsLastVisibleTaskBeforePip = false, - ) + minimizePipTask(task) - // No transition to exit Desktop mode is started - verifyWCTNotExecuted() - verify(desktopModeEnterExitTransitionListener, never()) - .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) - verify(desksOrganizer, never()).deactivateDesk(any(), eq(deskId)) - verify(desksTransitionsObserver, never()) - .addPendingTransition(DeskTransition.DeactivateDesk(transition, deskId)) + val arg = argumentCaptor<WindowContainerTransaction>() + verify(freeformTaskTransitionStarter).startPipTransition(arg.capture()) + arg.lastValue.assertPendingIntent(launchHomeIntent(DEFAULT_DISPLAY)) } @Test @@ -4283,6 +4252,25 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun handleRequest_fullscreenTask_noInDesk_enforceDesktop_secondaryDisplay_movesToDesk() { + val deskId = 5 + taskRepository.addDesk(displayId = SECONDARY_DISPLAY_ID, deskId = deskId) + taskRepository.setDeskInactive(deskId) + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + + val fullscreenTask = createFullscreenTask(displayId = SECONDARY_DISPLAY_ID) + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertNotNull(wct, "should handle request") + verify(desksOrganizer).moveTaskToDesk(wct, deskId, fullscreenTask) + } + + @Test fun handleRequest_fullscreenTask_notInDesk_enforceDesktop_fullscreenDisplay_returnNull() { taskRepository.setDeskInactive(deskId = 0) whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) @@ -5550,7 +5538,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.removeDesk(deskId = 2) - verify(desksOrganizer).removeDesk(any(), eq(2)) + verify(desksOrganizer).removeDesk(any(), eq(2), any()) verify(desksTransitionsObserver) .addPendingTransition( argThat { @@ -5566,6 +5554,49 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, ) + fun removeDesk_multipleDesks_removesRunningTasks() { + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_CLOSE), any(), anyOrNull())) + .thenReturn(transition) + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 2) + val task1 = setUpFreeformTask(deskId = 2) + val task2 = setUpFreeformTask(deskId = 2) + val task3 = setUpFreeformTask(deskId = 2) + + controller.removeDesk(deskId = 2) + + val wct = getLatestWct(TRANSIT_CLOSE) + wct.assertRemove(task1.token) + wct.assertRemove(task2.token) + wct.assertRemove(task3.token) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun removeDesk_multipleDesks_removesRecentTasks() { + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_CLOSE), any(), anyOrNull())) + .thenReturn(transition) + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 2) + val task1 = setUpFreeformTask(deskId = 2, background = true) + val task2 = setUpFreeformTask(deskId = 2, background = true) + val task3 = setUpFreeformTask(deskId = 2, background = true) + + controller.removeDesk(deskId = 2) + + verify(recentTasksController).removeBackgroundTask(task1.taskId) + verify(recentTasksController).removeBackgroundTask(task2.taskId) + verify(recentTasksController).removeBackgroundTask(task3.taskId) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun activateDesk_multipleDesks_addsPendingTransition() { val deskId = 0 val transition = Binder() @@ -5639,6 +5670,33 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun activateDesk_otherDeskWasActive_deactivatesOtherDesk() { + val previouslyActiveDeskId = 1 + val activatingDeskId = 0 + val transition = Binder() + val deskChange = mock(TransitionInfo.Change::class.java) + whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) + .thenReturn(transition) + whenever(desksOrganizer.isDeskActiveAtEnd(deskChange, activatingDeskId)).thenReturn(true) + // Make desk inactive by activating another desk. + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = previouslyActiveDeskId) + taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId = previouslyActiveDeskId) + + controller.activateDesk(activatingDeskId, RemoteTransition(TestRemoteTransition())) + + verify(desksOrganizer).deactivateDesk(any(), eq(previouslyActiveDeskId)) + verify(desksTransitionsObserver) + .addPendingTransition( + argThat { + this is DeskTransition.DeactivateDesk && + this.token == transition && + this.deskId == previouslyActiveDeskId + } + ) + } + + @Test @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, @@ -7495,11 +7553,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun testCreateDesk() { val currentDeskCount = taskRepository.getNumberOfDesks(DEFAULT_DISPLAY) - whenever(desksOrganizer.createDesk(eq(DEFAULT_DISPLAY), any())).thenAnswer { invocation -> - (invocation.arguments[1] as DesksOrganizer.OnCreateCallback).onCreated(deskId = 5) + whenever(desksOrganizer.createDesk(eq(DEFAULT_DISPLAY), any(), any())).thenAnswer { + invocation -> + (invocation.arguments[2] as DesksOrganizer.OnCreateCallback).onCreated(deskId = 5) } - controller.createDesk(DEFAULT_DISPLAY) + controller.createDesk(DEFAULT_DISPLAY, taskRepository.userId) assertThat(taskRepository.getNumberOfDesks(DEFAULT_DISPLAY)).isEqualTo(currentDeskCount + 1) } @@ -7509,7 +7568,17 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun testCreateDesk_invalidDisplay_dropsRequest() { controller.createDesk(INVALID_DISPLAY) - verify(desksOrganizer, never()).createDesk(any(), any()) + verify(desksOrganizer, never()).createDesk(any(), any(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun testCreateDesk_systemUser_dropsRequest() { + assumeTrue(UserManager.isHeadlessSystemUserMode()) + + controller.createDesk(DEFAULT_DISPLAY, UserHandle.USER_SYSTEM) + + verify(desksOrganizer, never()).createDesk(any(), any(), any()) } @Test @@ -8242,6 +8311,15 @@ private fun WindowContainerTransaction.assertReorderSequenceInRange( .inOrder() } +private fun WindowContainerTransaction.assertRemove(token: WindowContainerToken) { + assertThat( + hierarchyOps.any { hop -> + hop.container == token.asBinder() && hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK + } + ) + .isTrue() +} + private fun WindowContainerTransaction.assertRemoveAt(index: Int, token: WindowContainerToken) { assertIndexInBounds(index) val op = hierarchyOps[index] diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt index 5ef1ace7873d..1e0c94c2452c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt @@ -50,7 +50,6 @@ import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage -import java.util.Optional import org.junit.Before import org.junit.Rule import org.junit.Test @@ -86,7 +85,6 @@ class DesktopTasksTransitionObserverTest { private val userRepositories = mock<DesktopUserRepositories>() private val taskRepository = mock<DesktopRepository>() private val mixedHandler = mock<DesktopMixedTransitionHandler>() - private val pipTransitionObserver = mock<DesktopPipTransitionObserver>() private val backAnimationController = mock<BackAnimationController>() private val desktopWallpaperActivityTokenProvider = mock<DesktopWallpaperActivityTokenProvider>() @@ -111,7 +109,6 @@ class DesktopTasksTransitionObserverTest { transitions, shellTaskOrganizer, mixedHandler, - Optional.of(pipTransitionObserver), backAnimationController, desktopWallpaperActivityTokenProvider, shellInit, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt index 9af504797182..34f832b4fba4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt @@ -15,10 +15,11 @@ */ package com.android.wm.shell.desktopmode.multidesks +import android.app.ActivityManager import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.testing.AndroidTestingRunner -import android.view.Display +import android.view.Display.DEFAULT_DISPLAY import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.TransitionInfo @@ -40,15 +41,17 @@ import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer.DeskRo import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellInit import com.google.common.truth.Truth.assertThat +import kotlin.coroutines.suspendCoroutine import kotlin.test.assertNotNull import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertEquals import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito +import org.mockito.Mockito.never import org.mockito.Mockito.verify +import org.mockito.kotlin.any import org.mockito.kotlin.argThat import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -67,6 +70,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { private val mockShellCommandHandler = mock<ShellCommandHandler>() private val mockShellTaskOrganizer = mock<ShellTaskOrganizer>() private val launchAdjacentController = LaunchAdjacentController(mock()) + private val taskInfoChangedListener = mock<(ActivityManager.RunningTaskInfo) -> Unit>() private lateinit var organizer: RootTaskDesksOrganizer @@ -79,13 +83,24 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { mockShellTaskOrganizer, launchAdjacentController, ) + organizer.setOnDesktopTaskInfoChangedListener(taskInfoChangedListener) } - @Test fun testCreateDesk_createsDeskAndMinimizationRoots() = runTest { createDesk() } + @Test fun testCreateDesk_createsDeskAndMinimizationRoots() = runTest { createDeskSuspending() } + + @Test + fun testCreateDesk_rootExistsForOtherUser_reusesRoot() = runTest { + val desk = createDeskSuspending(userId = PRIMARY_USER_ID) + + val deskId = + organizer.createDeskSuspending(displayId = DEFAULT_DISPLAY, userId = SECONDARY_USER_ID) + + assertThat(deskId).isEqualTo(desk.deskRoot.deskId) + } @Test fun testCreateMinimizationRoot_marksHidden() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() verify(mockShellTaskOrganizer) .applyTransaction( @@ -110,7 +125,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testOnTaskAppeared_duplicateRoot_throws() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() assertThrows(Exception::class.java) { organizer.onTaskAppeared(desk.deskRoot.taskInfo, SurfaceControl()) @@ -119,7 +134,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testOnTaskAppeared_duplicateMinimizedRoot_throws() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() assertThrows(Exception::class.java) { organizer.onTaskAppeared(desk.minimizationRoot.taskInfo, SurfaceControl()) @@ -128,7 +143,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testOnTaskVanished_removesRoot() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() organizer.onTaskVanished(desk.deskRoot.taskInfo) @@ -137,7 +152,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testOnTaskVanished_removesMinimizedRoot() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() organizer.onTaskVanished(desk.deskRoot.taskInfo) organizer.onTaskVanished(desk.minimizationRoot.taskInfo) @@ -147,7 +162,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testDesktopWindowAppearsInDesk() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } organizer.onTaskAppeared(child, SurfaceControl()) @@ -157,7 +172,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testDesktopWindowAppearsInDeskMinimizationRoot() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val child = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } organizer.onTaskAppeared(child, SurfaceControl()) @@ -167,7 +182,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testDesktopWindowMovesToMinimizationRoot() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } organizer.onTaskAppeared(child, SurfaceControl()) @@ -180,7 +195,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testDesktopWindowDisappearsFromDesk() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } organizer.onTaskAppeared(child, SurfaceControl()) @@ -191,7 +206,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testDesktopWindowDisappearsFromDeskMinimizationRoot() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val child = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } organizer.onTaskAppeared(child, SurfaceControl()) @@ -201,11 +216,23 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test + fun testRemoveDesk_disablesAsLaunchRoot() = runTest { + val desk = createDeskSuspending(userId = PRIMARY_USER_ID) + val wct = WindowContainerTransaction() + organizer.activateDesk(wct, desk.deskRoot.deskId) + assertThat(desk.deskRoot.isLaunchRootRequested).isTrue() + + organizer.removeDesk(wct, desk.deskRoot.deskId, userId = PRIMARY_USER_ID) + + assertThat(desk.deskRoot.isLaunchRootRequested).isFalse() + } + + @Test fun testRemoveDesk_removesDeskRoot() = runTest { - val desk = createDesk() + val desk = createDeskSuspending(userId = PRIMARY_USER_ID) val wct = WindowContainerTransaction() - organizer.removeDesk(wct, desk.deskRoot.deskId) + organizer.removeDesk(wct, desk.deskRoot.deskId, userId = PRIMARY_USER_ID) assertThat( wct.hierarchyOps.any { hop -> @@ -218,10 +245,10 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testRemoveDesk_removesMinimizationRoot() = runTest { - val desk = createDesk() + val desk = createDeskSuspending(userId = PRIMARY_USER_ID) val wct = WindowContainerTransaction() - organizer.removeDesk(wct, desk.deskRoot.deskId) + organizer.removeDesk(wct, desk.deskRoot.deskId, userId = PRIMARY_USER_ID) assertThat( wct.hierarchyOps.any { hop -> @@ -233,8 +260,27 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test + fun testRemoveDesk_rootUsedByOtherUser_keepsDeskRoot() = runTest { + val primaryUserDesk = createDeskSuspending(userId = PRIMARY_USER_ID) + val secondaryUserDesk = createDeskSuspending(userId = SECONDARY_USER_ID) + assertThat(primaryUserDesk).isEqualTo(secondaryUserDesk) + + val wct = WindowContainerTransaction() + organizer.removeDesk(wct, primaryUserDesk.deskRoot.deskId, userId = PRIMARY_USER_ID) + + assertThat( + wct.hierarchyOps.any { hop -> + hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK && + hop.container == primaryUserDesk.deskRoot.token.asBinder() + } + ) + .isFalse() + assertThat(primaryUserDesk.deskRoot.users).containsExactly(SECONDARY_USER_ID) + } + + @Test fun testActivateDesk() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val wct = WindowContainerTransaction() organizer.activateDesk(wct, desk.deskRoot.deskId) @@ -266,7 +312,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testMoveTaskToDesk() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val desktopTask = createFreeformTask().apply { parentTaskId = -1 } val wct = WindowContainerTransaction() @@ -303,7 +349,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testGetDeskAtEnd() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } val endDesk = @@ -316,7 +362,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testGetDeskAtEnd_inMinimizationRoot() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val task = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } val endDesk = @@ -329,7 +375,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testIsDeskActiveAtEnd() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val isActive = organizer.isDeskActiveAtEnd( @@ -347,7 +393,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun deactivateDesk_clearsLaunchRoot() = runTest { val wct = WindowContainerTransaction() - val desk = createDesk() + val desk = createDeskSuspending() organizer.activateDesk(wct, desk.deskRoot.deskId) organizer.deactivateDesk(wct, desk.deskRoot.deskId) @@ -365,7 +411,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun isDeskChange_forDeskId() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() assertThat( organizer.isDeskChange( @@ -380,7 +426,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun isDeskChange_forDeskId_inMinimizationRoot() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() assertThat( organizer.isDeskChange( @@ -398,7 +444,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun isDeskChange_anyDesk() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() assertThat( organizer.isDeskChange( @@ -412,7 +458,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun isDeskChange_anyDesk_inMinimizationRoot() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() assertThat( organizer.isDeskChange( @@ -429,7 +475,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun minimizeTask() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } val wct = WindowContainerTransaction() organizer.moveTaskToDesk(wct, desk.deskRoot.deskId, task) @@ -442,7 +488,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun minimizeTask_alreadyMinimized_noOp() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val task = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } val wct = WindowContainerTransaction() organizer.onTaskAppeared(task, SurfaceControl()) @@ -454,8 +500,8 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun minimizeTask_inDifferentDesk_noOp() = runTest { - val desk = createDesk() - val otherDesk = createDesk() + val desk = createDeskSuspending() + val otherDesk = createDeskSuspending() val task = createFreeformTask().apply { parentTaskId = otherDesk.deskRoot.deskId } val wct = WindowContainerTransaction() organizer.onTaskAppeared(task, SurfaceControl()) @@ -467,7 +513,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun unminimizeTask() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } val wct = WindowContainerTransaction() organizer.moveTaskToDesk(wct, desk.deskRoot.deskId, task) @@ -484,7 +530,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun unminimizeTask_alreadyUnminimized_noOp() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } val wct = WindowContainerTransaction() organizer.moveTaskToDesk(wct, desk.deskRoot.deskId, task) @@ -498,7 +544,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun unminimizeTask_notInDesk_noOp() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val task = createFreeformTask() val wct = WindowContainerTransaction() @@ -509,7 +555,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun reorderTaskToFront() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } val wct = WindowContainerTransaction() organizer.onTaskAppeared(task, SurfaceControl()) @@ -529,7 +575,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun reorderTaskToFront_notInDesk_noOp() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val task = createFreeformTask() val wct = WindowContainerTransaction() @@ -548,7 +594,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun reorderTaskToFront_minimized_unminimizesAndReorders() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } val wct = WindowContainerTransaction() organizer.onTaskAppeared(task, SurfaceControl()) @@ -573,7 +619,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { fun onTaskAppeared_visibleDesk_onlyDesk_disablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = true - createDesk(visible = true) + createDeskSuspending(visible = true) assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse() } @@ -582,7 +628,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { fun onTaskAppeared_invisibleDesk_onlyDesk_enablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = false - createDesk(visible = false) + createDeskSuspending(visible = false) assertThat(launchAdjacentController.launchAdjacentEnabled).isTrue() } @@ -591,8 +637,8 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { fun onTaskAppeared_invisibleDesk_otherVisibleDesk_disablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = true - createDesk(visible = true) - createDesk(visible = false) + createDeskSuspending(visible = true) + createDeskSuspending(visible = false) assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse() } @@ -601,7 +647,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { fun onTaskInfoChanged_deskBecomesVisible_onlyDesk_disablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = true - val desk = createDesk(visible = false) + val desk = createDeskSuspending(visible = false) desk.deskRoot.taskInfo.isVisible = true organizer.onTaskInfoChanged(desk.deskRoot.taskInfo) @@ -612,7 +658,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { fun onTaskInfoChanged_deskBecomesInvisible_onlyDesk_enablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = false - val desk = createDesk(visible = true) + val desk = createDeskSuspending(visible = true) desk.deskRoot.taskInfo.isVisible = false organizer.onTaskInfoChanged(desk.deskRoot.taskInfo) @@ -623,8 +669,8 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { fun onTaskInfoChanged_deskBecomesInvisible_otherVisibleDesk_disablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = true - createDesk(visible = true) - val desk = createDesk(visible = true) + createDeskSuspending(visible = true) + val desk = createDeskSuspending(visible = true) desk.deskRoot.taskInfo.isVisible = false organizer.onTaskInfoChanged(desk.deskRoot.taskInfo) @@ -635,7 +681,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { fun onTaskVanished_visibleDeskDisappears_onlyDesk_enablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = false - val desk = createDesk(visible = true) + val desk = createDeskSuspending(visible = true) organizer.onTaskVanished(desk.deskRoot.taskInfo) assertThat(launchAdjacentController.launchAdjacentEnabled).isTrue() @@ -645,19 +691,50 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { fun onTaskVanished_visibleDeskDisappears_otherDeskVisible_disablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = true - createDesk(visible = true) - val desk = createDesk(visible = true) + createDeskSuspending(visible = true) + val desk = createDeskSuspending(visible = true) organizer.onTaskVanished(desk.deskRoot.taskInfo) assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse() } + @Test + fun onTaskInfoChanged_taskNotRoot_invokesListener() = runTest { + createDeskSuspending() + val task = createFreeformTask().apply { taskId = TEST_CHILD_TASK_ID } + + organizer.onTaskInfoChanged(task) + + verify(taskInfoChangedListener).invoke(task) + } + + @Test + fun onTaskInfoChanged_isDeskRoot_doesNotInvokeListener() = runTest { + val deskRoot = createDeskSuspending().deskRoot + + organizer.onTaskInfoChanged(deskRoot.taskInfo) + + verify(taskInfoChangedListener, never()).invoke(any()) + } + + @Test + fun onTaskInfoChanged_isMinimizationRoot_doesNotInvokeListener() = runTest { + val minimizationRoot = createDeskSuspending().minimizationRoot + + organizer.onTaskInfoChanged(minimizationRoot.taskInfo) + + verify(taskInfoChangedListener, never()).invoke(any()) + } + private data class DeskRoots( val deskRoot: DeskRoot, val minimizationRoot: DeskMinimizationRoot, ) - private suspend fun createDesk(visible: Boolean = true): DeskRoots { + private suspend fun createDeskSuspending( + visible: Boolean = true, + userId: Int = PRIMARY_USER_ID, + ): DeskRoots { val freeformRootTask = createFreeformTask().apply { parentTaskId = -1 @@ -668,7 +745,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { Mockito.reset(mockShellTaskOrganizer) whenever( mockShellTaskOrganizer.createRootTask( - Display.DEFAULT_DISPLAY, + DEFAULT_DISPLAY, WINDOWING_MODE_FREEFORM, organizer, true, @@ -682,13 +759,9 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { val listener = (invocation.arguments[2] as TaskListener) listener.onTaskAppeared(minimizationRootTask, SurfaceControl()) } - val deskId = organizer.createDesk(Display.DEFAULT_DISPLAY) - assertEquals(freeformRootTask.taskId, deskId) - val deskRoot = assertNotNull(organizer.deskRootsByDeskId.get(freeformRootTask.taskId)) - val minimizationRoot = - assertNotNull(organizer.deskMinimizationRootsByDeskId[freeformRootTask.taskId]) - assertThat(minimizationRoot.deskId).isEqualTo(freeformRootTask.taskId) - assertThat(minimizationRoot.rootId).isEqualTo(minimizationRootTask.taskId) + val deskId = organizer.createDeskSuspending(DEFAULT_DISPLAY, userId) + val deskRoot = assertNotNull(organizer.deskRootsByDeskId.get(deskId)) + val minimizationRoot = assertNotNull(organizer.deskMinimizationRootsByDeskId[deskId]) return DeskRoots(deskRoot, minimizationRoot) } @@ -712,4 +785,15 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { hop.newParent == desk.deskRoot.token.asBinder() && hop.toTop } + + private suspend fun DesksOrganizer.createDeskSuspending(displayId: Int, userId: Int): Int = + suspendCoroutine { cont -> + createDesk(displayId, userId) { deskId -> cont.resumeWith(Result.success(deskId)) } + } + + companion object { + private const val PRIMARY_USER_ID = 10 + private const val SECONDARY_USER_ID = 11 + private const val TEST_CHILD_TASK_ID = 100 + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index f6e49853eddf..64bd86134d92 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -167,6 +167,7 @@ public class StageCoordinatorTests extends ShellTestCase { private final TestShellExecutor mMainExecutor = new TestShellExecutor(); private final ShellExecutor mAnimExecutor = new TestShellExecutor(); private final Handler mMainHandler = new Handler(Looper.getMainLooper()); + private final Handler mAnimHandler = mock(Handler.class); private final DisplayAreaInfo mDisplayAreaInfo = new DisplayAreaInfo(new MockToken().token(), DEFAULT_DISPLAY, 0); private final ActivityManager.RunningTaskInfo mMainChildTaskInfo = @@ -571,8 +572,7 @@ public class StageCoordinatorTests extends ShellTestCase { } @Test - @DisableFlags({Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, - Flags.FLAG_ENABLE_INPUT_LAYER_TRANSITION_FIX}) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) public void testRequestEnterSplit_didNotEnterSplitSelect_doesNotApplyTransaction() { final WindowContainerTransaction wct = new WindowContainerTransaction(); mStageCoordinator.registerSplitSelectListener( @@ -630,7 +630,7 @@ public class StageCoordinatorTests extends ShellTestCase { ShellInit shellInit = new ShellInit(mMainExecutor); final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), mTaskOrganizer, mTransactionPool, mock(DisplayController.class), - mDisplayInsetsController, mMainExecutor, mMainHandler, mAnimExecutor, + mDisplayInsetsController, mMainExecutor, mMainHandler, mAnimExecutor, mAnimHandler, mock(HomeTransitionObserver.class), mock(FocusTransitionObserver.class)); shellInit.init(); return t; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java index 3a455ba6b5df..f11839ad4e72 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java @@ -24,8 +24,12 @@ import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assume.assumeTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -77,16 +81,15 @@ public class TaskViewTransitionsTest extends ShellTestCase { @Mock TaskViewTaskController mTaskViewTaskController; @Mock - ActivityManager.RunningTaskInfo mTaskInfo; - @Mock WindowContainerToken mToken; @Mock ShellTaskOrganizer mOrganizer; @Mock SyncTransactionQueue mSyncQueue; - Executor mExecutor = command -> command.run(); + Executor mExecutor = Runnable::run; + ActivityManager.RunningTaskInfo mTaskInfo; TaskViewRepository mTaskViewRepository; TaskViewTransitions mTaskViewTransitions; @@ -305,4 +308,66 @@ public class TaskViewTransitionsTest extends ShellTestCase { verify(mTaskViewTaskController).setTaskNotFound(); } + + @Test + public void updateBoundsForUnfold_taskNotFound_doesNothing() { + assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); + + ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.token = mock(WindowContainerToken.class); + taskInfo.taskId = 666; + Rect bounds = new Rect(100, 50, 200, 250); + SurfaceControl.Transaction startTransaction = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishTransaction = mock(SurfaceControl.Transaction.class); + assertThat( + mTaskViewTransitions.updateBoundsForUnfold(bounds, startTransaction, + finishTransaction, taskInfo, mock(SurfaceControl.class))) + .isFalse(); + + verify(startTransaction, never()).reparent(any(), any()); + } + + @Test + public void updateBoundsForUnfold_noPendingTransition_doesNothing() { + assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); + + Rect bounds = new Rect(100, 50, 200, 250); + mTaskViewTransitions.setTaskBounds(mTaskViewTaskController, bounds); + assertThat(mTaskViewTransitions.hasPending()).isFalse(); + + SurfaceControl.Transaction startTransaction = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishTransaction = mock(SurfaceControl.Transaction.class); + assertThat( + mTaskViewTransitions.updateBoundsForUnfold(bounds, startTransaction, + finishTransaction, mTaskInfo, mock(SurfaceControl.class))) + .isFalse(); + verify(startTransaction, never()).reparent(any(), any()); + } + + @Test + public void updateBoundsForUnfold() { + assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); + + Rect bounds = new Rect(100, 50, 200, 250); + mTaskViewTransitions.updateVisibilityState(mTaskViewTaskController, /* visible= */ true); + mTaskViewTransitions.setTaskBounds(mTaskViewTaskController, bounds); + assertThat(mTaskViewTransitions.hasPending()).isTrue(); + + SurfaceControl.Transaction startTransaction = createMockTransaction(); + SurfaceControl.Transaction finishTransaction = createMockTransaction(); + assertThat( + mTaskViewTransitions.updateBoundsForUnfold(bounds, startTransaction, + finishTransaction, mTaskInfo, mock(SurfaceControl.class))) + .isTrue(); + assertThat(mTaskViewRepository.byTaskView(mTaskViewTaskController).mBounds) + .isEqualTo(bounds); + } + + private SurfaceControl.Transaction createMockTransaction() { + SurfaceControl.Transaction transaction = mock(SurfaceControl.Transaction.class); + when(transaction.reparent(any(), any())).thenReturn(transaction); + when(transaction.setPosition(any(), anyFloat(), anyFloat())).thenReturn(transaction); + when(transaction.setWindowCrop(any(), anyInt(), anyInt())).thenReturn(transaction); + return transaction; + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java index 6996d44af034..2dab39184247 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java @@ -100,7 +100,8 @@ public class DefaultTransitionHandlerTest extends ShellTestCase { mTransitionHandler = new DefaultTransitionHandler( mContext, mShellInit, mDisplayController, mDisplayInsetsController, mTransactionPool, mMainExecutor, mMainHandler, mAnimExecutor, - mRootTaskDisplayAreaOrganizer, mock(InteractionJankMonitor.class)); + mock(Handler.class), mRootTaskDisplayAreaOrganizer, + mock(InteractionJankMonitor.class)); mShellInit.init(); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java index 52634c08dafd..5d77766dc0db 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java @@ -88,6 +88,7 @@ public class HomeTransitionObserverTest extends ShellTestCase { private final ShellExecutor mAnimExecutor = new TestShellExecutor(); private final TestShellExecutor mMainExecutor = new TestShellExecutor(); private final Handler mMainHandler = new Handler(Looper.getMainLooper()); + private final Handler mAnimHandler = mock(Handler.class); private final DisplayController mDisplayController = mock(DisplayController.class); private final DisplayInsetsController mDisplayInsetsController = mock(DisplayInsetsController.class); @@ -105,7 +106,7 @@ public class HomeTransitionObserverTest extends ShellTestCase { mDisplayInsetsController, mock(ShellInit.class)); mTransition = new Transitions(mContext, mock(ShellInit.class), mock(ShellController.class), mOrganizer, mTransactionPool, mDisplayController, mDisplayInsetsController, - mMainExecutor, mMainHandler, mAnimExecutor, mHomeTransitionObserver, + mMainExecutor, mMainHandler, mAnimExecutor, mAnimHandler, mHomeTransitionObserver, mock(FocusTransitionObserver.class)); mHomeTransitionObserver.setHomeTransitionListener(mTransition, mListener); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index 44bb2154f170..4dd9cab1d340 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -146,6 +146,7 @@ public class ShellTransitionTests extends ShellTestCase { private final ShellExecutor mAnimExecutor = new TestShellExecutor(); private final TestTransitionHandler mDefaultHandler = new TestTransitionHandler(); private final Handler mMainHandler = new Handler(Looper.getMainLooper()); + private final Handler mAnimHandler = mock(Handler.class); private final DisplayInsetsController mDisplayInsets = mock(DisplayInsetsController.class); @@ -160,7 +161,7 @@ public class ShellTransitionTests extends ShellTestCase { ShellInit shellInit = mock(ShellInit.class); final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer, mTransactionPool, createTestDisplayController(), mDisplayInsets, - mMainExecutor, mMainHandler, mAnimExecutor, + mMainExecutor, mMainHandler, mAnimExecutor, mAnimHandler, mock(HomeTransitionObserver.class), mock(FocusTransitionObserver.class)); // One from Transitions, one from RootTaskDisplayAreaOrganizer verify(shellInit).addInitCallback(any(), eq(t)); @@ -173,7 +174,7 @@ public class ShellTransitionTests extends ShellTestCase { ShellController shellController = mock(ShellController.class); final Transitions t = new Transitions(mContext, shellInit, shellController, mOrganizer, mTransactionPool, createTestDisplayController(), mDisplayInsets, - mMainExecutor, mMainHandler, mAnimExecutor, + mMainExecutor, mMainHandler, mAnimExecutor, mAnimHandler, mock(HomeTransitionObserver.class), mock(FocusTransitionObserver.class)); shellInit.init(); verify(shellController, times(1)).addExternalInterface( @@ -1318,7 +1319,7 @@ public class ShellTransitionTests extends ShellTestCase { final Transitions transitions = new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer, mTransactionPool, createTestDisplayController(), mDisplayInsets, - mMainExecutor, mMainHandler, mAnimExecutor, + mMainExecutor, mMainHandler, mAnimExecutor, mAnimHandler, mock(HomeTransitionObserver.class), mock(FocusTransitionObserver.class)); final RecentTasksController mockRecentsTaskController = mock(RecentTasksController.class); @@ -1914,7 +1915,8 @@ public class ShellTransitionTests extends ShellTestCase { ShellInit shellInit = new ShellInit(mMainExecutor); final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer, mTransactionPool, createTestDisplayController(), mDisplayInsets, - mMainExecutor, mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class), + mMainExecutor, mMainHandler, mAnimExecutor, mAnimHandler, + mock(HomeTransitionObserver.class), mock(FocusTransitionObserver.class)); shellInit.init(); return t; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java index aad18cba4436..e28d0acb579f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java @@ -60,6 +60,7 @@ import org.mockito.InOrder; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.concurrent.Executor; public class UnfoldTransitionHandlerTest extends ShellTestCase { @@ -98,7 +99,8 @@ public class UnfoldTransitionHandlerTest extends ShellTestCase { mTransactionPool, executor, mHandler, - mTransitions + mTransitions, + /* bubbleTaskUnfoldTransitionMerger= */ Optional.empty() ); shellInit.init(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt index b1f92411c5a3..067dcec5d65d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt @@ -28,7 +28,6 @@ import android.testing.TestableLooper.RunWithLooper import android.view.Display import android.view.Display.DEFAULT_DISPLAY import android.view.SurfaceControl -import android.view.WindowManager.TRANSIT_CHANGE import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn @@ -110,7 +109,7 @@ class DesktopModeWindowDecorViewModelAppHandleOnlyTest : onTaskOpening(task, taskSurface) assertTrue(windowDecorByTaskIdSpy.contains(task.taskId)) task.setActivityType(ACTIVITY_TYPE_UNDEFINED) - onTaskChanging(task, taskSurface, TRANSIT_CHANGE) + onTaskChanging(task, taskSurface) assertFalse(windowDecorByTaskIdSpy.contains(task.taskId)) verify(decoration).close() @@ -166,7 +165,7 @@ class DesktopModeWindowDecorViewModelAppHandleOnlyTest : setLargeScreen(false) setUpMockDecorationForTask(task) - onTaskChanging(task, taskSurface, TRANSIT_CHANGE) + onTaskChanging(task, taskSurface) assertFalse(windowDecorByTaskIdSpy.contains(task.taskId)) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index ad3426e82805..40aa41b2b72a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -51,7 +51,6 @@ import android.view.SurfaceView import android.view.View import android.view.ViewRootImpl import android.view.WindowInsets.Type.statusBars -import android.view.WindowManager.TRANSIT_CHANGE import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.HierarchyOp import androidx.test.filters.SmallTest @@ -135,7 +134,7 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest task.setWindowingMode(WINDOWING_MODE_UNDEFINED) task.setActivityType(ACTIVITY_TYPE_UNDEFINED) - onTaskChanging(task, taskSurface, TRANSIT_CHANGE) + onTaskChanging(task, taskSurface) assertFalse(windowDecorByTaskIdSpy.contains(task.taskId)) verify(decoration).close() @@ -150,12 +149,12 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest val taskSurface = SurfaceControl() setUpMockDecorationForTask(task) - onTaskChanging(task, taskSurface, TRANSIT_CHANGE) + onTaskChanging(task, taskSurface) assertFalse(windowDecorByTaskIdSpy.contains(task.taskId)) task.setWindowingMode(WINDOWING_MODE_FREEFORM) task.setActivityType(ACTIVITY_TYPE_STANDARD) - onTaskChanging(task, taskSurface, TRANSIT_CHANGE) + onTaskChanging(task, taskSurface) assertTrue(windowDecorByTaskIdSpy.contains(task.taskId)) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt index 2126d1d9b986..23994a2bd547 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt @@ -67,6 +67,7 @@ import com.android.wm.shell.desktopmode.DesktopUserRepositories import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository import com.android.wm.shell.desktopmode.education.AppHandleEducationController import com.android.wm.shell.desktopmode.education.AppToWebEducationController +import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer import com.android.wm.shell.freeform.FreeformTaskTransitionStarter import com.android.wm.shell.recents.RecentsTransitionHandler import com.android.wm.shell.recents.RecentsTransitionStateListener @@ -146,6 +147,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { protected val mockMultiDisplayDragMoveIndicatorController = mock<MultiDisplayDragMoveIndicatorController>() protected val mockCompatUIHandler = mock<CompatUIHandler>() + protected val mockDesksOrganizer = mock<DesksOrganizer>() protected val mockInputManager = mock<InputManager>() private val mockTaskPositionerFactory = mock<DesktopModeWindowDecorViewModel.TaskPositionerFactory>() @@ -246,6 +248,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { mockTilingWindowDecoration, mockMultiDisplayDragMoveIndicatorController, mockCompatUIHandler, + mockDesksOrganizer ) desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController) whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout) @@ -362,14 +365,12 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { protected fun onTaskChanging( task: RunningTaskInfo, leash: SurfaceControl = SurfaceControl(), - changeMode: Int ) { desktopModeWindowDecorViewModel.onTaskChanging( task, leash, StubTransaction(), StubTransaction(), - changeMode ) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 0908f56e1cfb..a0171ea04da3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -174,8 +174,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private static final boolean DEFAULT_HAS_GLOBAL_FOCUS = true; private static final boolean DEFAULT_SHOULD_IGNORE_CORNER_RADIUS = false; private static final boolean DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS = false; - private static final boolean DEFAULT_IS_RECENTS_TRANSITION_RUNNING = false; - private static final boolean DEFAULT_IS_MOVING_TO_BACK = false; @Mock @@ -443,9 +441,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, /* shouldIgnoreCornerRadius= */ true, - DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, - DEFAULT_IS_RECENTS_TRANSITION_RUNNING, - DEFAULT_IS_MOVING_TO_BACK); + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); assertThat(relayoutParams.mCornerRadius).isEqualTo(INVALID_CORNER_RADIUS); } @@ -544,9 +540,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, /* shouldIgnoreCornerRadius= */ true, - DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, - DEFAULT_IS_RECENTS_TRANSITION_RUNNING, - DEFAULT_IS_MOVING_TO_BACK); + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); assertThat(relayoutParams.mCornerRadiusId).isEqualTo(Resources.ID_NULL); } @@ -748,9 +742,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, - /* shouldExcludeCaptionFromAppBounds */ true, - DEFAULT_IS_RECENTS_TRANSITION_RUNNING, - DEFAULT_IS_MOVING_TO_BACK); + /* shouldExcludeCaptionFromAppBounds */ true); // Force consuming flags are disabled. assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) == 0).isTrue(); @@ -785,9 +777,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, - DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, - DEFAULT_IS_RECENTS_TRANSITION_RUNNING, - DEFAULT_IS_MOVING_TO_BACK); + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) != 0).isTrue(); assertThat( @@ -866,9 +856,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, - DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, - DEFAULT_IS_RECENTS_TRANSITION_RUNNING, - DEFAULT_IS_MOVING_TO_BACK); + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); // Takes status bar inset as padding, ignores caption bar inset. assertThat(relayoutParams.mCaptionTopPadding).isEqualTo(50); @@ -896,9 +884,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, - DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, - DEFAULT_IS_RECENTS_TRANSITION_RUNNING, - DEFAULT_IS_MOVING_TO_BACK); + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); assertThat(relayoutParams.mIsInsetSource).isFalse(); } @@ -925,9 +911,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, - DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, - DEFAULT_IS_RECENTS_TRANSITION_RUNNING, - DEFAULT_IS_MOVING_TO_BACK); + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); // Header is always shown because it's assumed the status bar is always visible. assertThat(relayoutParams.mIsCaptionVisible).isTrue(); @@ -954,9 +938,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, - DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, - DEFAULT_IS_RECENTS_TRANSITION_RUNNING, - DEFAULT_IS_MOVING_TO_BACK); + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); assertThat(relayoutParams.mIsCaptionVisible).isTrue(); } @@ -982,9 +964,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, - DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, - DEFAULT_IS_RECENTS_TRANSITION_RUNNING, - DEFAULT_IS_MOVING_TO_BACK); + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -1010,9 +990,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, - DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, - DEFAULT_IS_RECENTS_TRANSITION_RUNNING, - DEFAULT_IS_MOVING_TO_BACK); + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -1039,9 +1017,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, - DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, - DEFAULT_IS_RECENTS_TRANSITION_RUNNING, - DEFAULT_IS_MOVING_TO_BACK); + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); assertThat(relayoutParams.mIsCaptionVisible).isTrue(); @@ -1060,9 +1036,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, - DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, - DEFAULT_IS_RECENTS_TRANSITION_RUNNING, - DEFAULT_IS_MOVING_TO_BACK); + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -1089,9 +1063,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, - DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, - DEFAULT_IS_RECENTS_TRANSITION_RUNNING, - DEFAULT_IS_MOVING_TO_BACK); + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); assertThat(relayoutParams.mIsCaptionVisible).isTrue(); } @@ -1118,9 +1090,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, - DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, - DEFAULT_IS_RECENTS_TRANSITION_RUNNING, - DEFAULT_IS_MOVING_TO_BACK); + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -1151,65 +1121,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { assertThat(relayoutParams.mAsyncViewHost).isFalse(); } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_INPUT_LAYER_TRANSITION_FIX) - public void updateRelayoutParams_handle_movingToBack_captionNotVisible() { - final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); - taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); - final RelayoutParams relayoutParams = new RelayoutParams(); - - DesktopModeWindowDecoration.updateRelayoutParams( - relayoutParams, - mTestableContext, - taskInfo, - mMockSplitScreenController, - DEFAULT_APPLY_START_TRANSACTION_ON_DRAW, - DEFAULT_SHOULD_SET_TASK_POSITIONING_AND_CROP, - DEFAULT_IS_STATUSBAR_VISIBLE, - DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED, - DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, - DEFAULT_IS_DRAGGING, - new InsetsState(), - DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion, - DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, - DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, - DEFAULT_IS_RECENTS_TRANSITION_RUNNING, - /* isMovingToBack= */ true); - - assertThat(relayoutParams.mIsCaptionVisible).isFalse(); - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_INPUT_LAYER_TRANSITION_FIX) - public void updateRelayoutParams_handle_inRecentsTransition_captionNotVisible() { - final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); - taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); - final RelayoutParams relayoutParams = new RelayoutParams(); - - DesktopModeWindowDecoration.updateRelayoutParams( - relayoutParams, - mTestableContext, - taskInfo, - mMockSplitScreenController, - DEFAULT_APPLY_START_TRANSACTION_ON_DRAW, - DEFAULT_SHOULD_SET_TASK_POSITIONING_AND_CROP, - DEFAULT_IS_STATUSBAR_VISIBLE, - DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED, - DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, - DEFAULT_IS_DRAGGING, - new InsetsState(), - DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion, - DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, - DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, - /* isRecentsTransitionRunning= */ true, - DEFAULT_IS_MOVING_TO_BACK); - - assertThat(relayoutParams.mIsCaptionVisible).isFalse(); - } - @Test public void relayout_fullscreenTask_appliesTransactionImmediately() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); @@ -1846,9 +1757,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, - DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS, - DEFAULT_IS_RECENTS_TRANSITION_RUNNING, - DEFAULT_IS_MOVING_TO_BACK); + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); } private DesktopModeWindowDecoration createWindowDecoration( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeInputListenerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeInputListenerTest.kt index 360099777bde..95ae73cde19e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeInputListenerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeInputListenerTest.kt @@ -40,6 +40,7 @@ import com.android.wm.shell.windowdecor.DragResizeInputListener.TaskResizeInputE import com.google.common.truth.Truth.assertThat import java.util.function.Consumer import java.util.function.Supplier +import kotlin.test.assertNotNull import org.junit.After import org.junit.Test import org.junit.runner.RunWith @@ -69,9 +70,12 @@ class DragResizeInputListenerTest : ShellTestCase() { private val sinkInputChannel = mock<InputChannel>() private val decorationSurface = SurfaceControl.Builder().setName("decoration surface").build() private val createdSurfaces = ArrayList<SurfaceControl>() + private val removedSurfaces = ArrayList<SurfaceControl>() @After fun tearDown() { + createdSurfaces.clear() + removedSurfaces.clear() decorationSurface.release() } @@ -217,6 +221,19 @@ class DragResizeInputListenerTest : ShellTestCase() { assertThat(createdSurfaces[1].isValid).isFalse() } + @Test + fun testClose_releasesDecorationSurfaceWithoutRemoval() { + val inputListener = create() + testBgExecutor.flushAll() + inputListener.close() + testMainExecutor.flushAll() + testBgExecutor.flushAll() + + val decorationSurface = assertNotNull(createdSurfaces[0]) + assertThat(decorationSurface.isValid).isFalse() + assertThat(removedSurfaces.contains(decorationSurface)).isFalse() + } + private fun verifyNoInputChannelGrantRequests() { verify(mockWindowSession, never()) .grantInputChannel( @@ -258,7 +275,10 @@ class DragResizeInputListenerTest : ShellTestCase() { { object : StubTransaction() { override fun remove(sc: SurfaceControl): SurfaceControl.Transaction { - return super.remove(sc).also { sc.release() } + return super.remove(sc).also { + sc.release() + removedSurfaces.add(sc) + } } } }, diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp index e09ab5fd1643..6caae4c7623e 100644 --- a/libs/androidfw/AssetManager2.cpp +++ b/libs/androidfw/AssetManager2.cpp @@ -82,6 +82,9 @@ struct FindEntryResult { // The bitmask of configuration axis with which the resource value varies. uint32_t type_flags; + // The bitmask of ResTable_entry flags + uint16_t entry_flags; + // The dynamic package ID map for the package from which this resource came from. const DynamicRefTable* dynamic_ref_table; @@ -1031,6 +1034,7 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntryInternal( .entry = *entry, .config = *best_config, .type_flags = type_flags, + .entry_flags = (*best_entry_verified)->flags(), .dynamic_ref_table = package_group.dynamic_ref_table.get(), .package_name = &best_package->GetPackageName(), .type_string_ref = StringPoolRef(best_package->GetTypeStringPool(), best_type->id - 1), @@ -1185,16 +1189,16 @@ base::expected<AssetManager2::SelectedValue, NullOrIOError> AssetManager2::GetRe } // Create a reference since we can't represent this complex type as a Res_value. - return SelectedValue(Res_value::TYPE_REFERENCE, resid, result->cookie, result->type_flags, - resid, result->config); + return SelectedValue(Res_value::TYPE_REFERENCE, resid, result->cookie, result->entry_flags, + result->type_flags, resid, result->config); } // Convert the package ID to the runtime assigned package ID. Res_value value = std::get<Res_value>(result->entry); result->dynamic_ref_table->lookupResourceValue(&value); - return SelectedValue(value.dataType, value.data, result->cookie, result->type_flags, - resid, result->config); + return SelectedValue(value.dataType, value.data, result->cookie, result->entry_flags, + result->type_flags, resid, result->config); } base::expected<std::monostate, NullOrIOError> AssetManager2::ResolveReference( @@ -1847,8 +1851,8 @@ std::optional<AssetManager2::SelectedValue> Theme::GetAttribute(uint32_t resid) } return AssetManager2::SelectedValue(entry_it->value.dataType, entry_it->value.data, - entry_it->cookie, type_spec_flags, 0U /* resid */, - {} /* config */); + entry_it->cookie, 0U /* entry flags*/, type_spec_flags, + 0U /* resid */, {} /* config */); } return std::nullopt; } diff --git a/libs/androidfw/include/androidfw/AssetManager2.h b/libs/androidfw/include/androidfw/AssetManager2.h index b0179524f6cd..ffcef944a6ba 100644 --- a/libs/androidfw/include/androidfw/AssetManager2.h +++ b/libs/androidfw/include/androidfw/AssetManager2.h @@ -257,6 +257,7 @@ class AssetManager2 { : cookie(entry.cookie), data(entry.value.data), type(entry.value.dataType), + entry_flags(0U), flags(bag->type_spec_flags), resid(0U), config() { @@ -271,6 +272,9 @@ class AssetManager2 { // Type of the data value. uint8_t type; + // The bitmask of ResTable_entry flags + uint16_t entry_flags; + // The bitmask of configuration axis that this resource varies with. // See ResTable_config::CONFIG_*. uint32_t flags; @@ -283,9 +287,10 @@ class AssetManager2 { private: SelectedValue(uint8_t value_type, Res_value::data_type value_data, ApkAssetsCookie cookie, - uint32_t type_flags, uint32_t resid, ResTable_config config) : - cookie(cookie), data(value_data), type(value_type), flags(type_flags), - resid(resid), config(std::move(config)) {} + uint16_t entry_flags, uint32_t type_flags, uint32_t resid, ResTable_config config) + : + cookie(cookie), data(value_data), type(value_type), entry_flags(entry_flags), + flags(type_flags), resid(resid), config(std::move(config)) {} }; // Retrieves the best matching resource value with ID `resid`. diff --git a/libs/androidfw/tests/AssetManager2_test.cpp b/libs/androidfw/tests/AssetManager2_test.cpp index 3f228841f6ba..948437230ecc 100644 --- a/libs/androidfw/tests/AssetManager2_test.cpp +++ b/libs/androidfw/tests/AssetManager2_test.cpp @@ -23,6 +23,7 @@ #include "androidfw/ResourceUtils.h" #include "data/appaslib/R.h" #include "data/basic/R.h" +#include "data/flagged/R.h" #include "data/lib_one/R.h" #include "data/lib_two/R.h" #include "data/libclient/R.h" @@ -32,6 +33,7 @@ namespace app = com::android::app; namespace appaslib = com::android::appaslib::app; namespace basic = com::android::basic; +namespace flagged = com::android::flagged; namespace lib_one = com::android::lib_one; namespace lib_two = com::android::lib_two; namespace libclient = com::android::libclient; @@ -87,6 +89,10 @@ class AssetManager2Test : public ::testing::Test { overlayable_assets_ = ApkAssets::Load("overlayable/overlayable.apk"); ASSERT_THAT(overlayable_assets_, NotNull()); + + flagged_assets_ = ApkAssets::Load("flagged/flagged.apk"); + ASSERT_THAT(app_assets_, NotNull()); + chdir(original_path.c_str()); } @@ -104,6 +110,7 @@ class AssetManager2Test : public ::testing::Test { AssetManager2::ApkAssetsPtr app_assets_; AssetManager2::ApkAssetsPtr overlay_assets_; AssetManager2::ApkAssetsPtr overlayable_assets_; + AssetManager2::ApkAssetsPtr flagged_assets_; }; TEST_F(AssetManager2Test, FindsResourceFromSingleApkAssets) { @@ -856,4 +863,12 @@ TEST_F(AssetManager2Test, GetApkAssets) { EXPECT_EQ(1, lib_one_assets_->getStrongCount()); } +TEST_F(AssetManager2Test, GetFlaggedAssets) { + AssetManager2 assetmanager; + assetmanager.SetApkAssets({flagged_assets_}); + auto value = assetmanager.GetResource(flagged::R::xml::flagged, false, 0); + ASSERT_TRUE(value.has_value()); + EXPECT_TRUE(value->entry_flags & ResTable_entry::FLAG_USES_FEATURE_FLAGS); +} + } // namespace android diff --git a/libs/androidfw/tests/data/flagged/AndroidManifest.xml b/libs/androidfw/tests/data/flagged/AndroidManifest.xml new file mode 100644 index 000000000000..cc1394328797 --- /dev/null +++ b/libs/androidfw/tests/data/flagged/AndroidManifest.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.basic"> + <application /> +</manifest> diff --git a/libs/androidfw/tests/data/flagged/R.h b/libs/androidfw/tests/data/flagged/R.h new file mode 100644 index 000000000000..33ccab28cdd3 --- /dev/null +++ b/libs/androidfw/tests/data/flagged/R.h @@ -0,0 +1,35 @@ +/* +* Copyright (C) 2025 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#pragma once + +#include <cstdint> + +namespace com { +namespace android { +namespace flagged { + +struct R { + struct xml { + enum : uint32_t { + flagged = 0x7f010000, + }; + }; +}; + +} // namespace flagged +} // namespace android +} // namespace com
\ No newline at end of file diff --git a/libs/androidfw/tests/data/flagged/build b/libs/androidfw/tests/data/flagged/build new file mode 100755 index 000000000000..9e5d21ba1833 --- /dev/null +++ b/libs/androidfw/tests/data/flagged/build @@ -0,0 +1,28 @@ +#!/bin/bash +# +# Copyright (C) 2025 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +set -e + +PATH_TO_FRAMEWORK_RES=${ANDROID_BUILD_TOP}/prebuilts/sdk/current/public/android.jar + +aapt2 compile --dir res -o compiled.flata +aapt2 link -o flagged.apk \ + --manifest AndroidManifest.xml \ + -I $PATH_TO_FRAMEWORK_RES \ + -I ../basic/basic.apk \ + compiled.flata +rm compiled.flata diff --git a/libs/androidfw/tests/data/flagged/flagged.apk b/libs/androidfw/tests/data/flagged/flagged.apk Binary files differnew file mode 100644 index 000000000000..94b8f4d9fcf0 --- /dev/null +++ b/libs/androidfw/tests/data/flagged/flagged.apk diff --git a/libs/androidfw/tests/data/flagged/res/xml/flagged.xml b/libs/androidfw/tests/data/flagged/res/xml/flagged.xml new file mode 100644 index 000000000000..5fe8d1b3ac27 --- /dev/null +++ b/libs/androidfw/tests/data/flagged/res/xml/flagged.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<first xmlns:android="http://schemas.android.com/apk/res/android"> + <second android:featureFlag="android.content.res.always_false"/> +</first>
\ No newline at end of file diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index ab1be7e6128d..1bde5ff43aa8 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -168,6 +168,14 @@ cc_defaults { "libutils", ], }, + host_linux: { + shared_libs: [ + "libaconfig_storage_read_api_cc", + ], + whole_static_libs: [ + "hwui_flags_cc_lib", + ], + }, }, } diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp index 3ef970830dc4..46dcc1f4c50b 100644 --- a/libs/hwui/hwui/Bitmap.cpp +++ b/libs/hwui/hwui/Bitmap.cpp @@ -164,8 +164,13 @@ std::string Bitmap::getAshmemId(const char* tag, uint64_t bitmapId, android::base::ReadFileToString("/proc/self/cmdline", &temp); return temp; }(); - return std::format("bitmap/{}-id_{}-{}x{}-size_{}-{}", - tag, bitmapId, width, height, size, sCmdline); + /* counter is to ensure the uniqueness of the ashmem filename, + * e.g. a bitmap with same mId could be sent multiple times, an + * ashmem region is created each time + */ + static std::atomic<uint32_t> counter{0}; + return std::format("bitmap/{}_{}_{}x{}_size-{}_id-{}_{}", + tag, counter.fetch_add(1), width, height, size, bitmapId, sCmdline); } sk_sp<Bitmap> Bitmap::allocateAshmemBitmap(SkBitmap* bitmap) { diff --git a/libs/hwui/jni/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp index b6a2ad7064a9..1a258e022dd0 100644 --- a/libs/hwui/jni/Bitmap.cpp +++ b/libs/hwui/jni/Bitmap.cpp @@ -2,6 +2,9 @@ #include "Bitmap.h" #include <android-base/unique_fd.h> +#ifdef __linux__ +#include <com_android_graphics_hwui_flags.h> +#endif #include <hwui/Bitmap.h> #include <hwui/Paint.h> #include <inttypes.h> @@ -33,15 +36,6 @@ #endif #include "android_nio_utils.h" -#ifdef __ANDROID__ -#include <com_android_graphics_hwui_flags.h> -namespace hwui_flags = com::android::graphics::hwui::flags; -#else -namespace hwui_flags { -constexpr bool bitmap_parcel_ashmem_as_immutable() { return false; } -} -#endif - #define DEBUG_PARCEL 0 static jclass gBitmap_class; @@ -861,7 +855,7 @@ static bool shouldParcelAsMutable(SkBitmap& bitmap, AParcel* parcel) { return false; } - if (!hwui_flags::bitmap_parcel_ashmem_as_immutable()) { + if (!com::android::graphics::hwui::flags::bitmap_parcel_ashmem_as_immutable()) { return true; } diff --git a/location/api/system-current.txt b/location/api/system-current.txt index 47984745fafc..8026d4662cb9 100644 --- a/location/api/system-current.txt +++ b/location/api/system-current.txt @@ -32,7 +32,7 @@ package android.location { @FlaggedApi("android.location.flags.gnss_assistance_interface") public final class BeidouAssistance implements android.os.Parcelable { method public int describeContents(); method @Nullable public android.location.GnssAlmanac getAlmanac(); - method @Nullable public android.location.AuxiliaryInformation getAuxiliaryInformation(); + method @NonNull public java.util.List<android.location.AuxiliaryInformation> getAuxiliaryInformation(); method @Nullable public android.location.KlobucharIonosphericModel getIonosphericModel(); method @Nullable public android.location.LeapSecondsModel getLeapSecondsModel(); method @NonNull public java.util.List<android.location.RealTimeIntegrityModel> getRealTimeIntegrityModels(); @@ -48,7 +48,7 @@ package android.location { ctor public BeidouAssistance.Builder(); method @NonNull public android.location.BeidouAssistance build(); method @NonNull public android.location.BeidouAssistance.Builder setAlmanac(@Nullable android.location.GnssAlmanac); - method @NonNull public android.location.BeidouAssistance.Builder setAuxiliaryInformation(@Nullable android.location.AuxiliaryInformation); + method @NonNull public android.location.BeidouAssistance.Builder setAuxiliaryInformation(@NonNull java.util.List<android.location.AuxiliaryInformation>); method @NonNull public android.location.BeidouAssistance.Builder setIonosphericModel(@Nullable android.location.KlobucharIonosphericModel); method @NonNull public android.location.BeidouAssistance.Builder setLeapSecondsModel(@Nullable android.location.LeapSecondsModel); method @NonNull public android.location.BeidouAssistance.Builder setRealTimeIntegrityModels(@NonNull java.util.List<android.location.RealTimeIntegrityModel>); @@ -176,7 +176,7 @@ package android.location { @FlaggedApi("android.location.flags.gnss_assistance_interface") public final class GalileoAssistance implements android.os.Parcelable { method public int describeContents(); method @Nullable public android.location.GnssAlmanac getAlmanac(); - method @Nullable public android.location.AuxiliaryInformation getAuxiliaryInformation(); + method @NonNull public java.util.List<android.location.AuxiliaryInformation> getAuxiliaryInformation(); method @Nullable public android.location.GalileoIonosphericModel getIonosphericModel(); method @Nullable public android.location.LeapSecondsModel getLeapSecondsModel(); method @NonNull public java.util.List<android.location.RealTimeIntegrityModel> getRealTimeIntegrityModels(); @@ -192,7 +192,7 @@ package android.location { ctor public GalileoAssistance.Builder(); method @NonNull public android.location.GalileoAssistance build(); method @NonNull public android.location.GalileoAssistance.Builder setAlmanac(@Nullable android.location.GnssAlmanac); - method @NonNull public android.location.GalileoAssistance.Builder setAuxiliaryInformation(@Nullable android.location.AuxiliaryInformation); + method @NonNull public android.location.GalileoAssistance.Builder setAuxiliaryInformation(@NonNull java.util.List<android.location.AuxiliaryInformation>); method @NonNull public android.location.GalileoAssistance.Builder setIonosphericModel(@Nullable android.location.GalileoIonosphericModel); method @NonNull public android.location.GalileoAssistance.Builder setLeapSecondsModel(@Nullable android.location.LeapSecondsModel); method @NonNull public android.location.GalileoAssistance.Builder setRealTimeIntegrityModels(@NonNull java.util.List<android.location.RealTimeIntegrityModel>); @@ -346,7 +346,8 @@ package android.location { @FlaggedApi("android.location.flags.gnss_assistance_interface") public final class GlonassAssistance implements android.os.Parcelable { method public int describeContents(); method @Nullable public android.location.GlonassAlmanac getAlmanac(); - method @Nullable public android.location.AuxiliaryInformation getAuxiliaryInformation(); + method @NonNull public java.util.List<android.location.AuxiliaryInformation> getAuxiliaryInformation(); + method @NonNull public java.util.List<android.location.RealTimeIntegrityModel> getRealTimeIntegrityModels(); method @NonNull public java.util.List<android.location.GnssAssistance.GnssSatelliteCorrections> getSatelliteCorrections(); method @NonNull public java.util.List<android.location.GlonassSatelliteEphemeris> getSatelliteEphemeris(); method @NonNull public java.util.List<android.location.TimeModel> getTimeModels(); @@ -359,7 +360,8 @@ package android.location { ctor public GlonassAssistance.Builder(); method @NonNull public android.location.GlonassAssistance build(); method @NonNull public android.location.GlonassAssistance.Builder setAlmanac(@Nullable android.location.GlonassAlmanac); - method @NonNull public android.location.GlonassAssistance.Builder setAuxiliaryInformation(@Nullable android.location.AuxiliaryInformation); + method @NonNull public android.location.GlonassAssistance.Builder setAuxiliaryInformation(@NonNull java.util.List<android.location.AuxiliaryInformation>); + method @NonNull public android.location.GlonassAssistance.Builder setRealTimeIntegrityModels(@NonNull java.util.List<android.location.RealTimeIntegrityModel>); method @NonNull public android.location.GlonassAssistance.Builder setSatelliteCorrections(@NonNull java.util.List<android.location.GnssAssistance.GnssSatelliteCorrections>); method @NonNull public android.location.GlonassAssistance.Builder setSatelliteEphemeris(@NonNull java.util.List<android.location.GlonassSatelliteEphemeris>); method @NonNull public android.location.GlonassAssistance.Builder setTimeModels(@NonNull java.util.List<android.location.TimeModel>); @@ -717,7 +719,7 @@ package android.location { @FlaggedApi("android.location.flags.gnss_assistance_interface") public final class GpsAssistance implements android.os.Parcelable { method public int describeContents(); method @Nullable public android.location.GnssAlmanac getAlmanac(); - method @Nullable public android.location.AuxiliaryInformation getAuxiliaryInformation(); + method @NonNull public java.util.List<android.location.AuxiliaryInformation> getAuxiliaryInformation(); method @Nullable public android.location.KlobucharIonosphericModel getIonosphericModel(); method @Nullable public android.location.LeapSecondsModel getLeapSecondsModel(); method @NonNull public java.util.List<android.location.RealTimeIntegrityModel> getRealTimeIntegrityModels(); @@ -733,7 +735,7 @@ package android.location { ctor public GpsAssistance.Builder(); method @NonNull public android.location.GpsAssistance build(); method @NonNull public android.location.GpsAssistance.Builder setAlmanac(@Nullable android.location.GnssAlmanac); - method @NonNull public android.location.GpsAssistance.Builder setAuxiliaryInformation(@Nullable android.location.AuxiliaryInformation); + method @NonNull public android.location.GpsAssistance.Builder setAuxiliaryInformation(@NonNull java.util.List<android.location.AuxiliaryInformation>); method @NonNull public android.location.GpsAssistance.Builder setIonosphericModel(@Nullable android.location.KlobucharIonosphericModel); method @NonNull public android.location.GpsAssistance.Builder setLeapSecondsModel(@Nullable android.location.LeapSecondsModel); method @NonNull public android.location.GpsAssistance.Builder setRealTimeIntegrityModels(@NonNull java.util.List<android.location.RealTimeIntegrityModel>); @@ -1253,7 +1255,7 @@ package android.location { @FlaggedApi("android.location.flags.gnss_assistance_interface") public final class QzssAssistance implements android.os.Parcelable { method public int describeContents(); method @Nullable public android.location.GnssAlmanac getAlmanac(); - method @Nullable public android.location.AuxiliaryInformation getAuxiliaryInformation(); + method @NonNull public java.util.List<android.location.AuxiliaryInformation> getAuxiliaryInformation(); method @Nullable public android.location.KlobucharIonosphericModel getIonosphericModel(); method @Nullable public android.location.LeapSecondsModel getLeapSecondsModel(); method @NonNull public java.util.List<android.location.RealTimeIntegrityModel> getRealTimeIntegrityModels(); @@ -1269,7 +1271,7 @@ package android.location { ctor public QzssAssistance.Builder(); method @NonNull public android.location.QzssAssistance build(); method @NonNull public android.location.QzssAssistance.Builder setAlmanac(@Nullable android.location.GnssAlmanac); - method @NonNull public android.location.QzssAssistance.Builder setAuxiliaryInformation(@Nullable android.location.AuxiliaryInformation); + method @NonNull public android.location.QzssAssistance.Builder setAuxiliaryInformation(@NonNull java.util.List<android.location.AuxiliaryInformation>); method @NonNull public android.location.QzssAssistance.Builder setIonosphericModel(@Nullable android.location.KlobucharIonosphericModel); method @NonNull public android.location.QzssAssistance.Builder setLeapSecondsModel(@Nullable android.location.LeapSecondsModel); method @NonNull public android.location.QzssAssistance.Builder setRealTimeIntegrityModels(@NonNull java.util.List<android.location.RealTimeIntegrityModel>); diff --git a/location/java/android/location/BeidouAssistance.java b/location/java/android/location/BeidouAssistance.java index e35493ed1007..274332dab9a8 100644 --- a/location/java/android/location/BeidouAssistance.java +++ b/location/java/android/location/BeidouAssistance.java @@ -50,8 +50,8 @@ public final class BeidouAssistance implements Parcelable { /** The leap seconds model. */ @Nullable private final LeapSecondsModel mLeapSecondsModel; - /** The auxiliary information. */ - @Nullable private final AuxiliaryInformation mAuxiliaryInformation; + /** The list of auxiliary informations. */ + @NonNull private final List<AuxiliaryInformation> mAuxiliaryInformation; /** The list of time models. */ @NonNull private final List<TimeModel> mTimeModels; @@ -70,7 +70,12 @@ public final class BeidouAssistance implements Parcelable { mIonosphericModel = builder.mIonosphericModel; mUtcModel = builder.mUtcModel; mLeapSecondsModel = builder.mLeapSecondsModel; - mAuxiliaryInformation = builder.mAuxiliaryInformation; + if (builder.mAuxiliaryInformation != null) { + mAuxiliaryInformation = + Collections.unmodifiableList(new ArrayList<>(builder.mAuxiliaryInformation)); + } else { + mAuxiliaryInformation = new ArrayList<>(); + } if (builder.mTimeModels != null) { mTimeModels = Collections.unmodifiableList(new ArrayList<>(builder.mTimeModels)); } else { @@ -120,9 +125,9 @@ public final class BeidouAssistance implements Parcelable { return mLeapSecondsModel; } - /** Returns the auxiliary information. */ - @Nullable - public AuxiliaryInformation getAuxiliaryInformation() { + /** Returns the list of auxiliary informations. */ + @NonNull + public List<AuxiliaryInformation> getAuxiliaryInformation() { return mAuxiliaryInformation; } @@ -178,7 +183,7 @@ public final class BeidouAssistance implements Parcelable { dest.writeTypedObject(mIonosphericModel, flags); dest.writeTypedObject(mUtcModel, flags); dest.writeTypedObject(mLeapSecondsModel, flags); - dest.writeTypedObject(mAuxiliaryInformation, flags); + dest.writeTypedList(mAuxiliaryInformation); dest.writeTypedList(mTimeModels); dest.writeTypedList(mSatelliteEphemeris); dest.writeTypedList(mRealTimeIntegrityModels); @@ -196,7 +201,7 @@ public final class BeidouAssistance implements Parcelable { .setUtcModel(in.readTypedObject(UtcModel.CREATOR)) .setLeapSecondsModel(in.readTypedObject(LeapSecondsModel.CREATOR)) .setAuxiliaryInformation( - in.readTypedObject(AuxiliaryInformation.CREATOR)) + in.createTypedArrayList(AuxiliaryInformation.CREATOR)) .setTimeModels(in.createTypedArrayList(TimeModel.CREATOR)) .setSatelliteEphemeris( in.createTypedArrayList(BeidouSatelliteEphemeris.CREATOR)) @@ -219,7 +224,7 @@ public final class BeidouAssistance implements Parcelable { private KlobucharIonosphericModel mIonosphericModel; private UtcModel mUtcModel; private LeapSecondsModel mLeapSecondsModel; - private AuxiliaryInformation mAuxiliaryInformation; + private List<AuxiliaryInformation> mAuxiliaryInformation; private List<TimeModel> mTimeModels; private List<BeidouSatelliteEphemeris> mSatelliteEphemeris; private List<RealTimeIntegrityModel> mRealTimeIntegrityModels; @@ -253,10 +258,10 @@ public final class BeidouAssistance implements Parcelable { return this; } - /** Sets the auxiliary information. */ + /** Sets the list of auxiliary informations. */ @NonNull public Builder setAuxiliaryInformation( - @Nullable AuxiliaryInformation auxiliaryInformation) { + @NonNull List<AuxiliaryInformation> auxiliaryInformation) { mAuxiliaryInformation = auxiliaryInformation; return this; } diff --git a/location/java/android/location/GalileoAssistance.java b/location/java/android/location/GalileoAssistance.java index 7f81ccdf346f..f73ce400dd9d 100644 --- a/location/java/android/location/GalileoAssistance.java +++ b/location/java/android/location/GalileoAssistance.java @@ -50,8 +50,8 @@ public final class GalileoAssistance implements Parcelable { /** The leap seconds model. */ @Nullable private final LeapSecondsModel mLeapSecondsModel; - /** The auxiliary information. */ - @Nullable private final AuxiliaryInformation mAuxiliaryInformation; + /** The list of auxiliary informations. */ + @NonNull private final List<AuxiliaryInformation> mAuxiliaryInformation; /** The list of time models. */ @NonNull private final List<TimeModel> mTimeModels; @@ -70,7 +70,12 @@ public final class GalileoAssistance implements Parcelable { mIonosphericModel = builder.mIonosphericModel; mUtcModel = builder.mUtcModel; mLeapSecondsModel = builder.mLeapSecondsModel; - mAuxiliaryInformation = builder.mAuxiliaryInformation; + if (builder.mAuxiliaryInformation != null) { + mAuxiliaryInformation = + Collections.unmodifiableList(new ArrayList<>(builder.mAuxiliaryInformation)); + } else { + mAuxiliaryInformation = new ArrayList<>(); + } if (builder.mTimeModels != null) { mTimeModels = Collections.unmodifiableList(new ArrayList<>(builder.mTimeModels)); } else { @@ -120,9 +125,9 @@ public final class GalileoAssistance implements Parcelable { return mLeapSecondsModel; } - /** Returns the auxiliary information. */ - @Nullable - public AuxiliaryInformation getAuxiliaryInformation() { + /** Returns the list of auxiliary informations. */ + @NonNull + public List<AuxiliaryInformation> getAuxiliaryInformation() { return mAuxiliaryInformation; } @@ -161,7 +166,7 @@ public final class GalileoAssistance implements Parcelable { dest.writeTypedObject(mIonosphericModel, flags); dest.writeTypedObject(mUtcModel, flags); dest.writeTypedObject(mLeapSecondsModel, flags); - dest.writeTypedObject(mAuxiliaryInformation, flags); + dest.writeTypedList(mAuxiliaryInformation); dest.writeTypedList(mTimeModels); dest.writeTypedList(mSatelliteEphemeris); dest.writeTypedList(mRealTimeIntegrityModels); @@ -196,7 +201,7 @@ public final class GalileoAssistance implements Parcelable { .setUtcModel(in.readTypedObject(UtcModel.CREATOR)) .setLeapSecondsModel(in.readTypedObject(LeapSecondsModel.CREATOR)) .setAuxiliaryInformation( - in.readTypedObject(AuxiliaryInformation.CREATOR)) + in.createTypedArrayList(AuxiliaryInformation.CREATOR)) .setTimeModels(in.createTypedArrayList(TimeModel.CREATOR)) .setSatelliteEphemeris( in.createTypedArrayList(GalileoSatelliteEphemeris.CREATOR)) @@ -219,7 +224,7 @@ public final class GalileoAssistance implements Parcelable { private GalileoIonosphericModel mIonosphericModel; private UtcModel mUtcModel; private LeapSecondsModel mLeapSecondsModel; - private AuxiliaryInformation mAuxiliaryInformation; + private List<AuxiliaryInformation> mAuxiliaryInformation; private List<TimeModel> mTimeModels; private List<GalileoSatelliteEphemeris> mSatelliteEphemeris; private List<RealTimeIntegrityModel> mRealTimeIntegrityModels; @@ -253,10 +258,10 @@ public final class GalileoAssistance implements Parcelable { return this; } - /** Sets the auxiliary information. */ + /** Sets the list of auxiliary informations. */ @NonNull public Builder setAuxiliaryInformation( - @Nullable AuxiliaryInformation auxiliaryInformation) { + @NonNull List<AuxiliaryInformation> auxiliaryInformation) { mAuxiliaryInformation = auxiliaryInformation; return this; } diff --git a/location/java/android/location/GlonassAssistance.java b/location/java/android/location/GlonassAssistance.java index c7ed1c52b403..8c5ddbb10a07 100644 --- a/location/java/android/location/GlonassAssistance.java +++ b/location/java/android/location/GlonassAssistance.java @@ -44,8 +44,8 @@ public final class GlonassAssistance implements Parcelable { /** The UTC model. */ @Nullable private final UtcModel mUtcModel; - /** The auxiliary information. */ - @Nullable private final AuxiliaryInformation mAuxiliaryInformation; + /** The list of auxiliary informations. */ + @NonNull private final List<AuxiliaryInformation> mAuxiliaryInformation; /** The list of time models. */ @NonNull private final List<TimeModel> mTimeModels; @@ -56,10 +56,18 @@ public final class GlonassAssistance implements Parcelable { /** The list of Glonass satellite corrections. */ @NonNull private final List<GnssSatelliteCorrections> mSatelliteCorrections; + /** The list of real time integrity models. */ + @NonNull private final List<RealTimeIntegrityModel> mRealTimeIntegrityModels; + private GlonassAssistance(Builder builder) { mAlmanac = builder.mAlmanac; mUtcModel = builder.mUtcModel; - mAuxiliaryInformation = builder.mAuxiliaryInformation; + if (builder.mAuxiliaryInformation != null) { + mAuxiliaryInformation = + Collections.unmodifiableList(new ArrayList<>(builder.mAuxiliaryInformation)); + } else { + mAuxiliaryInformation = new ArrayList<>(); + } if (builder.mTimeModels != null) { mTimeModels = Collections.unmodifiableList(new ArrayList<>(builder.mTimeModels)); } else { @@ -77,6 +85,12 @@ public final class GlonassAssistance implements Parcelable { } else { mSatelliteCorrections = new ArrayList<>(); } + if (builder.mRealTimeIntegrityModels != null) { + mRealTimeIntegrityModels = + Collections.unmodifiableList(new ArrayList<>(builder.mRealTimeIntegrityModels)); + } else { + mRealTimeIntegrityModels = new ArrayList<>(); + } } /** Returns the Glonass almanac. */ @@ -109,9 +123,15 @@ public final class GlonassAssistance implements Parcelable { return mSatelliteCorrections; } - /** Returns the auxiliary information. */ - @Nullable - public AuxiliaryInformation getAuxiliaryInformation() { + /** Returns the list of real time integrity models. */ + @NonNull + public List<RealTimeIntegrityModel> getRealTimeIntegrityModels() { + return mRealTimeIntegrityModels; + } + + /** Returns the list of auxiliary informations. */ + @NonNull + public List<AuxiliaryInformation> getAuxiliaryInformation() { return mAuxiliaryInformation; } @@ -124,10 +144,11 @@ public final class GlonassAssistance implements Parcelable { public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeTypedObject(mAlmanac, flags); dest.writeTypedObject(mUtcModel, flags); - dest.writeTypedObject(mAuxiliaryInformation, flags); + dest.writeTypedList(mAuxiliaryInformation); dest.writeTypedList(mTimeModels); dest.writeTypedList(mSatelliteEphemeris); dest.writeTypedList(mSatelliteCorrections); + dest.writeTypedList(mRealTimeIntegrityModels); } @Override @@ -140,6 +161,7 @@ public final class GlonassAssistance implements Parcelable { builder.append(", timeModels = ").append(mTimeModels); builder.append(", satelliteEphemeris = ").append(mSatelliteEphemeris); builder.append(", satelliteCorrections = ").append(mSatelliteCorrections); + builder.append(", realTimeIntegrityModels = ").append(mRealTimeIntegrityModels); builder.append("]"); return builder.toString(); } @@ -152,12 +174,14 @@ public final class GlonassAssistance implements Parcelable { .setAlmanac(in.readTypedObject(GlonassAlmanac.CREATOR)) .setUtcModel(in.readTypedObject(UtcModel.CREATOR)) .setAuxiliaryInformation( - in.readTypedObject(AuxiliaryInformation.CREATOR)) + in.createTypedArrayList(AuxiliaryInformation.CREATOR)) .setTimeModels(in.createTypedArrayList(TimeModel.CREATOR)) .setSatelliteEphemeris( in.createTypedArrayList(GlonassSatelliteEphemeris.CREATOR)) .setSatelliteCorrections( in.createTypedArrayList(GnssSatelliteCorrections.CREATOR)) + .setRealTimeIntegrityModels( + in.createTypedArrayList(RealTimeIntegrityModel.CREATOR)) .build(); } @@ -171,10 +195,11 @@ public final class GlonassAssistance implements Parcelable { public static final class Builder { private GlonassAlmanac mAlmanac; private UtcModel mUtcModel; - private AuxiliaryInformation mAuxiliaryInformation; + private List<AuxiliaryInformation> mAuxiliaryInformation; private List<TimeModel> mTimeModels; private List<GlonassSatelliteEphemeris> mSatelliteEphemeris; private List<GnssSatelliteCorrections> mSatelliteCorrections; + private List<RealTimeIntegrityModel> mRealTimeIntegrityModels; /** Sets the Glonass almanac. */ @NonNull @@ -190,10 +215,10 @@ public final class GlonassAssistance implements Parcelable { return this; } - /** Sets the auxiliary information. */ + /** Sets the list of auxiliary informations. */ @NonNull public Builder setAuxiliaryInformation( - @Nullable AuxiliaryInformation auxiliaryInformation) { + @NonNull List<AuxiliaryInformation> auxiliaryInformation) { mAuxiliaryInformation = auxiliaryInformation; return this; } @@ -221,6 +246,14 @@ public final class GlonassAssistance implements Parcelable { return this; } + /** Sets the list of real time integrity models. */ + @NonNull + public Builder setRealTimeIntegrityModels( + @NonNull List<RealTimeIntegrityModel> realTimeIntegrityModels) { + mRealTimeIntegrityModels = realTimeIntegrityModels; + return this; + } + /** Builds the {@link GlonassAssistance}. */ @NonNull public GlonassAssistance build() { diff --git a/location/java/android/location/GpsAssistance.java b/location/java/android/location/GpsAssistance.java index 5a8802f057e2..45b13b2f97f6 100644 --- a/location/java/android/location/GpsAssistance.java +++ b/location/java/android/location/GpsAssistance.java @@ -51,8 +51,8 @@ public final class GpsAssistance implements Parcelable { /** The leap seconds model. */ @Nullable private final LeapSecondsModel mLeapSecondsModel; - /** The auxiliary information. */ - @Nullable private final AuxiliaryInformation mAuxiliaryInformation; + /** The list of auxiliary informations. */ + @NonNull private final List<AuxiliaryInformation> mAuxiliaryInformation; /** The list of time models. */ @NonNull private final List<TimeModel> mTimeModels; @@ -71,7 +71,12 @@ public final class GpsAssistance implements Parcelable { mIonosphericModel = builder.mIonosphericModel; mUtcModel = builder.mUtcModel; mLeapSecondsModel = builder.mLeapSecondsModel; - mAuxiliaryInformation = builder.mAuxiliaryInformation; + if (builder.mAuxiliaryInformation != null) { + mAuxiliaryInformation = + Collections.unmodifiableList(new ArrayList<>(builder.mAuxiliaryInformation)); + } else { + mAuxiliaryInformation = new ArrayList<>(); + } if (builder.mTimeModels != null) { mTimeModels = Collections.unmodifiableList(new ArrayList<>(builder.mTimeModels)); } else { @@ -121,9 +126,9 @@ public final class GpsAssistance implements Parcelable { return mLeapSecondsModel; } - /** Returns the auxiliary information. */ - @Nullable - public AuxiliaryInformation getAuxiliaryInformation() { + /** Returns the list of auxiliary informations. */ + @NonNull + public List<AuxiliaryInformation> getAuxiliaryInformation() { return mAuxiliaryInformation; } @@ -163,7 +168,7 @@ public final class GpsAssistance implements Parcelable { .setUtcModel(in.readTypedObject(UtcModel.CREATOR)) .setLeapSecondsModel(in.readTypedObject(LeapSecondsModel.CREATOR)) .setAuxiliaryInformation( - in.readTypedObject(AuxiliaryInformation.CREATOR)) + in.createTypedArrayList(AuxiliaryInformation.CREATOR)) .setTimeModels(in.createTypedArrayList(TimeModel.CREATOR)) .setSatelliteEphemeris( in.createTypedArrayList(GpsSatelliteEphemeris.CREATOR)) @@ -191,7 +196,7 @@ public final class GpsAssistance implements Parcelable { dest.writeTypedObject(mIonosphericModel, flags); dest.writeTypedObject(mUtcModel, flags); dest.writeTypedObject(mLeapSecondsModel, flags); - dest.writeTypedObject(mAuxiliaryInformation, flags); + dest.writeTypedList(mAuxiliaryInformation); dest.writeTypedList(mTimeModels); dest.writeTypedList(mSatelliteEphemeris); dest.writeTypedList(mRealTimeIntegrityModels); @@ -221,7 +226,7 @@ public final class GpsAssistance implements Parcelable { private KlobucharIonosphericModel mIonosphericModel; private UtcModel mUtcModel; private LeapSecondsModel mLeapSecondsModel; - private AuxiliaryInformation mAuxiliaryInformation; + private List<AuxiliaryInformation> mAuxiliaryInformation; private List<TimeModel> mTimeModels; private List<GpsSatelliteEphemeris> mSatelliteEphemeris; private List<RealTimeIntegrityModel> mRealTimeIntegrityModels; @@ -256,10 +261,10 @@ public final class GpsAssistance implements Parcelable { return this; } - /** Sets the auxiliary information. */ + /** Sets the list of auxiliary informations. */ @NonNull public Builder setAuxiliaryInformation( - @Nullable AuxiliaryInformation auxiliaryInformation) { + @NonNull List<AuxiliaryInformation> auxiliaryInformation) { mAuxiliaryInformation = auxiliaryInformation; return this; } diff --git a/location/java/android/location/QzssAssistance.java b/location/java/android/location/QzssAssistance.java index 27c34370316e..75a267f2dd2a 100644 --- a/location/java/android/location/QzssAssistance.java +++ b/location/java/android/location/QzssAssistance.java @@ -50,8 +50,8 @@ public final class QzssAssistance implements Parcelable { /** The leap seconds model. */ @Nullable private final LeapSecondsModel mLeapSecondsModel; - /** The auxiliary information. */ - @Nullable private final AuxiliaryInformation mAuxiliaryInformation; + /** The list of auxiliary informations. */ + @NonNull private final List<AuxiliaryInformation> mAuxiliaryInformation; /** The list of time models. */ @NonNull private final List<TimeModel> mTimeModels; @@ -70,7 +70,12 @@ public final class QzssAssistance implements Parcelable { mIonosphericModel = builder.mIonosphericModel; mUtcModel = builder.mUtcModel; mLeapSecondsModel = builder.mLeapSecondsModel; - mAuxiliaryInformation = builder.mAuxiliaryInformation; + if (builder.mAuxiliaryInformation != null) { + mAuxiliaryInformation = + Collections.unmodifiableList(new ArrayList<>(builder.mAuxiliaryInformation)); + } else { + mAuxiliaryInformation = new ArrayList<>(); + } if (builder.mTimeModels != null) { mTimeModels = Collections.unmodifiableList(new ArrayList<>(builder.mTimeModels)); } else { @@ -120,9 +125,9 @@ public final class QzssAssistance implements Parcelable { return mLeapSecondsModel; } - /** Returns the auxiliary information. */ - @Nullable - public AuxiliaryInformation getAuxiliaryInformation() { + /** Returns the list of auxiliary informations. */ + @NonNull + public List<AuxiliaryInformation> getAuxiliaryInformation() { return mAuxiliaryInformation; } @@ -162,7 +167,7 @@ public final class QzssAssistance implements Parcelable { .setUtcModel(in.readTypedObject(UtcModel.CREATOR)) .setLeapSecondsModel(in.readTypedObject(LeapSecondsModel.CREATOR)) .setAuxiliaryInformation( - in.readTypedObject(AuxiliaryInformation.CREATOR)) + in.createTypedArrayList(AuxiliaryInformation.CREATOR)) .setTimeModels(in.createTypedArrayList(TimeModel.CREATOR)) .setSatelliteEphemeris( in.createTypedArrayList(QzssSatelliteEphemeris.CREATOR)) @@ -190,7 +195,7 @@ public final class QzssAssistance implements Parcelable { dest.writeTypedObject(mIonosphericModel, flags); dest.writeTypedObject(mUtcModel, flags); dest.writeTypedObject(mLeapSecondsModel, flags); - dest.writeTypedObject(mAuxiliaryInformation, flags); + dest.writeTypedList(mAuxiliaryInformation); dest.writeTypedList(mTimeModels); dest.writeTypedList(mSatelliteEphemeris); dest.writeTypedList(mRealTimeIntegrityModels); @@ -220,7 +225,7 @@ public final class QzssAssistance implements Parcelable { private KlobucharIonosphericModel mIonosphericModel; private UtcModel mUtcModel; private LeapSecondsModel mLeapSecondsModel; - private AuxiliaryInformation mAuxiliaryInformation; + private List<AuxiliaryInformation> mAuxiliaryInformation; private List<TimeModel> mTimeModels; private List<QzssSatelliteEphemeris> mSatelliteEphemeris; private List<RealTimeIntegrityModel> mRealTimeIntegrityModels; @@ -254,10 +259,10 @@ public final class QzssAssistance implements Parcelable { return this; } - /** Sets the auxiliary information. */ + /** Sets the list of auxiliary informations. */ @NonNull public Builder setAuxiliaryInformation( - @Nullable AuxiliaryInformation auxiliaryInformation) { + @NonNull List<AuxiliaryInformation> auxiliaryInformation) { mAuxiliaryInformation = auxiliaryInformation; return this; } diff --git a/location/java/android/location/flags/location.aconfig b/location/java/android/location/flags/location.aconfig index f26e72fa79f1..4b460c6ab039 100644 --- a/location/java/android/location/flags/location.aconfig +++ b/location/java/android/location/flags/location.aconfig @@ -199,9 +199,33 @@ flag { } flag { + name: "fix_is_in_emergency_anr" + namespace: "location" + description: "Avoid calling IPC with a lock to avoid deadlock" + bug: "355384257" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "gnss_assistance_interface_jni" namespace: "location" description: "Flag for GNSS assistance interface JNI" bug: "209078566" } +flag { + name: "service_watcher_unstable_fallback" + namespace: "location" + description: "Flag for service watcher to fallback on an unstable service" + bug: "402997842" + is_fixed_read_only: true +} + +flag { + name: "missing_attribution_tags_in_overlay" + namespace: "location" + description: "Adds missing attribution tags in the Fused and Gnss overlay" + bug: "403337028" +} diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java index 15c832392a22..d0676e693b95 100644 --- a/media/java/android/media/MediaCodec.java +++ b/media/java/android/media/MediaCodec.java @@ -50,6 +50,7 @@ import android.os.Message; import android.os.PersistableBundle; import android.os.Trace; import android.view.Surface; +import android.util.Log; import java.io.IOException; import java.lang.annotation.Retention; @@ -1656,6 +1657,7 @@ import java.util.function.Supplier; </table> */ final public class MediaCodec { + private static final String TAG = "MediaCodec"; /** * Per buffer metadata includes an offset and size specifying @@ -2496,6 +2498,49 @@ final public class MediaCodec { } keys[i] = "audio-hw-sync"; values[i] = AudioSystem.getAudioHwSyncForSession(sessionId); + } else if (applyPictureProfiles() && mediaQualityFw() + && entry.getKey().equals(MediaFormat.KEY_PICTURE_PROFILE_INSTANCE)) { + PictureProfile pictureProfile = null; + try { + pictureProfile = (PictureProfile) entry.getValue(); + } catch (ClassCastException e) { + throw new IllegalArgumentException( + "Cannot cast the instance parameter to PictureProfile!"); + } catch (Exception e) { + Log.e(TAG, Log.getStackTraceString(e)); + throw new IllegalArgumentException("Unexpected exception when casting the " + + "instance parameter to PictureProfile!"); + } + if (pictureProfile == null) { + throw new IllegalArgumentException( + "Picture profile instance parameter is null!"); + } + PictureProfileHandle handle = pictureProfile.getHandle(); + if (handle != PictureProfileHandle.NONE) { + keys[i] = PARAMETER_KEY_PICTURE_PROFILE_HANDLE; + values[i] = Long.valueOf(handle.getId()); + } + } else if (applyPictureProfiles() && mediaQualityFw() + && entry.getKey().equals(MediaFormat.KEY_PICTURE_PROFILE_ID)) { + String pictureProfileId = null; + try { + pictureProfileId = (String) entry.getValue(); + } catch (ClassCastException e) { + throw new IllegalArgumentException( + "Cannot cast the KEY_PICTURE_PROFILE_ID parameter to String!"); + } catch (Exception e) { + Log.e(TAG, Log.getStackTraceString(e)); + throw new IllegalArgumentException("Unexpected exception when casting the " + + "KEY_PICTURE_PROFILE_ID parameter!"); + } + if (pictureProfileId == null) { + throw new IllegalArgumentException( + "KEY_PICTURE_PROFILE_ID parameter is null!"); + } + if (!pictureProfileId.isEmpty()) { + keys[i] = MediaFormat.KEY_PICTURE_PROFILE_ID; + values[i] = pictureProfileId; + } } else { keys[i] = entry.getKey(); values[i] = entry.getValue(); @@ -5424,7 +5469,7 @@ final public class MediaCodec { throw new IllegalArgumentException( "Cannot cast the instance parameter to PictureProfile!"); } catch (Exception e) { - android.util.Log.getStackTraceString(e); + Log.e(TAG, Log.getStackTraceString(e)); throw new IllegalArgumentException("Unexpected exception when casting the " + "instance parameter to PictureProfile!"); } @@ -5437,6 +5482,26 @@ final public class MediaCodec { keys[i] = PARAMETER_KEY_PICTURE_PROFILE_HANDLE; values[i] = Long.valueOf(handle.getId()); } + } else if (applyPictureProfiles() && mediaQualityFw() + && key.equals(MediaFormat.KEY_PICTURE_PROFILE_ID)) { + String pictureProfileId = null; + try { + pictureProfileId = (String) params.get(key); + } catch (ClassCastException e) { + throw new IllegalArgumentException( + "Cannot cast the KEY_PICTURE_PROFILE_ID parameter to String!"); + } catch (Exception e) { + Log.e(TAG, Log.getStackTraceString(e)); + throw new IllegalArgumentException("Unexpected exception when casting the " + + "KEY_PICTURE_PROFILE_ID parameter!"); + } + if (pictureProfileId == null) { + throw new IllegalArgumentException("KEY_PICTURE_PROFILE_ID parameter is null!"); + } + if (!pictureProfileId.isEmpty()) { + keys[i] = MediaFormat.KEY_PICTURE_PROFILE_ID; + values[i] = pictureProfileId; + } } else { keys[i] = key; Object value = params.get(key); @@ -5455,10 +5520,9 @@ final public class MediaCodec { } private void logAndRun(String message, Runnable r) { - final String TAG = "MediaCodec"; - android.util.Log.d(TAG, "enter: " + message); + Log.d(TAG, "enter: " + message); r.run(); - android.util.Log.d(TAG, "exit : " + message); + Log.d(TAG, "exit : " + message); } /** diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig index 7221f1ddeb7f..15e87f80ef64 100644 --- a/media/java/android/media/flags/media_better_together.aconfig +++ b/media/java/android/media/flags/media_better_together.aconfig @@ -62,6 +62,16 @@ flag { } flag { + name: "enable_fix_for_empty_system_routes_crash" + namespace: "media_better_together" + description: "Fixes a bug causing SystemUI to crash due to an empty system routes list in the routing framework." + bug: "357468728" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_suggested_device_api" is_exported: true namespace: "media_better_together" diff --git a/media/java/android/media/projection/MediaProjectionAppContent.aidl b/media/java/android/media/projection/MediaProjectionAppContent.aidl new file mode 100644 index 000000000000..6ead69b9fdc6 --- /dev/null +++ b/media/java/android/media/projection/MediaProjectionAppContent.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.projection; + +parcelable MediaProjectionAppContent;
\ No newline at end of file diff --git a/media/java/android/media/projection/MediaProjectionAppContent.java b/media/java/android/media/projection/MediaProjectionAppContent.java new file mode 100644 index 000000000000..da0bdc191c0c --- /dev/null +++ b/media/java/android/media/projection/MediaProjectionAppContent.java @@ -0,0 +1,123 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.projection; + +import android.annotation.FlaggedApi; +import android.graphics.Bitmap; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import java.util.Objects; + +/** + * Holds information about content an app can share via the MediaProjection APIs. + * <p> + * An application requesting a {@link MediaProjection session} can add its own content in the + * list of available content along with the whole screen or a single application. + * <p> + * Each instance of {@link MediaProjectionAppContent} contains an id that is used to identify the + * content chosen by the user back to the advertising application, thus the meaning of the id is + * only relevant to that application. + */ +@FlaggedApi(com.android.media.projection.flags.Flags.FLAG_APP_CONTENT_SHARING) +public final class MediaProjectionAppContent implements Parcelable { + + private final Bitmap mThumbnail; + private final CharSequence mTitle; + private final int mId; + + /** + * Constructor to pass a thumbnail, title and id. + * + * @param thumbnail The thumbnail representing this content to be shown to the user. + * @param title A user visible string representing the title of this content. + * @param id An arbitrary int defined by the advertising application to be fed back once + * the user made their choice. + */ + public MediaProjectionAppContent(@NonNull Bitmap thumbnail, @NonNull CharSequence title, + int id) { + mThumbnail = Objects.requireNonNull(thumbnail, "thumbnail can't be null").asShared(); + mTitle = Objects.requireNonNull(title, "title can't be null"); + mId = id; + } + + /** + * Returns thumbnail representing this content to be shown to the user. + * + * @hide + */ + @NonNull + public Bitmap getThumbnail() { + return mThumbnail; + } + + /** + * Returns user visible string representing the title of this content. + * + * @hide + */ + @NonNull + public CharSequence getTitle() { + return mTitle; + } + + /** + * Returns the arbitrary int defined by the advertising application to be fed back once + * the user made their choice. + * + * @hide + */ + public int getId() { + return mId; + } + + private MediaProjectionAppContent(Parcel in) { + mThumbnail = in.readParcelable(this.getClass().getClassLoader(), Bitmap.class); + mTitle = in.readCharSequence(); + mId = in.readInt(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeParcelable(mThumbnail, flags); + dest.writeCharSequence(mTitle); + dest.writeInt(mId); + } + + @Override + public int describeContents() { + return 0; + } + + @NonNull + public static final Creator<MediaProjectionAppContent> CREATOR = + new Creator<>() { + @NonNull + @Override + public MediaProjectionAppContent createFromParcel(@NonNull Parcel in) { + return new MediaProjectionAppContent(in); + } + + @NonNull + @Override + public MediaProjectionAppContent[] newArray(int size) { + return new MediaProjectionAppContent[size]; + } + }; +} diff --git a/media/java/android/media/projection/MediaProjectionConfig.java b/media/java/android/media/projection/MediaProjectionConfig.java index 598b534e81ca..cd674e9f2ad1 100644 --- a/media/java/android/media/projection/MediaProjectionConfig.java +++ b/media/java/android/media/projection/MediaProjectionConfig.java @@ -20,23 +20,56 @@ import static android.view.Display.DEFAULT_DISPLAY; import static java.lang.annotation.RetentionPolicy.SOURCE; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.os.Parcelable; -import com.android.internal.util.AnnotationValidations; +import com.android.media.projection.flags.Flags; import java.lang.annotation.Retention; +import java.util.Arrays; +import java.util.Objects; /** * Configure the {@link MediaProjection} session requested from * {@link MediaProjectionManager#createScreenCaptureIntent(MediaProjectionConfig)}. + * <p> + * This configuration should be used to provide the user with options for choosing the content to + * be shared with the requesting application. */ public final class MediaProjectionConfig implements Parcelable { /** + * Bitmask for setting whether this configuration is for projecting the whole display. + */ + @FlaggedApi(Flags.FLAG_APP_CONTENT_SHARING) + public static final int PROJECTION_SOURCE_DISPLAY = 1 << 1; + + /** + * Bitmask for setting whether this configuration is for projecting the a custom region display. + * + * @hide + */ + public static final int PROJECTION_SOURCE_DISPLAY_REGION = 1 << 2; + + /** + * Bitmask for setting whether this configuration is for projecting the a single application. + */ + @FlaggedApi(Flags.FLAG_APP_CONTENT_SHARING) + public static final int PROJECTION_SOURCE_APP = 1 << 3; + + /** + * Bitmask for setting whether this configuration is for projecting the content provided by an + * application. + */ + @FlaggedApi(com.android.media.projection.flags.Flags.FLAG_APP_CONTENT_SHARING) + public static final int PROJECTION_SOURCE_APP_CONTENT = 1 << 4; + + /** * The user, rather than the host app, determines which region of the display to capture. * * @hide @@ -44,39 +77,109 @@ public final class MediaProjectionConfig implements Parcelable { public static final int CAPTURE_REGION_USER_CHOICE = 0; /** + * @hide + */ + public static final int DEFAULT_PROJECTION_SOURCES = + PROJECTION_SOURCE_DISPLAY | PROJECTION_SOURCE_APP; + + /** * The host app specifies a particular display to capture. * * @hide */ public static final int CAPTURE_REGION_FIXED_DISPLAY = 1; + private static final int[] PROJECTION_SOURCES = + new int[]{PROJECTION_SOURCE_DISPLAY, PROJECTION_SOURCE_DISPLAY_REGION, + PROJECTION_SOURCE_APP, + PROJECTION_SOURCE_APP_CONTENT}; + + private static final String[] PROJECTION_SOURCES_STRING = + new String[]{"PROJECTION_SOURCE_DISPLAY", "PROJECTION_SOURCE_DISPLAY_REGION", + "PROJECTION_SOURCE_APP", "PROJECTION_SOURCE_APP_CONTENT"}; + + private static final int VALID_PROJECTION_SOURCES = createValidSourcesMask(); + + private final int mInitialSelection; + /** @hide */ @IntDef(prefix = "CAPTURE_REGION_", value = {CAPTURE_REGION_USER_CHOICE, CAPTURE_REGION_FIXED_DISPLAY}) @Retention(SOURCE) + @Deprecated // Remove when FLAG_APP_CONTENT_SHARING is removed public @interface CaptureRegion { } + /** @hide */ + @IntDef(flag = true, prefix = "PROJECTION_SOURCE_", value = {PROJECTION_SOURCE_DISPLAY, + PROJECTION_SOURCE_DISPLAY_REGION, PROJECTION_SOURCE_APP, PROJECTION_SOURCE_APP_CONTENT}) + @Retention(SOURCE) + public @interface MediaProjectionSource { + } + /** - * The particular display to capture. Only used when {@link #getRegionToCapture()} is - * {@link #CAPTURE_REGION_FIXED_DISPLAY}; ignored otherwise. + * The particular display to capture. Only used when {@link #PROJECTION_SOURCE_DISPLAY} is set, + * ignored otherwise. * <p> * Only supports values of {@link android.view.Display#DEFAULT_DISPLAY}. */ @IntRange(from = DEFAULT_DISPLAY, to = DEFAULT_DISPLAY) - private int mDisplayToCapture; + private final int mDisplayToCapture; /** * The region to capture. Defaults to the user's choice. */ @CaptureRegion + @Deprecated // Remove when FLAG_APP_CONTENT_SHARING is removed private int mRegionToCapture; /** + * The region to capture. Defaults to the user's choice. + */ + @MediaProjectionSource + private final int mProjectionSources; + + /** + * @see #getRequesterHint() + */ + @Nullable + private final String mRequesterHint; + + /** * Customized instance, with region set to the provided value. + * @deprecated To be removed FLAG_APP_CONTENT_SHARING is removed */ + @Deprecated // Remove when FLAG_APP_CONTENT_SHARING is removed private MediaProjectionConfig(@CaptureRegion int captureRegion) { + if (Flags.appContentSharing()) { + throw new UnsupportedOperationException( + "Flag FLAG_APP_CONTENT_SHARING enabled. This method must not be called."); + } mRegionToCapture = captureRegion; + mDisplayToCapture = DEFAULT_DISPLAY; + + mRequesterHint = null; + mInitialSelection = -1; + mProjectionSources = -1; + } + + /** + * Customized instance, with region set to the provided value. + */ + private MediaProjectionConfig(@MediaProjectionSource int projectionSource, + @Nullable String requesterHint, int displayId, int initialSelection) { + if (!Flags.appContentSharing()) { + throw new UnsupportedOperationException( + "Flag FLAG_APP_CONTENT_SHARING disabled. This method must not be called"); + } + if (projectionSource == 0) { + mProjectionSources = DEFAULT_PROJECTION_SOURCES; + } else { + mProjectionSources = projectionSource; + } + mRequesterHint = requesterHint; + mDisplayToCapture = displayId; + mInitialSelection = initialSelection; } /** @@ -84,16 +187,17 @@ public final class MediaProjectionConfig implements Parcelable { */ @NonNull public static MediaProjectionConfig createConfigForDefaultDisplay() { - MediaProjectionConfig config = new MediaProjectionConfig(CAPTURE_REGION_FIXED_DISPLAY); - config.mDisplayToCapture = DEFAULT_DISPLAY; - return config; + if (Flags.appContentSharing()) { + return new Builder().setSourceEnabled(PROJECTION_SOURCE_DISPLAY, true).build(); + } else { + return new MediaProjectionConfig(CAPTURE_REGION_FIXED_DISPLAY); + } } /** * Returns an instance which allows the user to decide which region is captured. The consent * dialog presents the user with all possible options. If the user selects display capture, * then only the {@link android.view.Display#DEFAULT_DISPLAY} is supported. - * * <p> * When passed in to * {@link MediaProjectionManager#createScreenCaptureIntent(MediaProjectionConfig)}, the consent @@ -103,13 +207,18 @@ public final class MediaProjectionConfig implements Parcelable { */ @NonNull public static MediaProjectionConfig createConfigForUserChoice() { - return new MediaProjectionConfig(CAPTURE_REGION_USER_CHOICE); + if (Flags.appContentSharing()) { + return new MediaProjectionConfig.Builder().build(); + } else { + return new MediaProjectionConfig(CAPTURE_REGION_USER_CHOICE); + } } /** * Returns string representation of the captured region. */ @NonNull + @Deprecated // Remove when FLAG_APP_CONTENT_SHARING is removed private static String captureRegionToString(int value) { return switch (value) { case CAPTURE_REGION_USER_CHOICE -> "CAPTURE_REGION_USERS_CHOICE"; @@ -118,16 +227,42 @@ public final class MediaProjectionConfig implements Parcelable { }; } + /** + * Returns string representation of the captured region. + */ + @NonNull + private static String projectionSourceToString(int value) { + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < PROJECTION_SOURCES.length; i++) { + if ((value & PROJECTION_SOURCES[i]) > 0) { + stringBuilder.append(PROJECTION_SOURCES_STRING[i]); + stringBuilder.append(" "); + value &= ~PROJECTION_SOURCES[i]; + } + } + if (value > 0) { + stringBuilder.append("Unknown projection sources: "); + stringBuilder.append(Integer.toHexString(value)); + } + return stringBuilder.toString(); + } + @Override public String toString() { - return "MediaProjectionConfig { " + "displayToCapture = " + mDisplayToCapture + ", " - + "regionToCapture = " + captureRegionToString(mRegionToCapture) + " }"; + if (Flags.appContentSharing()) { + return ("MediaProjectionConfig{mInitialSelection=%d, mDisplayToCapture=%d, " + + "mProjectionSource=%s, mRequesterHint='%s'}").formatted(mInitialSelection, + mDisplayToCapture, projectionSourceToString(mProjectionSources), + mRequesterHint); + } else { + return "MediaProjectionConfig { " + "displayToCapture = " + mDisplayToCapture + ", " + + "regionToCapture = " + captureRegionToString(mRegionToCapture) + " }"; + } } - /** - * The particular display to capture. Only used when {@link #getRegionToCapture()} is - * {@link #CAPTURE_REGION_FIXED_DISPLAY}; ignored otherwise. + * The particular display to capture. Only used when {@link #PROJECTION_SOURCE_DISPLAY} is + * set; ignored otherwise. * <p> * Only supports values of {@link android.view.Display#DEFAULT_DISPLAY}. * @@ -146,27 +281,57 @@ public final class MediaProjectionConfig implements Parcelable { return mRegionToCapture; } + /** + * A bitmask representing of requested projection sources. + * <p> + * The system supports different kind of media projection session. Although the user is + * picking the target content, the requesting application can configure the choices displayed + * to the user. + */ + @FlaggedApi(Flags.FLAG_APP_CONTENT_SHARING) + public @MediaProjectionSource int getProjectionSources() { + return mProjectionSources; + } + @Override public boolean equals(@Nullable Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MediaProjectionConfig that = (MediaProjectionConfig) o; - return mDisplayToCapture == that.mDisplayToCapture - && mRegionToCapture == that.mRegionToCapture; + if (Flags.appContentSharing()) { + return mDisplayToCapture == that.mDisplayToCapture + && mProjectionSources == that.mProjectionSources + && mInitialSelection == that.mInitialSelection + && Objects.equals(mRequesterHint, that.mRequesterHint); + } else { + return mDisplayToCapture == that.mDisplayToCapture + && mRegionToCapture == that.mRegionToCapture; + } } @Override public int hashCode() { int _hash = 1; - _hash = 31 * _hash + mDisplayToCapture; - _hash = 31 * _hash + mRegionToCapture; + if (Flags.appContentSharing()) { + return Objects.hash(mDisplayToCapture, mProjectionSources, mInitialSelection, + mRequesterHint); + } else { + _hash = 31 * _hash + mDisplayToCapture; + _hash = 31 * _hash + mRegionToCapture; + } return _hash; } @Override public void writeToParcel(@NonNull android.os.Parcel dest, int flags) { dest.writeInt(mDisplayToCapture); - dest.writeInt(mRegionToCapture); + if (Flags.appContentSharing()) { + dest.writeInt(mProjectionSources); + dest.writeString(mRequesterHint); + dest.writeInt(mInitialSelection); + } else { + dest.writeInt(mRegionToCapture); + } } @Override @@ -176,12 +341,17 @@ public final class MediaProjectionConfig implements Parcelable { /** @hide */ /* package-private */ MediaProjectionConfig(@NonNull android.os.Parcel in) { - int displayToCapture = in.readInt(); - int regionToCapture = in.readInt(); - - mDisplayToCapture = displayToCapture; - mRegionToCapture = regionToCapture; - AnnotationValidations.validate(CaptureRegion.class, null, mRegionToCapture); + mDisplayToCapture = in.readInt(); + if (Flags.appContentSharing()) { + mProjectionSources = in.readInt(); + mRequesterHint = in.readString(); + mInitialSelection = in.readInt(); + } else { + mRegionToCapture = in.readInt(); + mProjectionSources = -1; + mRequesterHint = null; + mInitialSelection = -1; + } } public static final @NonNull Parcelable.Creator<MediaProjectionConfig> CREATOR = @@ -196,4 +366,138 @@ public final class MediaProjectionConfig implements Parcelable { return new MediaProjectionConfig(in); } }; + + /** + * Returns true if the provided source should be enabled. + * + * @param projectionSource projection source integer to check for. The parameter can also be a + * bitmask of multiple sources. + */ + @FlaggedApi(Flags.FLAG_APP_CONTENT_SHARING) + public boolean isSourceEnabled(@MediaProjectionSource int projectionSource) { + return (mProjectionSources & projectionSource) > 0; + } + + /** + * Returns a bit mask of one, and only one, of the projection type flag. + */ + @FlaggedApi(Flags.FLAG_APP_CONTENT_SHARING) + @MediaProjectionSource + public int getInitiallySelectedSource() { + return mInitialSelection; + } + + /** + * A hint set by the requesting app indicating who the requester of this {@link MediaProjection} + * session is. + * <p> + * The UI component prompting the user for the permission to start the session can use + * this hint to provide more information about the origin of the request (e.g. a browser + * tab title, a meeting id if sharing to a video conferencing app, a player name if + * sharing the screen within a game). + * + * @return the hint to be displayed if set, null otherwise. + */ + @FlaggedApi(Flags.FLAG_APP_CONTENT_SHARING) + @Nullable + public CharSequence getRequesterHint() { + return mRequesterHint; + } + + private static int createValidSourcesMask() { + int validSources = 0; + for (int projectionSource : PROJECTION_SOURCES) { + validSources |= projectionSource; + } + return validSources; + } + + @FlaggedApi(Flags.FLAG_APP_CONTENT_SHARING) + public static final class Builder { + private int mOptions = 0; + private String mRequesterHint = null; + + @MediaProjectionSource + private int mInitialSelection; + + public Builder() { + if (!Flags.appContentSharing()) { + throw new UnsupportedOperationException("Flag FLAG_APP_CONTENT_SHARING disabled"); + } + } + + /** + * Indicates which projection source the UI component should display to the user + * first. Calling this method without enabling the respective choice will have no effect. + * + * @return instance of this {@link Builder}. + * @see #setSourceEnabled(int, boolean) + */ + @NonNull + public Builder setInitiallySelectedSource(@MediaProjectionSource int projectionSource) { + for (int source : PROJECTION_SOURCES) { + if (projectionSource == source) { + mInitialSelection = projectionSource; + return this; + } + } + throw new IllegalArgumentException( + ("projectionSource is no a valid projection source. projectionSource must be " + + "one of %s but was %s") + .formatted(Arrays.toString(PROJECTION_SOURCES_STRING), + projectionSourceToString(projectionSource))); + } + + /** + * Let the requesting app indicate who the requester of this {@link MediaProjection} + * session is.. + * <p> + * The UI component prompting the user for the permission to start the session can use + * this hint to provide more information about the origin of the request (e.g. a browser + * tab title, a meeting id if sharing to a video conferencing app, a player name if + * sharing the screen within a game). + * <p> + * Note that setting this won't hide or change the name of the application + * requesting the session. + * + * @return instance of this {@link Builder}. + */ + @NonNull + public Builder setRequesterHint(@Nullable String requesterHint) { + mRequesterHint = requesterHint; + return this; + } + + /** + * Set whether the UI component requesting the user permission to share their screen + * should display an option to share the specified source + * + * @param source the projection source to enable or disable + * @param enabled true to enable the source, false otherwise + * @return this instance for chaining. + * @throws IllegalArgumentException if the source is not one of the valid sources. + */ + @NonNull + @SuppressLint("MissingGetterMatchingBuilder") // isSourceEnabled is defined + public Builder setSourceEnabled(@MediaProjectionSource int source, boolean enabled) { + if ((source & VALID_PROJECTION_SOURCES) == 0) { + throw new IllegalArgumentException( + ("source is no a valid projection source. source must be " + + "any of %s but was %s") + .formatted(Arrays.toString(PROJECTION_SOURCES_STRING), + projectionSourceToString(source))); + } + mOptions = enabled ? mOptions | source : mOptions & ~source; + return this; + } + + /** + * Builds a new immutable instance of {@link MediaProjectionConfig} + */ + @NonNull + public MediaProjectionConfig build() { + return new MediaProjectionConfig(mOptions, mRequesterHint, DEFAULT_DISPLAY, + mInitialSelection); + } + } } diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java index 9036bf385d96..4a5392d3c0c3 100644 --- a/media/java/android/media/projection/MediaProjectionManager.java +++ b/media/java/android/media/projection/MediaProjectionManager.java @@ -29,6 +29,7 @@ import android.compat.annotation.Overridable; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.hardware.display.VirtualDisplay; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; @@ -78,9 +79,12 @@ public final class MediaProjectionManager { private static final String TAG = "MediaProjectionManager"; /** - * This change id ensures that users are presented with a choice of capturing a single app - * or the entire screen when initiating a MediaProjection session, overriding the usage of - * MediaProjectionConfig#createConfigForDefaultDisplay. + * If enabled, this change id ensures that users are presented with a choice of capturing a + * single app and the entire screen when initiating a MediaProjection session, overriding the + * usage of MediaProjectionConfig#createConfigForDefaultDisplay. + * <p> + * + * <a href=" https://developer.android.com/guide/practices/device-compatibility-mode#override_disable_media_projection_single_app_option">More info</a> * * @hide */ diff --git a/media/java/android/media/projection/TEST_MAPPING b/media/java/android/media/projection/TEST_MAPPING index ea62287b7411..62e776b822d2 100644 --- a/media/java/android/media/projection/TEST_MAPPING +++ b/media/java/android/media/projection/TEST_MAPPING @@ -4,4 +4,4 @@ "path": "frameworks/base/services/core/java/com/android/server/media/projection" } ] -}
\ No newline at end of file +} diff --git a/media/java/android/media/quality/ActiveProcessingPicture.java b/media/java/android/media/quality/ActiveProcessingPicture.java index e16ad62e23f2..15c2e47fe820 100644 --- a/media/java/android/media/quality/ActiveProcessingPicture.java +++ b/media/java/android/media/quality/ActiveProcessingPicture.java @@ -31,16 +31,26 @@ import androidx.annotation.NonNull; public final class ActiveProcessingPicture implements Parcelable { private final int mId; private final String mProfileId; + private final boolean mForGlobal; public ActiveProcessingPicture(int id, @NonNull String profileId) { mId = id; mProfileId = profileId; + mForGlobal = true; + } + + /** @hide */ + public ActiveProcessingPicture(int id, @NonNull String profileId, boolean forGlobal) { + mId = id; + mProfileId = profileId; + mForGlobal = forGlobal; } /** @hide */ ActiveProcessingPicture(Parcel in) { mId = in.readInt(); mProfileId = in.readString(); + mForGlobal = in.readBoolean(); } @NonNull @@ -73,6 +83,14 @@ public final class ActiveProcessingPicture implements Parcelable { return mProfileId; } + /** + * @hide + */ + @NonNull + public boolean isForGlobal() { + return mForGlobal; + } + @Override public int describeContents() { return 0; @@ -82,5 +100,6 @@ public final class ActiveProcessingPicture implements Parcelable { public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(mId); dest.writeString(mProfileId); + dest.writeBoolean(mForGlobal); } } diff --git a/media/java/android/media/quality/MediaQualityManager.java b/media/java/android/media/quality/MediaQualityManager.java index bfd01380a2ee..3f0ba3100191 100644 --- a/media/java/android/media/quality/MediaQualityManager.java +++ b/media/java/android/media/quality/MediaQualityManager.java @@ -214,11 +214,31 @@ public final class MediaQualityManager { } }; + IActiveProcessingPictureListener apListener = new IActiveProcessingPictureListener.Stub() { + @Override + public void onActiveProcessingPicturesChanged(List<ActiveProcessingPicture> aps) { + List<ActiveProcessingPicture> nonGlobal = new ArrayList<>(); + for (ActiveProcessingPicture ap : aps) { + if (!ap.isForGlobal()) { + nonGlobal.add(ap); + } + } + for (ActiveProcessingPictureListenerRecord record : mApListenerRecords) { + if (record.mIsGlobal) { + record.postActiveProcessingPicturesChanged(aps); + } else { + record.postActiveProcessingPicturesChanged(nonGlobal); + } + } + } + }; + try { if (mService != null) { mService.registerPictureProfileCallback(ppCallback); mService.registerSoundProfileCallback(spCallback); mService.registerAmbientBacklightCallback(abCallback); + mService.registerActiveProcessingPictureListener(apListener); } } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -378,6 +398,18 @@ public final class MediaQualityManager { } /** + * Gets picture profile handle for TV input. + * @hide + */ + public long getPictureProfileForTvInput(String inputId) { + try { + return mService.getPictureProfileForTvInput(inputId, mUserHandle.getIdentifier()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Gets sound profile handle by profile ID. * @hide */ @@ -1213,6 +1245,15 @@ public final class MediaQualityManager { public Consumer<List<ActiveProcessingPicture>> getListener() { return mListener; } + + public void postActiveProcessingPicturesChanged(List<ActiveProcessingPicture> aps) { + mExecutor.execute(new Runnable() { + @Override + public void run() { + mListener.accept(aps); + } + }); + } } /** diff --git a/media/java/android/media/quality/PictureProfile.java b/media/java/android/media/quality/PictureProfile.java index 8a585efe032c..0121193a7c86 100644 --- a/media/java/android/media/quality/PictureProfile.java +++ b/media/java/android/media/quality/PictureProfile.java @@ -18,6 +18,7 @@ package android.media.quality; import android.annotation.FlaggedApi; import android.annotation.IntDef; +import android.annotation.StringDef; import android.annotation.SystemApi; import android.media.tv.TvInputInfo; import android.media.tv.flags.Flags; @@ -72,6 +73,19 @@ public final class PictureProfile implements Parcelable { */ public static final int TYPE_APPLICATION = 2; + /** + * Default profile name + * @hide + */ + public static final String NAME_DEFAULT = "default"; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @StringDef(prefix = "NAME_", value = { + NAME_DEFAULT + }) + public @interface ProfileName {} + /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = false, prefix = "ERROR_", value = { @@ -114,6 +128,32 @@ public final class PictureProfile implements Parcelable { */ public static final int ERROR_NOT_ALLOWLISTED = 4; + /** + * SDR status. + * @hide + */ + public static final String STATUS_SDR = "SDR"; + + /** + * HDR status. + * @hide + */ + public static final String STATUS_HDR = "HDR"; + + /** @hide */ + public static final String NAME_STANDARD = "standard"; + /** @hide */ + public static final String NAME_VIVID = "vivid"; + /** @hide */ + public static final String NAME_SPORTS = "sports"; + /** @hide */ + public static final String NAME_GAME = "game"; + /** @hide */ + public static final String NAME_MOVIE = "movie"; + /** @hide */ + public static final String NAME_ENERGY_SAVING = "energy_saving"; + /** @hide */ + public static final String NAME_USER = "user"; private PictureProfile(@NonNull Parcel in) { mId = in.readString(); diff --git a/media/java/android/media/quality/aidl/android/media/quality/IActiveProcessingPictureListener.aidl b/media/java/android/media/quality/aidl/android/media/quality/IActiveProcessingPictureListener.aidl new file mode 100644 index 000000000000..f7d19baac7a1 --- /dev/null +++ b/media/java/android/media/quality/aidl/android/media/quality/IActiveProcessingPictureListener.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.media.quality; + +import android.media.quality.ActiveProcessingPicture; + +/** + * Interface to receive event from media quality service. + * @hide + */ +oneway interface IActiveProcessingPictureListener { + void onActiveProcessingPicturesChanged(in List<ActiveProcessingPicture> ap); +} diff --git a/media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl b/media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl index 0191ea786de0..ff1bf0228474 100644 --- a/media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl @@ -17,6 +17,7 @@ package android.media.quality; import android.media.quality.AmbientBacklightSettings; +import android.media.quality.IActiveProcessingPictureListener; import android.media.quality.IAmbientBacklightCallback; import android.media.quality.IPictureProfileCallback; import android.media.quality.ISoundProfileCallback; @@ -32,7 +33,7 @@ import android.media.quality.SoundProfile; */ interface IMediaQualityManager { // TODO: use UserHandle - PictureProfile createPictureProfile(in PictureProfile pp, int userId); + void createPictureProfile(in PictureProfile pp, int userId); void updatePictureProfile(in String id, in PictureProfile pp, int userId); void removePictureProfile(in String id, int userId); boolean setDefaultPictureProfile(in String id, int userId); @@ -47,7 +48,13 @@ interface IMediaQualityManager { void setPictureProfileAllowList(in List<String> packages, int userId); List<PictureProfileHandle> getPictureProfileHandle(in String[] id, int userId); - SoundProfile createSoundProfile(in SoundProfile pp, int userId); + long getPictureProfileHandleValue(in String id, int userId); + long getDefaultPictureProfileHandleValue(int userId); + void notifyPictureProfileHandleSelection(in long handle, int userId); + + long getPictureProfileForTvInput(in String inputId, int userId); + + void createSoundProfile(in SoundProfile pp, int userId); void updateSoundProfile(in String id, in SoundProfile pp, int userId); void removeSoundProfile(in String id, int userId); boolean setDefaultSoundProfile(in String id, int userId); @@ -64,6 +71,7 @@ interface IMediaQualityManager { void registerPictureProfileCallback(in IPictureProfileCallback cb); void registerSoundProfileCallback(in ISoundProfileCallback cb); void registerAmbientBacklightCallback(in IAmbientBacklightCallback cb); + void registerActiveProcessingPictureListener(in IActiveProcessingPictureListener l); List<ParameterCapability> getParameterCapabilities(in List<String> names, int userId); diff --git a/media/java/android/media/tv/extension/scan/IScanInterface.aidl b/media/java/android/media/tv/extension/scan/IScanInterface.aidl index b44d1d243150..ea6e8a1d4104 100644 --- a/media/java/android/media/tv/extension/scan/IScanInterface.aidl +++ b/media/java/android/media/tv/extension/scan/IScanInterface.aidl @@ -24,7 +24,7 @@ import android.os.Bundle; */ interface IScanInterface { IBinder createSession(int broadcastType, String countryCode, String operator, - in IScanListener listener); + in IScanListener listener, in Bundle optionalParams); Bundle getParameters(int broadcastType, String countryCode, String operator, in Bundle params); } diff --git a/media/tests/projection/Android.bp b/media/tests/projection/Android.bp index 48621e4e2094..0b4b7dbbca1f 100644 --- a/media/tests/projection/Android.bp +++ b/media/tests/projection/Android.bp @@ -3,7 +3,7 @@ //######################################################################## package { - default_team: "trendy_team_lse_desktop_os_experience", + default_team: "trendy_team_media_projection", // See: http://go/android-license-faq // A large-scale-change added 'default_applicable_licenses' to import // all of the 'license_kinds' from "frameworks_base_license" @@ -26,6 +26,7 @@ android_test { "androidx.test.runner", "androidx.test.rules", "androidx.test.ext.junit", + "flag-junit", "frameworks-base-testutils", "mockito-target-extended-minus-junit4", "platform-test-annotations", diff --git a/media/tests/projection/src/android/media/projection/MediaProjectionAppContentTest.java b/media/tests/projection/src/android/media/projection/MediaProjectionAppContentTest.java new file mode 100644 index 000000000000..7e167c63a2a2 --- /dev/null +++ b/media/tests/projection/src/android/media/projection/MediaProjectionAppContentTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.projection; + +import static com.google.common.truth.Truth.assertThat; + +import android.graphics.Bitmap; +import android.os.Parcel; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class MediaProjectionAppContentTest { + + @Test + public void testConstructorAndGetters() { + // Create a mock Bitmap + Bitmap mockBitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + + // Create a MediaProjectionAppContent object + MediaProjectionAppContent content = new MediaProjectionAppContent(mockBitmap, "Test Title", + 123); + + // Verify the values using getters + assertThat(content.getTitle()).isEqualTo("Test Title"); + assertThat(content.getId()).isEqualTo(123); + // Compare bitmap configurations and dimensions + assertThat(content.getThumbnail().getConfig()).isEqualTo(mockBitmap.getConfig()); + assertThat(content.getThumbnail().getWidth()).isEqualTo(mockBitmap.getWidth()); + assertThat(content.getThumbnail().getHeight()).isEqualTo(mockBitmap.getHeight()); + } + + @Test + public void testParcelable() { + // Create a mock Bitmap + Bitmap mockBitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + + // Create a MediaProjectionAppContent object + MediaProjectionAppContent content = new MediaProjectionAppContent(mockBitmap, "Test Title", + 123); + + // Parcel and unparcel the object + Parcel parcel = Parcel.obtain(); + content.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + MediaProjectionAppContent unparceledContent = + MediaProjectionAppContent.CREATOR.createFromParcel(parcel); + + // Verify the values of the unparceled object + assertThat(unparceledContent.getTitle()).isEqualTo("Test Title"); + assertThat(unparceledContent.getId()).isEqualTo(123); + // Compare bitmap configurations and dimensions + assertThat(unparceledContent.getThumbnail().getConfig()).isEqualTo(mockBitmap.getConfig()); + assertThat(unparceledContent.getThumbnail().getWidth()).isEqualTo(mockBitmap.getWidth()); + assertThat(unparceledContent.getThumbnail().getHeight()).isEqualTo(mockBitmap.getHeight()); + + parcel.recycle(); + } + + @Test + public void testCreatorNewArray() { + // Create a new array using the CREATOR + MediaProjectionAppContent[] contentArray = MediaProjectionAppContent.CREATOR.newArray(5); + + // Verify that the array is not null and has the correct size + assertThat(contentArray).isNotNull(); + assertThat(contentArray).hasLength(5); + } +} diff --git a/media/tests/projection/src/android/media/projection/MediaProjectionConfigTest.java b/media/tests/projection/src/android/media/projection/MediaProjectionConfigTest.java index 2820606958b7..bc0eae1a3ec7 100644 --- a/media/tests/projection/src/android/media/projection/MediaProjectionConfigTest.java +++ b/media/tests/projection/src/android/media/projection/MediaProjectionConfigTest.java @@ -18,22 +18,31 @@ package android.media.projection; import static android.media.projection.MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY; import static android.media.projection.MediaProjectionConfig.CAPTURE_REGION_USER_CHOICE; +import static android.media.projection.MediaProjectionConfig.PROJECTION_SOURCE_DISPLAY; +import static android.media.projection.MediaProjectionConfig.DEFAULT_PROJECTION_SOURCES; import static android.view.Display.DEFAULT_DISPLAY; import static com.google.common.truth.Truth.assertThat; import android.os.Parcel; import android.platform.test.annotations.Presubmit; +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 androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.media.projection.flags.Flags; + +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; /** * Tests for the {@link MediaProjectionConfig} class. - * + * <p> * Build/Install/Run: * atest MediaProjectionTests:MediaProjectionConfigTest */ @@ -41,6 +50,11 @@ import org.junit.runner.RunWith; @Presubmit @RunWith(AndroidJUnit4.class) public class MediaProjectionConfigTest { + + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + private static final MediaProjectionConfig DISPLAY_CONFIG = MediaProjectionConfig.createConfigForDefaultDisplay(); private static final MediaProjectionConfig USERS_CHOICE_CONFIG = @@ -57,17 +71,33 @@ public class MediaProjectionConfigTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_CONTENT_SHARING) public void testCreateDisplayConfig() { assertThat(DISPLAY_CONFIG.getRegionToCapture()).isEqualTo(CAPTURE_REGION_FIXED_DISPLAY); assertThat(DISPLAY_CONFIG.getDisplayToCapture()).isEqualTo(DEFAULT_DISPLAY); } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_CONTENT_SHARING) public void testCreateUsersChoiceConfig() { assertThat(USERS_CHOICE_CONFIG.getRegionToCapture()).isEqualTo(CAPTURE_REGION_USER_CHOICE); } @Test + @RequiresFlagsEnabled(Flags.FLAG_APP_CONTENT_SHARING) + public void testDefaultProjectionSources() { + assertThat(USERS_CHOICE_CONFIG.getProjectionSources()) + .isEqualTo(DEFAULT_PROJECTION_SOURCES); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_APP_CONTENT_SHARING) + public void testCreateDisplayConfigProjectionSource() { + assertThat(DISPLAY_CONFIG.getProjectionSources()).isEqualTo(PROJECTION_SOURCE_DISPLAY); + assertThat(DISPLAY_CONFIG.getDisplayToCapture()).isEqualTo(DEFAULT_DISPLAY); + } + + @Test public void testEquals() { assertThat(MediaProjectionConfig.createConfigForUserChoice()).isEqualTo( USERS_CHOICE_CONFIG); diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml index 60f209b47482..574671376e2e 100644 --- a/packages/CompanionDeviceManager/res/values/strings.xml +++ b/packages/CompanionDeviceManager/res/values/strings.xml @@ -106,16 +106,13 @@ <!-- Description of the helper dialog for NEARBY_DEVICE_STREAMING profile. [CHAR LIMIT=NONE] --> <string name="helper_summary_nearby_device_streaming"><xliff:g id="app_name" example="Exo">%1$s</xliff:g> is requesting permission on behalf of <xliff:g id="device_name" example="Chromebook">%2$s</xliff:g> to stream apps from your <xliff:g id="device_type" example="phone">%3$s</xliff:g></string> - <!-- ================= DEVICE_PROFILE_SENSOR_DEVICE_STREAMING ================= --> + <!-- ================= DEVICE_PROFILE_VIRTUAL_DEVICE ================= --> - <!-- Confirmation for associating an application with a companion device of SENSOR_DEVICE_STREAMING profile (type) [CHAR LIMIT=NONE] --> - <string name="title_sensor_device_streaming">Allow <strong><xliff:g id="app_name" example="Exo">%1$s</xliff:g></strong> to stream audio and system features between your <xliff:g id="device_type" example="phone">%2$s</xliff:g> and <strong><xliff:g id="device_name" example="Chromebook">%3$s</xliff:g></strong>?</string> + <!-- Confirmation for associating an application with a companion device of VIRTUAL_DEVICE profile (type) [CHAR LIMIT=NONE] --> + <string name="title_virtual_device">Allow <strong><xliff:g id="app_name" example="Exo">%1$s</xliff:g></strong> to stream audio and system features between your <xliff:g id="device_type" example="phone">%3$s</xliff:g> and <strong><xliff:g id="device_name" example="Chromebook">%2$s</xliff:g></strong>?</string> - <!-- Summary for associating an application with a companion device of SENSOR_DEVICE_STREAMING profile [CHAR LIMIT=NONE] --> - <string name="summary_sensor_device_streaming"><xliff:g id="app_name" example="Exo">%1$s</xliff:g> will have access to anything that’s played on your <xliff:g id="device_name" example="Chromebook">%3$s</xliff:g>.<br/><br/><xliff:g id="app_name" example="Exo">%1$s</xliff:g> will be able to stream audio to <xliff:g id="device_name" example="Chromebook">%3$s</xliff:g> until you remove access to this permission.</string> - - <!-- Description of the helper dialog for SENSOR_DEVICE_STREAMING profile. [CHAR LIMIT=NONE] --> - <string name="helper_summary_sensor_device_streaming"><xliff:g id="app_name" example="Exo">%1$s</xliff:g> is requesting permission on behalf of <xliff:g id="device_name" example="Chromebook">%2$s</xliff:g> to stream audio and system features between your devices.</string> + <!-- Summary for associating an application with a companion device of VIRTUAL_DEVICE profile [CHAR LIMIT=NONE] --> + <string name="summary_virtual_device"><xliff:g id="app_name" example="Exo">%2$s</xliff:g> will have access to anything that’s played on <xliff:g id="device_name" example="Chromebook">%3$s</xliff:g>.<br/><br/><xliff:g id="app_name" example="Exo">%2$s</xliff:g> will be able to stream audio to <xliff:g id="device_name" example="Chromebook">%3$s</xliff:g> until you remove access to this permission.</string> <!-- ================= null profile ================= --> diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java index 964268e4ad14..c07e572eb649 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java @@ -359,11 +359,7 @@ public class CompanionAssociationActivity extends FragmentActivity implements if (CompanionDeviceDiscoveryService.getScanResult().getValue().isEmpty()) { // If the scan times out, do NOT close the activity automatically and let the // user manually cancel the flow. - synchronized (LOCK) { - if (sDiscoveryStarted) { - stopDiscovery(); - } - } + stopDiscovery(); mTimeoutMessage.setText(getString(R.string.message_discovery_hard_timeout)); mTimeoutMessage.setVisibility(View.VISIBLE); } @@ -455,8 +451,14 @@ public class CompanionAssociationActivity extends FragmentActivity implements } private void stopDiscovery() { - if (mRequest != null && !mRequest.isSelfManaged()) { - CompanionDeviceDiscoveryService.stop(this); + if (mRequest == null || mRequest.isSelfManaged()) { + return; + } + + synchronized (LOCK) { + if (sDiscoveryStarted) { + CompanionDeviceDiscoveryService.stop(this); + } } } @@ -665,7 +667,8 @@ public class CompanionAssociationActivity extends FragmentActivity implements final int summaryResourceId = PROFILE_SUMMARIES.get(deviceProfile); final String remoteDeviceName = mSelectedDevice.getDisplayName(); final Spanned title = getHtmlFromResources( - this, PROFILE_TITLES.get(deviceProfile), mAppLabel, remoteDeviceName); + this, PROFILE_TITLES.get(deviceProfile), mAppLabel, remoteDeviceName, + getString(R.string.device_type)); final Spanned summary; if (deviceProfile == null && mRequest.isSingleDevice()) { @@ -673,7 +676,8 @@ public class CompanionAssociationActivity extends FragmentActivity implements mConstraintList.setVisibility(View.GONE); } else { summary = getHtmlFromResources( - this, summaryResourceId, getString(R.string.device_type)); + this, summaryResourceId, getString(R.string.device_type), mAppLabel, + remoteDeviceName); setupPermissionList(deviceProfile); } diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java index 50a01b3bc7c9..7b4794506adb 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java @@ -136,7 +136,12 @@ public class CompanionDeviceDiscoveryService extends Service { intent.setAction(ACTION_START_DISCOVERY); intent.putExtra(EXTRA_ASSOCIATION_REQUEST, associationRequest); - context.startService(intent); + try { + context.startService(intent); + } catch (IllegalStateException e) { + Slog.e(TAG, "Failed to start discovery.", e); + return false; + } return true; } @@ -144,7 +149,12 @@ public class CompanionDeviceDiscoveryService extends Service { static void stop(@NonNull Context context) { final Intent intent = new Intent(context, CompanionDeviceDiscoveryService.class); intent.setAction(ACTION_STOP_DISCOVERY); - context.startService(intent); + + try { + context.startService(intent); + } catch (IllegalStateException e) { + Slog.e(TAG, "Failed to stop discovery.", e); + } } static LiveData<List<DeviceFilterPair<?>>> getScanResult() { diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java index f756a6235c14..f6e680207530 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java @@ -21,7 +21,7 @@ import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PRO import static android.companion.AssociationRequest.DEVICE_PROFILE_COMPUTER; import static android.companion.AssociationRequest.DEVICE_PROFILE_GLASSES; import static android.companion.AssociationRequest.DEVICE_PROFILE_NEARBY_DEVICE_STREAMING; -import static android.companion.AssociationRequest.DEVICE_PROFILE_SENSOR_DEVICE_STREAMING; +import static android.companion.AssociationRequest.DEVICE_PROFILE_VIRTUAL_DEVICE; import static android.companion.AssociationRequest.DEVICE_PROFILE_WATCH; import static android.companion.AssociationRequest.DEVICE_PROFILE_WEARABLE_SENSING; import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; @@ -118,7 +118,7 @@ final class CompanionDeviceResources { map.put(DEVICE_PROFILE_AUTOMOTIVE_PROJECTION, R.string.title_automotive_projection); map.put(DEVICE_PROFILE_COMPUTER, R.string.title_computer); map.put(DEVICE_PROFILE_NEARBY_DEVICE_STREAMING, R.string.title_nearby_device_streaming); - map.put(DEVICE_PROFILE_SENSOR_DEVICE_STREAMING, R.string.title_sensor_device_streaming); + map.put(DEVICE_PROFILE_VIRTUAL_DEVICE, R.string.title_virtual_device); map.put(DEVICE_PROFILE_WATCH, R.string.confirmation_title); map.put(DEVICE_PROFILE_GLASSES, R.string.confirmation_title_glasses); map.put(null, R.string.confirmation_title); @@ -133,7 +133,7 @@ final class CompanionDeviceResources { map.put(DEVICE_PROFILE_GLASSES, R.string.summary_glasses); map.put(DEVICE_PROFILE_APP_STREAMING, R.string.summary_app_streaming); map.put(DEVICE_PROFILE_NEARBY_DEVICE_STREAMING, R.string.summary_nearby_device_streaming); - map.put(DEVICE_PROFILE_SENSOR_DEVICE_STREAMING, R.string.summary_sensor_device_streaming); + map.put(DEVICE_PROFILE_VIRTUAL_DEVICE, R.string.summary_virtual_device); map.put(null, R.string.summary_generic); PROFILE_SUMMARIES = unmodifiableMap(map); @@ -145,8 +145,6 @@ final class CompanionDeviceResources { map.put(DEVICE_PROFILE_APP_STREAMING, R.string.helper_summary_app_streaming); map.put(DEVICE_PROFILE_NEARBY_DEVICE_STREAMING, R.string.helper_summary_nearby_device_streaming); - map.put(DEVICE_PROFILE_SENSOR_DEVICE_STREAMING, - R.string.helper_summary_sensor_device_streaming); map.put(DEVICE_PROFILE_COMPUTER, R.string.helper_summary_computer); PROFILE_HELPER_SUMMARIES = unmodifiableMap(map); @@ -178,6 +176,7 @@ final class CompanionDeviceResources { final Map<String, Integer> map = new ArrayMap<>(); map.put(DEVICE_PROFILE_WATCH, R.string.profile_name_watch); map.put(DEVICE_PROFILE_GLASSES, R.string.profile_name_glasses); + map.put(DEVICE_PROFILE_VIRTUAL_DEVICE, R.string.profile_name_generic); map.put(null, R.string.profile_name_generic); PROFILE_NAMES = unmodifiableMap(map); @@ -188,6 +187,7 @@ final class CompanionDeviceResources { final Map<String, Integer> map = new ArrayMap<>(); map.put(DEVICE_PROFILE_WATCH, R.drawable.ic_watch); map.put(DEVICE_PROFILE_GLASSES, R.drawable.ic_glasses); + map.put(DEVICE_PROFILE_VIRTUAL_DEVICE, R.drawable.ic_device_other); map.put(null, R.drawable.ic_device_other); PROFILE_ICONS = unmodifiableMap(map); @@ -198,6 +198,7 @@ final class CompanionDeviceResources { final Set<String> set = new ArraySet<>(); set.add(DEVICE_PROFILE_WATCH); set.add(DEVICE_PROFILE_GLASSES); + set.add(DEVICE_PROFILE_VIRTUAL_DEVICE); set.add(null); SUPPORTED_PROFILES = unmodifiableSet(set); @@ -210,7 +211,6 @@ final class CompanionDeviceResources { set.add(DEVICE_PROFILE_COMPUTER); set.add(DEVICE_PROFILE_AUTOMOTIVE_PROJECTION); set.add(DEVICE_PROFILE_NEARBY_DEVICE_STREAMING); - set.add(DEVICE_PROFILE_SENSOR_DEVICE_STREAMING); set.add(DEVICE_PROFILE_WEARABLE_SENSING); set.add(null); diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java index defbc1142adb..28b891ebc3c9 100644 --- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java +++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java @@ -596,7 +596,10 @@ public class ExternalStorageProvider extends FileSystemProvider { } @Override - protected void onDocIdDeleted(String docId) { + protected void onDocIdDeleted(String docId, boolean shouldRevokeUriPermission) { + if (!shouldRevokeUriPermission) { + return; + } Uri uri = DocumentsContract.buildDocumentUri(AUTHORITY, docId); getContext().revokeUriPermission(uri, ~0); } diff --git a/packages/FusedLocation/AndroidManifest.xml b/packages/FusedLocation/AndroidManifest.xml index 158c33ae2035..56fe38905660 100644 --- a/packages/FusedLocation/AndroidManifest.xml +++ b/packages/FusedLocation/AndroidManifest.xml @@ -30,6 +30,9 @@ <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" /> <uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" /> + <attribution android:tag="FusedOverlayService" android:label="@string/fused_overlay_service"/> + <attribution android:tag="GnssOverlayService" android:label="@string/gnss_overlay_service"/> + <application android:label="@string/app_label" android:process="system" diff --git a/packages/FusedLocation/res/values/strings.xml b/packages/FusedLocation/res/values/strings.xml index 5b78e39d1ba6..25e1fe7677bb 100644 --- a/packages/FusedLocation/res/values/strings.xml +++ b/packages/FusedLocation/res/values/strings.xml @@ -2,4 +2,8 @@ <resources> <!-- Name of the application. [CHAR LIMIT=35] --> <string name="app_label">Fused Location</string> + <!-- Attribution for Fused Overlay Service. [CHAR LIMIT=NONE]--> + <string name="fused_overlay_service">Fused Overlay Service</string> + <!-- Attribution for GNSS Overlay Service. [CHAR LIMIT=NONE]--> + <string name="gnss_overlay_service">GNSS Overlay Service</string> </resources> diff --git a/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java b/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java index a0e008c9437f..78b2f7e52ca5 100644 --- a/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java +++ b/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java @@ -53,6 +53,7 @@ import java.util.concurrent.atomic.AtomicInteger; public class FusedLocationProvider extends LocationProviderBase { private static final String TAG = "FusedLocationProvider"; + private static final String ATTRIBUTION_TAG = "FusedOverlayService"; private static final ProviderProperties PROPERTIES = new ProviderProperties.Builder() .setHasAltitudeSupport(true) @@ -89,8 +90,12 @@ public class FusedLocationProvider extends LocationProviderBase { public FusedLocationProvider(Context context) { super(context, TAG, PROPERTIES); - mContext = context; - mLocationManager = Objects.requireNonNull(context.getSystemService(LocationManager.class)); + if (Flags.missingAttributionTagsInOverlay()) { + mContext = context.createAttributionContext(ATTRIBUTION_TAG); + } else { + mContext = context; + } + mLocationManager = Objects.requireNonNull(mContext.getSystemService(LocationManager.class)); mGpsListener = new ChildLocationListener(GPS_PROVIDER); mNetworkListener = new ChildLocationListener(NETWORK_PROVIDER); diff --git a/packages/FusedLocation/src/com/android/location/gnss/GnssOverlayLocationProvider.java b/packages/FusedLocation/src/com/android/location/gnss/GnssOverlayLocationProvider.java index c6576e39de99..86bcd99822fc 100644 --- a/packages/FusedLocation/src/com/android/location/gnss/GnssOverlayLocationProvider.java +++ b/packages/FusedLocation/src/com/android/location/gnss/GnssOverlayLocationProvider.java @@ -21,6 +21,7 @@ import static android.location.provider.ProviderProperties.POWER_USAGE_HIGH; import android.annotation.Nullable; import android.content.Context; +import android.location.flags.Flags; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; @@ -41,6 +42,7 @@ import java.util.List; public class GnssOverlayLocationProvider extends LocationProviderBase { private static final String TAG = "GnssOverlay"; + private static final String ATTRIBUTION_TAG = "GnssOverlayService"; private static final ProviderProperties PROPERTIES = new ProviderProperties.Builder() .setHasAltitudeSupport(true) @@ -87,7 +89,13 @@ public class GnssOverlayLocationProvider extends LocationProviderBase { public GnssOverlayLocationProvider(Context context) { super(context, TAG, PROPERTIES); - mLocationManager = context.getSystemService(LocationManager.class); + + if (Flags.missingAttributionTagsInOverlay()) { + Context contextWithAttribution = context.createAttributionContext(ATTRIBUTION_TAG); + mLocationManager = contextWithAttribution.getSystemService(LocationManager.class); + } else { + mLocationManager = context.getSystemService(LocationManager.class); + } } void start() { diff --git a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_button_background_normal.xml b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_button_background_normal.xml index 8037a8bb75be..8a234fa6ca9e 100644 --- a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_button_background_normal.xml +++ b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_button_background_normal.xml @@ -17,8 +17,8 @@ <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_enabled="false" - android:alpha="@dimen/material_emphasis_disabled_background" android:color="?attr/colorOnSurface"/> + android:alpha="@dimen/material_emphasis_disabled_background" android:color="@color/settingslib_materialColorPrimary"/> <item android:state_checked="true" android:color="?attr/colorContainerChecked"/> <item android:state_checkable="true" android:color="?attr/colorContainerUnchecked"/> - <item android:color="?attr/colorContainer" /> + <item android:color="@color/settingslib_materialColorPrimary" /> </selector>
\ No newline at end of file diff --git a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_high.xml b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_high.xml new file mode 100644 index 000000000000..43b236938956 --- /dev/null +++ b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_high.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false" + android:alpha="@dimen/material_emphasis_disabled" android:color="@color/settingslib_colorContentLevel_high"/> + <item android:color="@color/settingslib_colorContentLevel_high" /> +</selector>
\ No newline at end of file diff --git a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_low.xml b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_low.xml new file mode 100644 index 000000000000..b7a9d7c5175b --- /dev/null +++ b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_low.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false" + android:alpha="@dimen/material_emphasis_disabled" android:color="@color/settingslib_colorContentLevel_low"/> + <item android:color="@color/settingslib_colorContentLevel_low" /> +</selector>
\ No newline at end of file diff --git a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_medium.xml b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_medium.xml new file mode 100644 index 000000000000..8e41cb03f4d1 --- /dev/null +++ b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_medium.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false" + android:alpha="@dimen/material_emphasis_disabled" android:color="@color/settingslib_colorContentLevel_medium"/> + <item android:color="@color/settingslib_colorContentLevel_medium" /> +</selector>
\ No newline at end of file diff --git a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_normal.xml b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_normal.xml new file mode 100644 index 000000000000..1dd5cdecfffc --- /dev/null +++ b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_normal.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false" + android:alpha="@dimen/material_emphasis_disabled" android:color="@color/settingslib_materialColorOnPrimary"/> + <item android:color="@color/settingslib_materialColorOnPrimary" /> +</selector>
\ No newline at end of file diff --git a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_outline_button_content.xml b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_outline_button_content.xml new file mode 100644 index 000000000000..3a06fb38d5d8 --- /dev/null +++ b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_outline_button_content.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false" + android:alpha="@dimen/material_emphasis_disabled" android:color="@color/settingslib_materialColorOnSurface"/> + <item android:color="@color/settingslib_materialColorOnSurface" /> +</selector>
\ No newline at end of file diff --git a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_outline_button_stroke_normal.xml b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_outline_button_stroke_normal.xml new file mode 100644 index 000000000000..8d0b65712d35 --- /dev/null +++ b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_outline_button_stroke_normal.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false" + android:alpha="@dimen/material_emphasis_disabled_background" android:color="@color/settingslib_materialColorOutline"/> + <item android:color="@color/settingslib_materialColorOutline" /> +</selector>
\ No newline at end of file diff --git a/packages/SettingsLib/BannerMessagePreference/res/values-v36/styles_expressive.xml b/packages/SettingsLib/BannerMessagePreference/res/values-v36/styles_expressive.xml index 09e07ccef683..cd9faecc49c4 100644 --- a/packages/SettingsLib/BannerMessagePreference/res/values-v36/styles_expressive.xml +++ b/packages/SettingsLib/BannerMessagePreference/res/values-v36/styles_expressive.xml @@ -64,7 +64,6 @@ <style name="Banner.PositiveButton.SettingsLib.Expressive" parent="@style/SettingsLibButtonStyle.Expressive.Filled.Extra"> - <item name="android:textColor">?android:attr/textColorPrimaryInverse</item> <item name="materialSizeOverlay">@style/SizeOverlay.Material3Expressive.Button.Small</item> </style> diff --git a/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java b/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java index c90a76a39510..dbd0f6424ff8 100644 --- a/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java +++ b/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java @@ -58,35 +58,42 @@ public class BannerMessagePreference extends Preference implements GroupSectionD HIGH(0, R.color.banner_background_attention_high, R.color.banner_accent_attention_high, - R.color.settingslib_banner_button_background_high), + R.color.settingslib_banner_button_background_high, + R.color.settingslib_banner_filled_button_content_high), MEDIUM(1, R.color.banner_background_attention_medium, R.color.banner_accent_attention_medium, - R.color.settingslib_banner_button_background_medium), + R.color.settingslib_banner_button_background_medium, + R.color.settingslib_banner_filled_button_content_medium), LOW(2, R.color.banner_background_attention_low, R.color.banner_accent_attention_low, - R.color.settingslib_banner_button_background_low), + R.color.settingslib_banner_button_background_low, + R.color.settingslib_banner_filled_button_content_low), NORMAL(3, R.color.banner_background_attention_normal, R.color.banner_accent_attention_normal, - R.color.settingslib_banner_button_background_normal); + R.color.settingslib_banner_button_background_normal, + R.color.settingslib_banner_filled_button_content_normal); // Corresponds to the enum value of R.attr.attentionLevel private final int mAttrValue; @ColorRes private final int mBackgroundColorResId; @ColorRes private final int mAccentColorResId; @ColorRes private final int mButtonBackgroundColorResId; + @ColorRes private final int mButtonContentColorResId; AttentionLevel( int attrValue, @ColorRes int backgroundColorResId, @ColorRes int accentColorResId, - @ColorRes int buttonBackgroundColorResId) { + @ColorRes int buttonBackgroundColorResId, + @ColorRes int buttonContentColorResId) { mAttrValue = attrValue; mBackgroundColorResId = backgroundColorResId; mAccentColorResId = accentColorResId; mButtonBackgroundColorResId = buttonBackgroundColorResId; + mButtonContentColorResId = buttonContentColorResId; } static AttentionLevel fromAttr(int attrValue) { @@ -109,6 +116,10 @@ public class BannerMessagePreference extends Preference implements GroupSectionD public @ColorRes int getButtonBackgroundColorResId() { return mButtonBackgroundColorResId; } + + public @ColorRes int getButtonContentColorResId() { + return mButtonContentColorResId; + } } private static final String TAG = "BannerPreference"; @@ -181,6 +192,7 @@ public class BannerMessagePreference extends Preference implements GroupSectionD public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { super.onBindViewHolder(holder); final Context context = getContext(); + final Resources resources = context.getResources(); final TextView titleView = (TextView) holder.findViewById(R.id.banner_title); CharSequence title = getTitle(); @@ -200,7 +212,7 @@ public class BannerMessagePreference extends Preference implements GroupSectionD final Resources.Theme theme = context.getTheme(); @ColorInt final int accentColor = - context.getResources().getColor(mAttentionLevel.getAccentColorResId(), theme); + resources.getColor(mAttentionLevel.getAccentColorResId(), theme); final ImageView iconView = (ImageView) holder.findViewById(R.id.banner_icon); if (iconView != null) { @@ -211,9 +223,7 @@ public class BannerMessagePreference extends Preference implements GroupSectionD } else { iconView.setVisibility(View.VISIBLE); iconView.setImageDrawable( - icon == null - ? getContext().getDrawable(R.drawable.ic_warning) - : icon); + icon == null ? context.getDrawable(R.drawable.ic_warning) : icon); if (mAttentionLevel != AttentionLevel.NORMAL && !SettingsThemeHelper.isExpressiveTheme(context)) { iconView.setColorFilter( @@ -224,14 +234,24 @@ public class BannerMessagePreference extends Preference implements GroupSectionD if (IS_AT_LEAST_S) { @ColorInt final int backgroundColor = - context.getResources().getColor( - mAttentionLevel.getBackgroundColorResId(), theme); - - @ColorInt final int btnBackgroundColor = - context.getResources().getColor(mAttentionLevel.getButtonBackgroundColorResId(), - theme); - ColorStateList strokeColor = context.getResources().getColorStateList( - mAttentionLevel.getButtonBackgroundColorResId(), theme); + resources.getColor(mAttentionLevel.getBackgroundColorResId(), theme); + + ColorStateList btnBackgroundColor = + resources.getColorStateList( + mAttentionLevel.getButtonBackgroundColorResId(), theme); + ColorStateList btnStrokeColor = + mAttentionLevel == AttentionLevel.NORMAL + ? resources.getColorStateList( + R.color.settingslib_banner_outline_button_stroke_normal, theme) + : btnBackgroundColor; + ColorStateList filledBtnTextColor = + resources.getColorStateList( + mAttentionLevel.getButtonContentColorResId(), theme); + ColorStateList outlineBtnTextColor = + mAttentionLevel == AttentionLevel.NORMAL + ? btnBackgroundColor + : resources.getColorStateList( + R.color.settingslib_banner_outline_button_content, theme); holder.setDividerAllowedAbove(false); holder.setDividerAllowedBelow(false); @@ -242,10 +262,10 @@ public class BannerMessagePreference extends Preference implements GroupSectionD mPositiveButtonInfo.mColor = accentColor; mNegativeButtonInfo.mColor = accentColor; - if (mAttentionLevel != AttentionLevel.NORMAL) { - mPositiveButtonInfo.mBackgroundColor = btnBackgroundColor; - mNegativeButtonInfo.mStrokeColor = strokeColor; - } + mPositiveButtonInfo.mBackgroundColor = btnBackgroundColor; + mPositiveButtonInfo.mTextColor = filledBtnTextColor; + mNegativeButtonInfo.mStrokeColor = btnStrokeColor; + mNegativeButtonInfo.mTextColor = outlineBtnTextColor; mDismissButtonInfo.mButton = (ImageButton) holder.findViewById(R.id.banner_dismiss_btn); mDismissButtonInfo.setUpButton(); @@ -261,8 +281,6 @@ public class BannerMessagePreference extends Preference implements GroupSectionD headerView.setText(mHeader); headerView.setVisibility(TextUtils.isEmpty(mHeader) ? View.GONE : View.VISIBLE); } - - } else { holder.setDividerAllowedAbove(true); holder.setDividerAllowedBelow(true); @@ -567,8 +585,9 @@ public class BannerMessagePreference extends Preference implements GroupSectionD private boolean mIsVisible = true; private boolean mIsEnabled = true; @ColorInt private int mColor; - @ColorInt private int mBackgroundColor; + @Nullable private ColorStateList mBackgroundColor; @Nullable private ColorStateList mStrokeColor; + @Nullable private ColorStateList mTextColor; void setUpButton() { if (mButton == null) { @@ -586,12 +605,15 @@ public class BannerMessagePreference extends Preference implements GroupSectionD if (IS_AT_LEAST_S) { if (btn != null && SettingsThemeHelper.isExpressiveTheme(btn.getContext())) { - if (mBackgroundColor != 0) { - btn.setBackgroundColor(mBackgroundColor); + if (mBackgroundColor != null) { + btn.setBackgroundTintList(mBackgroundColor); } if (mStrokeColor != null) { btn.setStrokeColor(mStrokeColor); } + if (mTextColor != null) { + btn.setTextColor(mTextColor); + } } else { mButton.setTextColor(mColor); } diff --git a/packages/SettingsLib/Metadata/Android.bp b/packages/SettingsLib/Metadata/Android.bp index 564c3985264d..8701d3d8daae 100644 --- a/packages/SettingsLib/Metadata/Android.bp +++ b/packages/SettingsLib/Metadata/Android.bp @@ -19,4 +19,7 @@ android_library { "androidx.fragment_fragment", ], kotlincflags: ["-Xjvm-default=all"], + optimize: { + proguard_flags_files: ["proguard.pgcfg"], + }, } diff --git a/packages/SettingsLib/Metadata/proguard.pgcfg b/packages/SettingsLib/Metadata/proguard.pgcfg new file mode 100644 index 000000000000..3a137732a229 --- /dev/null +++ b/packages/SettingsLib/Metadata/proguard.pgcfg @@ -0,0 +1,8 @@ +# Preserve names for IPC codec to support unmarshalling Parcelable +-keepnames class com.android.settingslib.metadata.PreferenceCoordinate { + public static final ** CREATOR; +} + +-keepnames class com.android.settingslib.metadata.PreferenceScreenCoordinate { + public static final ** CREATOR; +} diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v36/settingslib_expressive_preference_selector_with_widget.xml b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v36/settingslib_preference_selector_with_widget.xml index a79d69dbff8c..adaec8524241 100644 --- a/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v36/settingslib_expressive_preference_selector_with_widget.xml +++ b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v36/settingslib_preference_selector_with_widget.xml @@ -23,34 +23,20 @@ android:background="?android:attr/selectableItemBackground" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeightSmall" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> <LinearLayout android:id="@android:id/widget_frame" android:layout_width="wrap_content" - android:layout_height="match_parent" - android:paddingEnd="@dimen/settingslib_expressive_space_extrasmall6" + android:layout_height="wrap_content" android:gravity="center" - android:minWidth="32dp" + android:minWidth="@dimen/settingslib_expressive_space_medium3" + android:minHeight="@dimen/settingslib_expressive_space_medium3" + android:layout_marginEnd="@dimen/settingslib_expressive_space_extrasmall6" android:orientation="vertical"/> - <LinearLayout - android:id="@+id/icon_frame" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:gravity="center_vertical" - android:minWidth="32dp" - android:orientation="horizontal" - android:layout_marginEnd="@dimen/settingslib_expressive_space_small1" - android:paddingTop="@dimen/settingslib_expressive_space_extrasmall2" - android:paddingBottom="@dimen/settingslib_expressive_space_extrasmall2"> - <androidx.preference.internal.PreferenceImageView - android:id="@android:id/icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - settings:maxWidth="@dimen/secondary_app_icon_size" - settings:maxHeight="@dimen/secondary_app_icon_size"/> - </LinearLayout> + <include layout="@layout/settingslib_expressive_preference_icon_frame"/> <LinearLayout android:layout_width="0dp" diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java b/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java index 465b6ccf4d9c..cde8b332f2e7 100644 --- a/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java +++ b/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java @@ -238,10 +238,7 @@ public class SelectorWithWidgetPreference extends CheckBoxPreference { } else { setWidgetLayoutResource(R.layout.settingslib_preference_widget_radiobutton); } - int resID = SettingsThemeHelper.isExpressiveTheme(context) - ? R.layout.settingslib_expressive_preference_selector_with_widget - : R.layout.preference_selector_with_widget; - setLayoutResource(resID); + setLayoutResource(R.layout.preference_selector_with_widget); setIconSpaceReserved(false); final TypedArray a = diff --git a/packages/SettingsLib/SettingsSpinner/res/drawable-v36/settings_expressive_spinner_dropdown_background.xml b/packages/SettingsLib/SettingsSpinner/res/drawable-v36/settings_expressive_spinner_dropdown_background.xml new file mode 100644 index 000000000000..f29f3ae79fa6 --- /dev/null +++ b/packages/SettingsLib/SettingsSpinner/res/drawable-v36/settings_expressive_spinner_dropdown_background.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + + <!-- Highlight the selected item --> + <item android:state_activated="true" android:drawable="@drawable/settings_expressive_spinner_dropdown_item_selected"/> +</selector> diff --git a/packages/SettingsLib/SettingsSpinner/res/drawable-v36/settings_expressive_spinner_dropdown_item_selected.xml b/packages/SettingsLib/SettingsSpinner/res/drawable-v36/settings_expressive_spinner_dropdown_item_selected.xml new file mode 100644 index 000000000000..5da3f7172582 --- /dev/null +++ b/packages/SettingsLib/SettingsSpinner/res/drawable-v36/settings_expressive_spinner_dropdown_item_selected.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="?android:colorControlHighlight"> + <item> + <shape android:shape="rectangle"> + <solid + android:color="@color/settingslib_materialColorPrimaryContainer" /> + <corners + android:radius="@dimen/settingslib_expressive_radius_large2" /> + </shape> + </item> +</ripple> diff --git a/packages/SettingsLib/SettingsSpinner/res/layout-v33/settings_spinner_view.xml b/packages/SettingsLib/SettingsSpinner/res/layout-v33/settings_spinner_view.xml index 1d0c9b941881..3c379bf0162d 100644 --- a/packages/SettingsLib/SettingsSpinner/res/layout-v33/settings_spinner_view.xml +++ b/packages/SettingsLib/SettingsSpinner/res/layout-v33/settings_spinner_view.xml @@ -18,6 +18,8 @@ <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/text1" + android:layout_centerVertical="true" + android:gravity="center_vertical" style="@style/SettingsSpinnerTitleBar" android:layout_width="wrap_content" android:layout_height="wrap_content"/> diff --git a/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_preference_full.xml b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_preference_full.xml new file mode 100644 index 000000000000..6d1057c8780b --- /dev/null +++ b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_preference_full.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?android:attr/listPreferredItemHeight" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:filterTouchesWhenObscured="true"> + <Spinner + android:id="@+id/spinner" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_centerVertical="true"/> +</RelativeLayout> diff --git a/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_preference_full_outlined.xml b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_preference_full_outlined.xml new file mode 100644 index 000000000000..217d1431cd18 --- /dev/null +++ b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_preference_full_outlined.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?android:attr/listPreferredItemHeight" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:filterTouchesWhenObscured="true"> + + <Spinner + android:id="@+id/spinner" + style="@style/SettingslibSpinnerStyle.Expressive.Outlined" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_centerVertical="true"/> +</RelativeLayout> diff --git a/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_preference_outlined.xml b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_preference_outlined.xml new file mode 100644 index 000000000000..3aefb887cedb --- /dev/null +++ b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_preference_outlined.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?android:attr/listPreferredItemHeight" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:filterTouchesWhenObscured="true"> + + <Spinner + android:id="@+id/spinner" + style="@style/SettingslibSpinnerStyle.Expressive.Outlined" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerVertical="true"/> +</RelativeLayout> diff --git a/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_full.xml b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_full.xml new file mode 100644 index 000000000000..d3832f786ccb --- /dev/null +++ b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_full.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<TextView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@android:id/text1" + android:layout_centerVertical="true" + android:gravity="center_vertical" + style="@style/SettingsSpinnerTitleBar.Expressive.Large" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:filterTouchesWhenObscured="true"/> diff --git a/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_full_outlined.xml b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_full_outlined.xml new file mode 100644 index 000000000000..2c172e955a09 --- /dev/null +++ b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_full_outlined.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<TextView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@android:id/text1" + android:layout_centerVertical="true" + android:gravity="center_vertical" + style="@style/SettingsSpinnerTitleBar.Expressive.Large.Outlined" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:filterTouchesWhenObscured="true"/> diff --git a/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_large.xml b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_large.xml new file mode 100644 index 000000000000..3e7f0fa7ca4f --- /dev/null +++ b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_large.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<TextView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@android:id/text1" + android:layout_centerVertical="true" + android:gravity="center_vertical" + style="@style/SettingsSpinnerTitleBar.Expressive.Large" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:filterTouchesWhenObscured="true"/> diff --git a/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_large_outlined.xml b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_large_outlined.xml new file mode 100644 index 000000000000..6601c8cd97a5 --- /dev/null +++ b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_large_outlined.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<TextView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@android:id/text1" + android:layout_centerVertical="true" + android:gravity="center_vertical" + style="@style/SettingsSpinnerTitleBar.Expressive.Large.Outlined" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:filterTouchesWhenObscured="true"/> diff --git a/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressvie_spinner_dropdown_view.xml b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressvie_spinner_dropdown_view.xml new file mode 100644 index 000000000000..acf2a0dd5858 --- /dev/null +++ b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressvie_spinner_dropdown_view.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@style/SettingsSpinnerDropdown.Expressive"> + <ImageView + android:id="@android:id/icon" + android:layout_width="@dimen/settingslib_expressive_space_small3" + android:layout_height="@dimen/settingslib_expressive_space_small3" + android:importantForAccessibility="no" + android:src="@drawable/settingslib_expressive_icon_check" + android:tint="@color/settingslib_spinner_dropdown_color" + android:layout_gravity="center_vertical" + android:layout_marginEnd="@dimen/settingslib_expressive_space_extrasmall4" + android:scaleType="centerInside"/> + + <TextView + android:id="@android:id/text1" + style="@style/SettingsSpinnerDropdownText" + android:gravity="center_vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:filterTouchesWhenObscured="true"/> +</LinearLayout> diff --git a/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressvie_spinner_view.xml b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressvie_spinner_view.xml new file mode 100644 index 000000000000..e300099ee298 --- /dev/null +++ b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressvie_spinner_view.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<TextView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@android:id/text1" + android:layout_centerVertical="true" + android:gravity="center_vertical" + style="@style/SettingsSpinnerTitleBar.Expressive" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:filterTouchesWhenObscured="true"/> diff --git a/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressvie_spinner_view_outlined.xml b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressvie_spinner_view_outlined.xml new file mode 100644 index 000000000000..73e254e9bc15 --- /dev/null +++ b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressvie_spinner_view_outlined.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<TextView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@android:id/text1" + android:layout_centerVertical="true" + android:gravity="center_vertical" + style="@style/SettingsSpinnerTitleBar.Expressive.Outlined" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:filterTouchesWhenObscured="true"/> diff --git a/packages/SettingsLib/SettingsSpinner/res/values-v33/styles.xml b/packages/SettingsLib/SettingsSpinner/res/values-v33/styles.xml index 6e26ae180685..2d720d210def 100644 --- a/packages/SettingsLib/SettingsSpinner/res/values-v33/styles.xml +++ b/packages/SettingsLib/SettingsSpinner/res/values-v33/styles.xml @@ -27,6 +27,7 @@ <item name="android:paddingEnd">36dp</item> <item name="android:paddingTop">@dimen/spinner_padding_top_or_bottom</item> <item name="android:paddingBottom">@dimen/spinner_padding_top_or_bottom</item> + <item name="android:filterTouchesWhenObscured">true</item> </style> <style name="SettingsSpinnerDropdown"> @@ -40,5 +41,6 @@ <item name="android:paddingEnd">36dp</item> <item name="android:paddingTop">@dimen/spinner_padding_top_or_bottom</item> <item name="android:paddingBottom">@dimen/spinner_padding_top_or_bottom</item> + <item name="android:filterTouchesWhenObscured">true</item> </style> </resources> diff --git a/packages/SettingsLib/SettingsSpinner/res/values-v36/attr.xml b/packages/SettingsLib/SettingsSpinner/res/values-v36/attr.xml new file mode 100644 index 000000000000..154149acf26d --- /dev/null +++ b/packages/SettingsLib/SettingsSpinner/res/values-v36/attr.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2025 The Android Open Source Project + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<resources> + <attr name="SettingsSpinnerPreferenceStyle" format="reference"/> + <declare-styleable name="SettingsSpinnerPreference"> + <attr name="style" format="enum"> + <enum name="normal" value="0"/> + <enum name="large" value="1"/> + <enum name="full" value="2"/> + <enum name="outlined" value="3"/> + <enum name="large_outlined" value="4"/> + <enum name="full_outlined" value="5"/> + </attr> + </declare-styleable> +</resources> diff --git a/packages/SettingsLib/SettingsSpinner/res/values-v36/styles.xml b/packages/SettingsLib/SettingsSpinner/res/values-v36/styles.xml new file mode 100644 index 000000000000..2cb4518af287 --- /dev/null +++ b/packages/SettingsLib/SettingsSpinner/res/values-v36/styles.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources> + <style name="SettingsSpinnerTitleBar.Expressive"> + <item name="android:textAppearance">@style/TextAppearance.SettingsLib.LabelLarge</item> + <item name="android:textColor">@color/settingslib_materialColorOnSecondaryContainer</item> + <item name="android:maxLines">1</item> + <item name="android:ellipsize">marquee</item> + <item name="android:minHeight">@dimen/settingslib_expressive_space_medium3</item> + <item name="android:paddingHorizontal">@dimen/settingslib_expressive_space_small1</item> + <item name="android:paddingVertical">@dimen/settingslib_expressive_space_extrasmall5</item> + </style> + + <style name="SettingsSpinnerTitleBar.Expressive.Large"> + <item name="android:textAppearance">@style/TextAppearance.SettingsLib.TitleMedium</item> + <item name="android:minHeight">@dimen/settingslib_expressive_space_medium5</item> + <item name="android:paddingHorizontal">@dimen/settingslib_expressive_space_small4</item> + <item name="android:paddingVertical">@dimen/settingslib_expressive_space_small1</item> + </style> + + <style name="SettingsSpinnerTitleBar.Expressive.Outlined"> + <item name="android:textColor">@color/settingslib_materialColorPrimary</item> + </style> + + <style name="SettingsSpinnerTitleBar.Expressive.Large.Outlined"> + <item name="android:textColor">@color/settingslib_materialColorPrimary</item> + </style> + + <style name="SettingsSpinnerDropdown.Expressive"> + <item name="android:background">@drawable/settings_expressive_spinner_dropdown_background</item> + <item name="android:minHeight">@dimen/spinner_dropdown_height</item> + <item name="android:paddingStart">@dimen/settingslib_expressive_space_extrasmall4</item> + <item name="android:paddingEnd">@dimen/settingslib_expressive_space_extrasmall4</item> + <item name="android:paddingTop">@dimen/settingslib_expressive_space_extrasmall7</item> + <item name="android:paddingBottom">@dimen/settingslib_expressive_space_extrasmall7</item> + </style> + + <style name="SettingsSpinnerDropdownText"> + <item name="android:textAppearance">@style/TextAppearance.SettingsLib.LabelLarge</item> + <item name="android:textColor">@color/settingslib_spinner_dropdown_color</item> + <item name="android:maxLines">1</item> + <item name="android:ellipsize">marquee</item> + </style> +</resources> diff --git a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerAdapter.java b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerAdapter.java index f33cacd36c6d..2f9f7038f6f7 100644 --- a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerAdapter.java +++ b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerAdapter.java @@ -22,7 +22,13 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.android.settingslib.widget.SettingsSpinnerPreference.Style; import com.android.settingslib.widget.spinner.R; + /** * An ArrayAdapter which was used by Spinner with settings style. * @param <T> the data type to be loaded. @@ -30,8 +36,13 @@ import com.android.settingslib.widget.spinner.R; public class SettingsSpinnerAdapter<T> extends ArrayAdapter<T> { private static final int DEFAULT_RESOURCE = R.layout.settings_spinner_view; - private static final int DFAULT_DROPDOWN_RESOURCE = R.layout.settings_spinner_dropdown_view; + private static final int DEFAULT_DROPDOWN_RESOURCE = R.layout.settings_spinner_dropdown_view; + private static final int DEFAULT_EXPRESSIVE_RESOURCE = + R.layout.settings_expressvie_spinner_view; + private static final int DEFAULT_EXPRESSIVE_DROPDOWN_RESOURCE = + R.layout.settings_expressvie_spinner_dropdown_view; private final LayoutInflater mDefaultInflater; + private int mSelectedPosition = -1; /** * Constructs a new SettingsSpinnerAdapter with the given context. @@ -41,17 +52,74 @@ public class SettingsSpinnerAdapter<T> extends ArrayAdapter<T> { * access the current theme, resources, etc. */ public SettingsSpinnerAdapter(Context context) { - super(context, getDefaultResource()); + super(context, getDefaultResource(context, Style.NORMAL)); + + setDropDownViewResource(getDropdownResource(context)); + mDefaultInflater = LayoutInflater.from(context); + } + + @Override + public View getDropDownView( + int position, @Nullable View convertView, @NonNull ViewGroup parent) { + View view; + if (convertView == null) { + view = + mDefaultInflater.inflate( + getDropdownResource(getContext()), parent, false /* attachToRoot */); + } else { + view = convertView; + } + TextView textView = view.findViewById(android.R.id.text1); + ImageView iconView = view.findViewById(android.R.id.icon); + iconView.setVisibility((position == mSelectedPosition) ? View.VISIBLE : View.GONE); + String item = (String) getItem(position); + textView.setText(item); + return view; + } - setDropDownViewResource(getDropdownResource()); + public void setSelectedPosition(int pos) { + mSelectedPosition = pos; + } + + public SettingsSpinnerAdapter(Context context, SettingsSpinnerPreference.Style style) { + super(context, getDefaultResource(context, style)); + + setDropDownViewResource(getDropdownResource(context)); mDefaultInflater = LayoutInflater.from(context); } + private static int getDefaultResourceWithStyle(Style style) { + switch (style) { + case NORMAL -> { + return DEFAULT_EXPRESSIVE_RESOURCE; + } + case LARGE -> { + return R.layout.settings_expressive_spinner_view_large; + } + case FULL_WIDTH -> { + return R.layout.settings_expressive_spinner_view_full; + } + case OUTLINED -> { + return R.layout.settings_expressvie_spinner_view_outlined; + } + case LARGE_OUTLINED -> { + return R.layout.settings_expressive_spinner_view_large_outlined; + } + case FULL_OUTLINED -> { + return R.layout.settings_expressive_spinner_view_full_outlined; + } + default -> { + return DEFAULT_RESOURCE; + } + } + } + /** * In overridded {@link #getView(int, View, ViewGroup)}, use this method to get default view. */ public View getDefaultView(int position, View convertView, ViewGroup parent) { - return mDefaultInflater.inflate(getDefaultResource(), parent, false /* attachToRoot */); + return mDefaultInflater.inflate( + getDefaultResource(getContext(), Style.NORMAL), parent, false /* attachToRoot */); } /** @@ -59,15 +127,21 @@ public class SettingsSpinnerAdapter<T> extends ArrayAdapter<T> { * drop down view. */ public View getDefaultDropDownView(int position, View convertView, ViewGroup parent) { - return mDefaultInflater.inflate(getDropdownResource(), parent, false /* attachToRoot */); + return mDefaultInflater.inflate( + getDropdownResource(getContext()), parent, false /* attachToRoot */); } - private static int getDefaultResource() { + private static int getDefaultResource(Context context, Style style) { + int resId = SettingsThemeHelper.isExpressiveTheme(context) + ? getDefaultResourceWithStyle(style) : DEFAULT_DROPDOWN_RESOURCE; return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) - ? DEFAULT_RESOURCE : android.R.layout.simple_spinner_dropdown_item; + ? resId : android.R.layout.simple_spinner_dropdown_item; } - private static int getDropdownResource() { + + private static int getDropdownResource(Context context) { + int resId = SettingsThemeHelper.isExpressiveTheme(context) + ? DEFAULT_EXPRESSIVE_DROPDOWN_RESOURCE : DEFAULT_DROPDOWN_RESOURCE; return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) - ? DFAULT_DROPDOWN_RESOURCE : android.R.layout.simple_spinner_dropdown_item; + ? resId : android.R.layout.simple_spinner_dropdown_item; } } diff --git a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java index 1170f1e7c695..b357369155b6 100644 --- a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java +++ b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java @@ -17,12 +17,15 @@ package com.android.settingslib.widget; import android.content.Context; +import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.widget.AdapterView; import android.widget.Spinner; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.preference.Preference; import androidx.preference.Preference.OnPreferenceClickListener; import androidx.preference.PreferenceViewHolder; @@ -44,29 +47,28 @@ public class SettingsSpinnerPreference extends Preference /** * Perform inflation from XML and apply a class-specific base style. * - * @param context The {@link Context} this is associated with, through which it can - * access the current theme, resources, {@link SharedPreferences}, etc. - * @param attrs The attributes of the XML tag that is inflating the preference + * @param context The {@link Context} this is associated with, through which it can access the + * current theme, resources, {@link SharedPreferences}, etc. + * @param attrs The attributes of the XML tag that is inflating the preference * @param defStyle An attribute in the current theme that contains a reference to a style - * resource that supplies default values for the view. Can be 0 to not - * look for defaults. + * resource that supplies default values for the view. Can be 0 to not look for defaults. */ public SettingsSpinnerPreference(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - setLayoutResource(R.layout.settings_spinner_preference); + initAttributes(context, attrs, defStyle); setOnPreferenceClickListener(this); } /** * Perform inflation from XML and apply a class-specific base style. * - * @param context The {@link Context} this is associated with, through which it can - * access the current theme, resources, {@link SharedPreferences}, etc. - * @param attrs The attributes of the XML tag that is inflating the preference + * @param context The {@link Context} this is associated with, through which it can access the + * current theme, resources, {@link SharedPreferences}, etc. + * @param attrs The attributes of the XML tag that is inflating the preference */ - public SettingsSpinnerPreference(Context context, AttributeSet attrs) { + public SettingsSpinnerPreference(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); - setLayoutResource(R.layout.settings_spinner_preference); + initAttributes(context, attrs, 0); setOnPreferenceClickListener(this); } @@ -75,8 +77,36 @@ public class SettingsSpinnerPreference extends Preference * * @param context The Context this is associated with. */ - public SettingsSpinnerPreference(Context context) { + public SettingsSpinnerPreference(@NonNull Context context) { this(context, null); + initAttributes(context, null, 0); + } + + public enum Style { + NORMAL, + LARGE, + FULL_WIDTH, + OUTLINED, + LARGE_OUTLINED, + FULL_OUTLINED, + } + + private void initAttributes( + @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + int layoutRes = R.layout.settings_spinner_preference; + try (TypedArray a = + context.obtainStyledAttributes( + attrs, R.styleable.SettingsSpinnerPreference, defStyleAttr, 0)) { + int style = a.getInteger(R.styleable.SettingsSpinnerPreference_style, 0); + switch (style) { + case 2 -> layoutRes = R.layout.settings_expressive_spinner_preference_full; + case 3 -> layoutRes = R.layout.settings_expressive_spinner_preference_outlined; + case 4 -> layoutRes = R.layout.settings_expressive_spinner_preference_outlined; + case 5 -> layoutRes = R.layout.settings_expressive_spinner_preference_full_outlined; + default -> layoutRes = R.layout.settings_spinner_preference; + } + } + setLayoutResource(layoutRes); } @Override @@ -115,6 +145,12 @@ public class SettingsSpinnerPreference extends Preference public void onBindViewHolder(PreferenceViewHolder holder) { super.onBindViewHolder(holder); final Spinner spinner = (Spinner) holder.findViewById(R.id.spinner); + if (spinner == null) { + return; + } + if (mAdapter != null) { + mAdapter.setSelectedPosition(mPosition); + } spinner.setAdapter(mAdapter); spinner.setSelection(mPosition); spinner.setOnItemSelectedListener(mOnSelectedListener); @@ -140,20 +176,22 @@ public class SettingsSpinnerPreference extends Preference private final AdapterView.OnItemSelectedListener mOnSelectedListener = new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { - if (mPosition == position) return; - mPosition = position; - if (mListener != null) { - mListener.onItemSelected(parent, view, position, id); - } - } + @Override + public void onItemSelected( + AdapterView<?> parent, View view, int position, long id) { + if (mPosition == position) return; + mPosition = position; + mAdapter.setSelectedPosition(mPosition); + if (mListener != null) { + mListener.onItemSelected(parent, view, position, id); + } + } - @Override - public void onNothingSelected(AdapterView<?> parent) { - if (mListener != null) { - mListener.onNothingSelected(parent); - } - } - }; + @Override + public void onNothingSelected(AdapterView<?> parent) { + if (mListener != null) { + mListener.onNothingSelected(parent); + } + } + }; } diff --git a/packages/SettingsLib/SettingsTheme/res/color-v31/settingslib_neutral_variant55.xml b/packages/SettingsLib/SettingsTheme/res/color-v31/settingslib_neutral_variant55.xml new file mode 100644 index 000000000000..53ffa234f432 --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/color-v31/settingslib_neutral_variant55.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="@android:color/system_neutral2_500" android:lStar="55"/> +</selector>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_expressive_spinner_background.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_expressive_spinner_background.xml new file mode 100644 index 000000000000..139418b38e03 --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_expressive_spinner_background.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<ripple + xmlns:android="http://schemas.android.com/apk/res/android" + android:color="?android:attr/colorControlHighlight"> + + <item android:id="@android:id/background"> + <layer-list + android:paddingMode="stack" + android:paddingStart="0dp" + android:paddingEnd="@dimen/settingslib_expressive_space_small4"> + <item> + <shape> + <corners android:radius="@dimen/settingslib_expressive_radius_full"/> + <solid android:color="@color/settingslib_materialColorSecondaryContainer"/> + <size android:height="@dimen/settingslib_expressive_space_medium3"/> + </shape> + </item> + + <item + android:gravity="center|end" + android:width="@dimen/settingslib_expressive_space_small4" + android:height="@dimen/settingslib_expressive_space_small4" + android:end="@dimen/settingslib_expressive_space_small1"> + <vector + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="@color/settingslib_materialColorOnSecondaryContainer"> + <path + android:fillColor="@android:color/white" + android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6 -1.41,-1.41z"/> + </vector> + </item> + </layer-list> + </item> +</ripple> diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_expressive_spinner_background_outlined.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_expressive_spinner_background_outlined.xml new file mode 100644 index 000000000000..f32e13e7f83a --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_expressive_spinner_background_outlined.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<ripple + xmlns:android="http://schemas.android.com/apk/res/android" + android:color="?android:attr/colorControlHighlight"> + + <item android:id="@android:id/background"> + <layer-list + android:paddingMode="stack" + android:paddingStart="0dp" + android:paddingEnd="@dimen/settingslib_expressive_space_small4"> + <item> + <shape> + <corners android:radius="@dimen/settingslib_expressive_radius_full"/> + <stroke + android:color="@color/settingslib_materialColorOutlineVariant" + android:width="1dp"/> + <size android:height="@dimen/settingslib_expressive_space_medium3"/> + </shape> + </item> + + <item + android:gravity="center|end" + android:width="@dimen/settingslib_expressive_space_small4" + android:height="@dimen/settingslib_expressive_space_small4" + android:end="@dimen/settingslib_expressive_space_small1"> + <vector + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="@color/settingslib_materialColorPrimary"> + <path + android:fillColor="@android:color/white" + android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6 -1.41,-1.41z"/> + </vector> + </item> + </layer-list> + </item> +</ripple> diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_expressive_spinner_dropdown_background.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_expressive_spinner_dropdown_background.xml new file mode 100644 index 000000000000..ac38c3e9223b --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_expressive_spinner_dropdown_background.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<ripple + xmlns:android="http://schemas.android.com/apk/res/android" + android:color="?android:attr/colorControlHighlight"> + + <item android:id="@android:id/background"> + <layer-list + android:paddingMode="stack" + android:paddingStart="@dimen/settingslib_expressive_space_extrasmall4" + android:paddingEnd="@dimen/settingslib_expressive_space_extrasmall4" + android:paddingTop="@dimen/settingslib_expressive_space_extrasmall4" + android:paddingBottom="@dimen/settingslib_expressive_space_extrasmall4"> + + <item> + <shape> + <corners android:radius="@dimen/settingslib_expressive_radius_large3"/> + <solid android:color="@color/settingslib_materialColorSurfaceContainerLow"/> + </shape> + </item> + </layer-list> + </item> +</ripple> diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_round_background_bottom_selected.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_round_background_bottom_selected.xml index 543b237373fb..e1dde1a8a184 100644 --- a/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_round_background_bottom_selected.xml +++ b/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_round_background_bottom_selected.xml @@ -26,10 +26,7 @@ <solid android:color="@color/settingslib_materialColorSurfaceContainer" /> <corners - android:topLeftRadius="4dp" - android:bottomLeftRadius="@dimen/settingslib_preference_corner_radius" - android:topRightRadius="4dp" - android:bottomRightRadius="@dimen/settingslib_preference_corner_radius" /> + android:radius="@dimen/settingslib_expressive_radius_extralarge1" /> <padding android:bottom="16dp"/> </shape> diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_round_background_center_selected.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_round_background_center_selected.xml index 6d2cd1a51620..bf1b9149dd58 100644 --- a/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_round_background_center_selected.xml +++ b/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_round_background_center_selected.xml @@ -25,7 +25,7 @@ <solid android:color="@color/settingslib_materialColorSurfaceContainer" /> <corners - android:radius="4dp" /> + android:radius="@dimen/settingslib_expressive_radius_extralarge1" /> </shape> </item> </ripple>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_round_background_selected.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_round_background_selected.xml index bcdbf1d19545..e1dde1a8a184 100644 --- a/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_round_background_selected.xml +++ b/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_round_background_selected.xml @@ -26,7 +26,7 @@ <solid android:color="@color/settingslib_materialColorSurfaceContainer" /> <corners - android:radius="@dimen/settingslib_preference_corner_radius" /> + android:radius="@dimen/settingslib_expressive_radius_extralarge1" /> <padding android:bottom="16dp"/> </shape> diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_round_background_top_selected.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_round_background_top_selected.xml index d4b658c384e6..bf1b9149dd58 100644 --- a/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_round_background_top_selected.xml +++ b/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_round_background_top_selected.xml @@ -25,10 +25,7 @@ <solid android:color="@color/settingslib_materialColorSurfaceContainer" /> <corners - android:topLeftRadius="@dimen/settingslib_preference_corner_radius" - android:bottomLeftRadius="4dp" - android:topRightRadius="@dimen/settingslib_preference_corner_radius" - android:bottomRightRadius="4dp" /> + android:radius="@dimen/settingslib_expressive_radius_extralarge1" /> </shape> </item> </ripple>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/drawable/settingslib_expressive_button_background_filled.xml b/packages/SettingsLib/SettingsTheme/res/drawable/settingslib_expressive_button_background_filled.xml new file mode 100644 index 000000000000..68cc058c5974 --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/drawable/settingslib_expressive_button_background_filled.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="@color/settingslib_materialColorPrimary"/> + <corners android:radius="@dimen/settingslib_expressive_radius_full"/> +</shape>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/drawable/settingslib_expressive_button_background_outline.xml b/packages/SettingsLib/SettingsTheme/res/drawable/settingslib_expressive_button_background_outline.xml new file mode 100644 index 000000000000..213289d5158b --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/drawable/settingslib_expressive_button_background_outline.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <stroke android:width="1dp" + android:color="@color/settingslib_materialColorOutlineVariant"/> + <corners android:radius="@dimen/settingslib_expressive_radius_full"/> +</shape>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v36/settingslib_expressive_preference_category.xml b/packages/SettingsLib/SettingsTheme/res/layout-v36/settingslib_expressive_preference_category.xml new file mode 100644 index 000000000000..44b8e7c96a88 --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/layout-v36/settingslib_expressive_preference_category.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingLeft="?android:attr/listPreferredItemPaddingLeft" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingRight="?android:attr/listPreferredItemPaddingRight" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:background="?android:attr/selectableItemBackground" + android:baselineAligned="false" + android:layout_marginTop="@dimen/settingslib_expressive_space_small1" + android:gravity="center_vertical" + android:filterTouchesWhenObscured="false"> + + <TextView + android:id="@android:id/title" + android:paddingStart="@dimen/settingslib_expressive_space_extrasmall4" + android:paddingTop="@dimen/settingslib_expressive_space_extrasmall4" + android:paddingBottom="@dimen/settingslib_expressive_space_extrasmall4" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="start" + android:textAlignment="viewStart" + style="@style/PreferenceCategoryTitleTextStyle"/> +</LinearLayout>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/values-night-v36/styles.xml b/packages/SettingsLib/SettingsTheme/res/values-night-v36/styles.xml new file mode 100644 index 000000000000..ca99d449b6b3 --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/values-night-v36/styles.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<resources> + <style name="Seekbar.SettingsLib" parent="@android:style/Widget.Material.SeekBar"> + <item name="android:thumbTint">@android:color/system_accent1_100</item> + <item name="android:progressTint">@android:color/system_accent1_100</item> + <item name="android:progressBackgroundTint">@android:color/system_neutral2_500</item> + <item name="android:progressBackgroundTintMode">src_over</item> + </style> +</resources>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/values-v36/dimens.xml b/packages/SettingsLib/SettingsTheme/res/values-v36/dimens.xml index 193ae618e803..d783956ee240 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-v36/dimens.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-v36/dimens.xml @@ -17,5 +17,4 @@ <resources> <dimen name="settingslib_preference_corner_radius">20dp</dimen> - <dimen name="settingslib_preference_corner_radius_selected">28dp</dimen> </resources>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/values-v36/styles.xml b/packages/SettingsLib/SettingsTheme/res/values-v36/styles.xml new file mode 100644 index 000000000000..a31983a04753 --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/values-v36/styles.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<resources> + <style name="Seekbar.SettingsLib" parent="@android:style/Widget.Material.SeekBar"> + <item name="android:thumbTint">@android:color/system_accent1_800</item> + <item name="android:progressTint">@android:color/system_accent1_800</item> + <item name="android:progressBackgroundTint">@color/settingslib_neutral_variant55</item> + <item name="android:progressBackgroundTintMode">src_over</item> + </style> +</resources>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/values-v36/styles_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v36/styles_expressive.xml index 3af88c48e8ca..9cdbce4a4c78 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-v36/styles_expressive.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-v36/styles_expressive.xml @@ -31,6 +31,17 @@ <item name="trackTint">@color/settingslib_expressive_color_main_switch_track</item> </style> + <style name="SettingslibSpinnerStyle.Expressive" + parent="android:style/Widget.Material.Spinner"> + <item name="android:background">@drawable/settingslib_expressive_spinner_background</item> + <item name="android:popupBackground">@drawable/settingslib_expressive_spinner_dropdown_background</item> + <item name="android:dropDownVerticalOffset">@dimen/settingslib_expressive_space_large3</item> + </style> + + <style name="SettingslibSpinnerStyle.Expressive.Outlined"> + <item name="android:background">@drawable/settingslib_expressive_spinner_background_outlined</item> + </style> + <style name="EntityHeader"> <item name="android:paddingTop">@dimen/settingslib_expressive_space_small4</item> <item name="android:paddingBottom">@dimen/settingslib_expressive_space_small1</item> @@ -90,4 +101,39 @@ parent="@style/TextAppearance.SettingsLib.BodyMedium"> <item name="android:textColor">@color/settingslib_text_color_secondary</item> </style> -</resources>
\ No newline at end of file + + <style name="Widget.SettingsLib.DialogWindowTitle" parent=""> + <item name="android:scrollHorizontally">false</item> + <item name="android:ellipsize">end</item> + <item name="android:textAppearance">@style/TextAppearance.SettingsLib.HeadlineSmall</item> + </style> + + <style name="Widget.SettingsLib.ButtonBar" parent="@style/Widget.AppCompat.ButtonBar.AlertDialog"> + <item name="android:paddingTop">@dimen/settingslib_expressive_space_extrasmall2</item> + <item name="android:paddingBottom">@dimen/settingslib_expressive_space_small4</item> + </style> + + <style name="Widget.SettingsLib.DialogButton" parent="@style/Widget.AppCompat.Button"> + <item name="android:layout_height">wrap_content</item> + <item name="android:minHeight">0dp</item> + <item name="android:minWidth">0dp</item> + <item name="android:paddingVertical">@dimen/settingslib_expressive_space_extrasmall5</item> + <item name="android:paddingHorizontal">@dimen/settingslib_expressive_space_small1</item> + <item name="android:stateListAnimator">@null</item> + <item name="textAllCaps">false</item> + <item name="android:textAppearance">@style/TextAppearance.SettingsLib.LabelLarge</item> + <item name="android:textColor">@color/settingslib_materialColorPrimary</item> + <item name="android:background">@android:color/transparent</item> + </style> + + <style name="Widget.SettingsLib.DialogButton.Filled"> + <item name="android:layout_marginEnd">@dimen/settingslib_expressive_space_extrasmall6</item> + <item name="android:background">@drawable/settingslib_expressive_button_background_filled</item> + <item name="android:textColor">@color/settingslib_materialColorOnPrimary</item> + </style> + + <style name="Widget.SettingsLib.DialogButton.Outline"> + <item name="android:layout_marginEnd">@dimen/settingslib_expressive_space_extrasmall4</item> + <item name="android:background">@drawable/settingslib_expressive_button_background_outline</item> + </style> +</resources> diff --git a/packages/SettingsLib/SettingsTheme/res/values-v36/styles_preference_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v36/styles_preference_expressive.xml index cec8e45e2bfb..ec6fe887d31e 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-v36/styles_preference_expressive.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-v36/styles_preference_expressive.xml @@ -40,6 +40,7 @@ </style> <style name="SettingsLibPreference.Category.Expressive"> + <item name="layout">@layout/settingslib_expressive_preference_category</item> </style> <style name="SettingsLibPreference.CheckBoxPreference.Expressive"> diff --git a/packages/SettingsLib/SettingsTheme/res/values-v36/themes.xml b/packages/SettingsLib/SettingsTheme/res/values-v36/themes.xml index 1c45ff6ca6cf..54bd069f2fc3 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-v36/themes.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-v36/themes.xml @@ -22,6 +22,7 @@ <item name="android:textColorPrimary">@color/settingslib_materialColorOnSurface</item> <item name="android:textColorSecondary">@color/settingslib_text_color_secondary</item> <item name="android:textColorTertiary">@color/settingslib_materialColorOutline</item> + <item name="android:seekBarStyle">@style/Seekbar.SettingsLib</item> <!-- Set up edge-to-edge configuration for top app bar --> <item name="android:clipToPadding">false</item> <item name="android:clipChildren">false</item> diff --git a/packages/SettingsLib/SettingsTheme/res/values-v36/themes_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v36/themes_expressive.xml index 14f214a96435..ffbc65cc622b 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-v36/themes_expressive.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-v36/themes_expressive.xml @@ -32,9 +32,9 @@ <item name="preferenceTheme">@style/PreferenceTheme.SettingsLib.Expressive</item> <!-- Set up Spinner style --> - <!--item name="android:spinnerStyle"></item> - <item name="android:spinnerItemStyle"></item> - <item name="android:spinnerDropDownItemStyle"></item--> + <item name="android:spinnerStyle">@style/SettingslibSpinnerStyle.Expressive</item> + <!--<item name="android:spinnerItemStyle"></item> + <item name="android:spinnerDropDownItemStyle"></item>--> <!-- Set up edge-to-edge configuration for top app bar --> <item name="android:clipToPadding">false</item> @@ -53,6 +53,17 @@ <item name="colorControlNormal">?android:attr/colorControlNormal</item> <!-- For AndroidX AlertDialog --> - <!--item name="alertDialogTheme">@style/Theme.AlertDialog.SettingsLib</item--> + <item name="alertDialogTheme">@style/Theme.AlertDialog.SettingsLib.Expressive</item> </style> -</resources>
\ No newline at end of file + + <style name="Theme.AlertDialog.SettingsLib.Expressive"> + <item name="colorAccent">@color/settingslib_materialColorPrimary</item> + <item name="android:colorBackground">@color/settingslib_materialColorSurfaceContainerHigh</item> + <item name="android:windowTitleStyle">@style/Widget.SettingsLib.DialogWindowTitle</item> + <item name="dialogPreferredPadding">@dimen/settingslib_expressive_space_small4</item> + <item name="buttonBarStyle">@style/Widget.SettingsLib.ButtonBar</item> + <item name="buttonBarPositiveButtonStyle">@style/Widget.SettingsLib.DialogButton.Filled</item> + <item name="buttonBarNegativeButtonStyle">@style/Widget.SettingsLib.DialogButton.Outline</item> + <item name="buttonBarNeutralButtonStyle">@style/Widget.SettingsLib.DialogButton</item> + </style> +</resources> diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlterDialogContent.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlterDialogContent.kt index 9f2210d852a9..058fe53f7201 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlterDialogContent.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlterDialogContent.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.layout.Placeable import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEachIndexed @@ -226,7 +227,12 @@ internal fun AlertDialogFlowRow( val childrenMainAxisSizes = IntArray(placeables.size) { j -> placeables[j].width + - if (j < placeables.lastIndex) mainAxisSpacing.roundToPx() else 0 + if ((layoutDirection == LayoutDirection.Ltr && j < placeables.lastIndex) + || (layoutDirection == LayoutDirection.Rtl && j > 0)) { + mainAxisSpacing.roundToPx() + } else { + 0 + } } val arrangement = Arrangement.End val mainAxisPositions = IntArray(childrenMainAxisSizes.size) { 0 } diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt index 5580d2e3211b..6dd5e371d8a6 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt @@ -37,6 +37,7 @@ import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel import com.android.settingslib.spa.widget.ui.AnnotatedText +import com.android.settingslib.spa.widget.ui.Category import com.android.settingslib.spaprivileged.model.app.AppRecord import com.android.settingslib.spaprivileged.model.app.IPackageManagers import com.android.settingslib.spaprivileged.model.app.PackageManagers @@ -153,13 +154,15 @@ internal fun <T : AppRecord> TogglePermissionAppListModel<T>.TogglePermissionApp override val changeable = { isChangeable } override val onCheckedChange: (Boolean) -> Unit = { setAllowed(record, it) } } - RestrictedSwitchPreference( - model = switchModel, - restrictions = getRestrictions(userId, packageName, isAllowed()), - ifBlockedByAdminOverrideCheckedValueTo = switchifBlockedByAdminOverrideCheckedValueTo, - restrictionsProviderFactory = restrictionsProviderFactory, - ) - InfoPageAdditionalContent(record, isAllowed) + Category { + RestrictedSwitchPreference( + model = switchModel, + restrictions = getRestrictions(userId, packageName, isAllowed()), + ifBlockedByAdminOverrideCheckedValueTo = + switchifBlockedByAdminOverrideCheckedValueTo, + restrictionsProviderFactory = restrictionsProviderFactory, + ) + } } } diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt index 771eb85ee21a..a3e4aa0420ff 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt @@ -91,9 +91,6 @@ interface TogglePermissionAppListModel<T : AppRecord> { * Sets whether the permission is allowed for the given app. */ fun setAllowed(record: T, newAllowed: Boolean) - - @Composable - fun InfoPageAdditionalContent(record: T, isAllowed: () -> Boolean?) {} } /** diff --git a/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt b/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt index e6c6638f7de4..c62aed1da352 100644 --- a/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt +++ b/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt @@ -49,6 +49,7 @@ class StatusBannerPreference @JvmOverloads constructor( var iconLevel: BannerStatus = BannerStatus.GENERIC set(value) { field = value + updateIconTint(value) notifyChanged() } var buttonLevel: BannerStatus = BannerStatus.GENERIC @@ -81,7 +82,7 @@ class StatusBannerPreference @JvmOverloads constructor( if (icon == null) { icon = getIconDrawable(iconLevel) } else { - icon!!.setTintList(ColorStateList.valueOf(getBackgroundColor(iconLevel))) + updateIconTint(iconLevel) } buttonLevel = getInteger(R.styleable.StatusBanner_buttonLevel, 0).toBannerStatus() buttonText = getString(R.styleable.StatusBanner_buttonText) ?: "" @@ -252,4 +253,12 @@ class StatusBannerPreference @JvmOverloads constructor( ) } } -}
\ No newline at end of file + + /** + * Sets the icon's tint color based on the icon level. If an icon is not defined, this is a + * no-op. + */ + private fun updateIconTint(iconLevel: BannerStatus) { + icon?.setTintList(ColorStateList.valueOf(getBackgroundColor(iconLevel))) + } +} diff --git a/packages/SettingsLib/res/values-af/strings.xml b/packages/SettingsLib/res/values-af/strings.xml index f1859f2fbd6a..54074ec63456 100644 --- a/packages/SettingsLib/res/values-af/strings.xml +++ b/packages/SettingsLib/res/values-af/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Ingeboude luidspreker"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"TV-oudio"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Kan nie koppel nie. Skakel toestel af en weer aan"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Kan nie koppel nie"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Bedrade oudiotoestel"</string> <string name="help_label" msgid="3528360748637781274">"Hulp en terugvoer"</string> <string name="storage_category" msgid="2287342585424631813">"Berging"</string> diff --git a/packages/SettingsLib/res/values-am/strings.xml b/packages/SettingsLib/res/values-am/strings.xml index 77009d747bad..1fab31b41dec 100644 --- a/packages/SettingsLib/res/values-am/strings.xml +++ b/packages/SettingsLib/res/values-am/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"አብሮ የተሰራ ድምፅ ማውጫ"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"የTV ኦዲዮ"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"መገናኘት ላይ ችግር። መሳሪያውን ያጥፉት እና እንደገና ያብሩት"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"ማገናኘት አልተቻለም"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"ባለገመድ የኦዲዮ መሣሪያ"</string> <string name="help_label" msgid="3528360748637781274">"እገዛ እና ግብረመልስ"</string> <string name="storage_category" msgid="2287342585424631813">"ማከማቻ"</string> diff --git a/packages/SettingsLib/res/values-ar/strings.xml b/packages/SettingsLib/res/values-ar/strings.xml index a522e07baaec..6cb81540a52b 100644 --- a/packages/SettingsLib/res/values-ar/strings.xml +++ b/packages/SettingsLib/res/values-ar/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"مكبِّر الصوت المُدمَج"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"صوت التلفزيون"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"حدثت مشكلة أثناء الاتصال. يُرجى إيقاف الجهاز ثم إعادة تشغيله."</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"يتعذّر الاتصال"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"جهاز سماعي سلكي"</string> <string name="help_label" msgid="3528360748637781274">"المساعدة والملاحظات"</string> <string name="storage_category" msgid="2287342585424631813">"مساحة التخزين"</string> diff --git a/packages/SettingsLib/res/values-as/strings.xml b/packages/SettingsLib/res/values-as/strings.xml index 0ed118be8ae3..b39593eae46c 100644 --- a/packages/SettingsLib/res/values-as/strings.xml +++ b/packages/SettingsLib/res/values-as/strings.xml @@ -77,7 +77,7 @@ <string name="osu_sign_up_failed" msgid="5605453599586001793">"ছাইন আপ সম্পূৰ্ণ কৰিব পৰা নগ’ল। আকৌ চেষ্টা কৰিবলৈ টিপক।"</string> <string name="osu_sign_up_complete" msgid="7640183358878916847">"ছাইন আপ সম্পূৰ্ণ হৈছে সংযোগ কৰি থকা হৈছে…"</string> <string name="speed_label_slow" msgid="6069917670665664161">"লেহেমীয়া"</string> - <string name="speed_label_okay" msgid="1253594383880810424">"ঠিক"</string> + <string name="speed_label_okay" msgid="1253594383880810424">"ঠিক আছে"</string> <string name="speed_label_fast" msgid="2677719134596044051">"দ্ৰুত"</string> <string name="speed_label_very_fast" msgid="8215718029533182439">"অতি দ্ৰুত"</string> <string name="wifi_passpoint_expired" msgid="6540867261754427561">"ম্যাদ উকলিছে"</string> @@ -578,7 +578,7 @@ <string name="next" msgid="2699398661093607009">"পৰৱৰ্তী"</string> <string name="back" msgid="5554327870352703710">"উভতি যাওক"</string> <string name="save" msgid="3745809743277153149">"ছেভ কৰক"</string> - <string name="okay" msgid="949938843324579502">"ঠিক"</string> + <string name="okay" msgid="949938843324579502">"ঠিক আছে"</string> <string name="done" msgid="381184316122520313">"হ’ল"</string> <string name="alarms_and_reminders_label" msgid="6918395649731424294">"এলাৰ্ম আৰু ৰিমাইণ্ডাৰ"</string> <string name="alarms_and_reminders_switch_title" msgid="4939393911531826222">"এলাৰ্ম আৰু ৰিমাইণ্ডাৰ ছেট কৰাৰ অনুমতি দিয়ক"</string> @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"বিল্ট-ইন স্পীকাৰ"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"টিভিৰ অডিঅ’"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"সংযোগ হোৱাত সমস্যা হৈছে। ডিভাইচটো অফ কৰি পুনৰ অন কৰক"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"সংযোগ কৰিব নোৱাৰি"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"তাঁৰযুক্ত অডিঅ’ ডিভাইচ"</string> <string name="help_label" msgid="3528360748637781274">"সহায় আৰু মতামত"</string> <string name="storage_category" msgid="2287342585424631813">"ষ্ট’ৰেজ"</string> diff --git a/packages/SettingsLib/res/values-az/strings.xml b/packages/SettingsLib/res/values-az/strings.xml index 6ba40f7fcbac..de74a46afe53 100644 --- a/packages/SettingsLib/res/values-az/strings.xml +++ b/packages/SettingsLib/res/values-az/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Daxili dinamik"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"TV audiosu"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Qoşulmaqla bağlı problem. Cihazı deaktiv edin, sonra yenidən aktiv edin"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Qoşulmaq mümkün deyil"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Simli audio cihaz"</string> <string name="help_label" msgid="3528360748637781274">"Yardım və rəy"</string> <string name="storage_category" msgid="2287342585424631813">"Yaddaş"</string> diff --git a/packages/SettingsLib/res/values-b+sr+Latn/strings.xml b/packages/SettingsLib/res/values-b+sr+Latn/strings.xml index 0ebeb554c758..3db1617619bb 100644 --- a/packages/SettingsLib/res/values-b+sr+Latn/strings.xml +++ b/packages/SettingsLib/res/values-b+sr+Latn/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Ugrađeni zvučnik"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Zvuk TV-a"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Problem pri povezivanju. Isključite uređaj, pa ga ponovo uključite"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Povezivanje nije uspelo"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Žičani audio uređaj"</string> <string name="help_label" msgid="3528360748637781274">"Pomoć i povratne informacije"</string> <string name="storage_category" msgid="2287342585424631813">"Memorijski prostor"</string> diff --git a/packages/SettingsLib/res/values-be/strings.xml b/packages/SettingsLib/res/values-be/strings.xml index 8e1fd28ee242..74dd6669b50d 100644 --- a/packages/SettingsLib/res/values-be/strings.xml +++ b/packages/SettingsLib/res/values-be/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Убудаваны дынамік"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Аўдыя тэлевізара"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Праблема з падключэннем. Выключыце і зноў уключыце прыладу"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Не ўдаецца падключыцца"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Правадная аўдыяпрылада"</string> <string name="help_label" msgid="3528360748637781274">"Даведка і водгукі"</string> <string name="storage_category" msgid="2287342585424631813">"Сховішча"</string> diff --git a/packages/SettingsLib/res/values-bg/strings.xml b/packages/SettingsLib/res/values-bg/strings.xml index bf10d1d94a39..b0ff126d1c1f 100644 --- a/packages/SettingsLib/res/values-bg/strings.xml +++ b/packages/SettingsLib/res/values-bg/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Вграден високоговорител"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Аудио на телевизора"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"При свързването възникна проблем. Изключете устройството и го включете отново"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Няма връзка"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Аудиоустройство с кабел"</string> <string name="help_label" msgid="3528360748637781274">"Помощ и отзиви"</string> <string name="storage_category" msgid="2287342585424631813">"Хранилище"</string> diff --git a/packages/SettingsLib/res/values-bn/strings.xml b/packages/SettingsLib/res/values-bn/strings.xml index 5f37d44c3626..9afef6243643 100644 --- a/packages/SettingsLib/res/values-bn/strings.xml +++ b/packages/SettingsLib/res/values-bn/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"বিল্ট-ইন স্পিকার"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"টিভি অডিও"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"কানেক্ট করতে সমস্যা হচ্ছে। ডিভাইস বন্ধ করে আবার চালু করুন"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"কানেক্ট করা যাচ্ছে না"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"ওয়্যার অডিও ডিভাইস"</string> <string name="help_label" msgid="3528360748637781274">"সহায়তা ও মতামত"</string> <string name="storage_category" msgid="2287342585424631813">"স্টোরেজ"</string> diff --git a/packages/SettingsLib/res/values-bs/strings.xml b/packages/SettingsLib/res/values-bs/strings.xml index 8cc8199ce07e..22de5005947c 100644 --- a/packages/SettingsLib/res/values-bs/strings.xml +++ b/packages/SettingsLib/res/values-bs/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Ugrađeni zvučnik"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Zvuk TV-a"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Došlo je do problema prilikom povezivanja. Isključite, pa ponovo uključite uređaj"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Nije se moguće povezati"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Žičani audio uređaj"</string> <string name="help_label" msgid="3528360748637781274">"Pomoć i povratne informacije"</string> <string name="storage_category" msgid="2287342585424631813">"Pohrana"</string> diff --git a/packages/SettingsLib/res/values-ca/strings.xml b/packages/SettingsLib/res/values-ca/strings.xml index ee683e57b783..da50f09164a6 100644 --- a/packages/SettingsLib/res/values-ca/strings.xml +++ b/packages/SettingsLib/res/values-ca/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Altaveu integrat"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Àudio de la televisió"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Hi ha hagut un problema amb la connexió. Apaga el dispositiu i torna\'l a encendre."</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"No es pot establir la connexió"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Dispositiu d\'àudio amb cable"</string> <string name="help_label" msgid="3528360748637781274">"Ajuda i suggeriments"</string> <string name="storage_category" msgid="2287342585424631813">"Emmagatzematge"</string> diff --git a/packages/SettingsLib/res/values-cs/strings.xml b/packages/SettingsLib/res/values-cs/strings.xml index 58cdc19c5c73..9d61a5a10502 100644 --- a/packages/SettingsLib/res/values-cs/strings.xml +++ b/packages/SettingsLib/res/values-cs/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Vestavěný reproduktor"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Zvuk televize"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Problém s připojením. Vypněte zařízení a znovu jej zapněte"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Nelze se připojit"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Kabelové audiozařízení"</string> <string name="help_label" msgid="3528360748637781274">"Nápověda a zpětná vazba"</string> <string name="storage_category" msgid="2287342585424631813">"Úložiště"</string> diff --git a/packages/SettingsLib/res/values-da/strings.xml b/packages/SettingsLib/res/values-da/strings.xml index 257f673c93dd..91b6420d4183 100644 --- a/packages/SettingsLib/res/values-da/strings.xml +++ b/packages/SettingsLib/res/values-da/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Indbygget højttaler"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"TV-lyd"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Der kunne ikke oprettes forbindelse. Sluk og tænd enheden"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Der kan ikke oprettes forbindelse"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Lydenhed med ledning"</string> <string name="help_label" msgid="3528360748637781274">"Hjælp og feedback"</string> <string name="storage_category" msgid="2287342585424631813">"Lager"</string> diff --git a/packages/SettingsLib/res/values-de/strings.xml b/packages/SettingsLib/res/values-de/strings.xml index 8bfaa0e0aeb9..0c7ea025a44e 100644 --- a/packages/SettingsLib/res/values-de/strings.xml +++ b/packages/SettingsLib/res/values-de/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Integrierter Lautsprecher"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"TV‑Audio"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Verbindung kann nicht hergestellt werden. Schalte das Gerät aus und wieder ein."</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Verbindung nicht möglich"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Netzbetriebenes Audiogerät"</string> <string name="help_label" msgid="3528360748637781274">"Hilfe und Feedback"</string> <string name="storage_category" msgid="2287342585424631813">"Speicher"</string> diff --git a/packages/SettingsLib/res/values-el/strings.xml b/packages/SettingsLib/res/values-el/strings.xml index 83fb6ff6ed70..06620f30d7f1 100644 --- a/packages/SettingsLib/res/values-el/strings.xml +++ b/packages/SettingsLib/res/values-el/strings.xml @@ -257,14 +257,14 @@ <string name="vpn_settings_not_available" msgid="2894137119965668920">"Οι ρυθμίσεις VPN δεν είναι διαθέσιμες γι\' αυτόν το χρήστη"</string> <string name="tethering_settings_not_available" msgid="266821736434699780">"Οι ρυθμίσεις σύνδεσης μέσω κινητής συσκευής δεν είναι διαθέσιμες γι\' αυτόν το χρήστη"</string> <string name="apn_settings_not_available" msgid="1147111671403342300">"Οι ρυθμίσεις ονόματος σημείου πρόσβασης δεν είναι διαθέσιμες γι\' αυτόν το χρήστη"</string> - <string name="enable_adb" msgid="8072776357237289039">"Εντοπισμός σφαλμάτων USB"</string> + <string name="enable_adb" msgid="8072776357237289039">"Αποσφαλμάτωση USB"</string> <string name="enable_adb_summary" msgid="3711526030096574316">"Λειτουργία εντοπισμού σφαλμάτων όταν το USB είναι συνδεδεμένο"</string> - <string name="clear_adb_keys" msgid="3010148733140369917">"Ανάκληση εξ/σεων εντ/σμού σφ/των USB"</string> - <string name="enable_adb_wireless" msgid="6973226350963971018">"Ασύρματος εντοπισμός σφαλμάτων"</string> + <string name="clear_adb_keys" msgid="3010148733140369917">"Ανάκληση εξ/σεων αποσφαλμάτ. USB"</string> + <string name="enable_adb_wireless" msgid="6973226350963971018">"Ασύρματη αποσφαλμάτωση"</string> <string name="enable_adb_wireless_summary" msgid="7344391423657093011">"Λειτουργία εντοπισμού σφαλμάτων όταν το Wi‑Fi είναι συνδεδεμένο"</string> <string name="adb_wireless_error" msgid="721958772149779856">"Σφάλμα"</string> - <string name="adb_wireless_settings" msgid="2295017847215680229">"Ασύρματος εντοπισμός σφαλμάτων"</string> - <string name="adb_wireless_list_empty_off" msgid="1713707973837255490">"Για να δείτε και να χρησιμοποιήσετε τις διαθέσιμες συσκευές, ενεργοποιήστε τον ασύρματο εντοπισμό σφαλμάτων"</string> + <string name="adb_wireless_settings" msgid="2295017847215680229">"Ασύρματη αποσφαλμάτωση"</string> + <string name="adb_wireless_list_empty_off" msgid="1713707973837255490">"Για να δείτε και να χρησιμοποιήσετε τις διαθέσιμες συσκευές, ενεργοποιήστε την ασύρματη αποσφαλμάτωση"</string> <string name="adb_pair_method_qrcode_title" msgid="6982904096137468634">"Σύζευξη συσκευής με κωδικό QR"</string> <string name="adb_pair_method_qrcode_summary" msgid="7130694277228970888">"Σύζευξη νέων συσκευών με τη χρήση σαρωτή κωδικών QR"</string> <string name="adb_pair_method_code_title" msgid="1122590300445142904">"Σύζευξη συσκευής με κωδικό σύζευξης"</string> @@ -287,7 +287,7 @@ <string name="adb_wireless_qrcode_pairing_title" msgid="1906409667944674707">"Σάρωση κωδικού QR"</string> <string name="adb_wireless_qrcode_pairing_description" msgid="6014121407143607851">"Σύζευξη συσκευής μέσω Wi‑Fi με τη σάρωση ενός κωδικού QR"</string> <string name="adb_wireless_no_network_msg" msgid="2365795244718494658">"Συνδεθείτε σε ένα δίκτυο Wi-Fi"</string> - <string name="keywords_adb_wireless" msgid="6507505581882171240">"adb, εντοπισμός σφαλμάτων, προγραμματιστής"</string> + <string name="keywords_adb_wireless" msgid="6507505581882171240">"adb, αποσφαλμάτωση, προγραμματιστής"</string> <string name="bugreport_in_power" msgid="8664089072534638709">"Συντόμευση αναφοράς σφαλμάτων"</string> <string name="bugreport_in_power_summary" msgid="1885529649381831775">"Εμφάνιση κουμπιού στο μενού ενεργοποίησης για τη λήψη αναφοράς σφαλμάτων"</string> <string name="keep_screen_on" msgid="1187161672348797558">"Παραμονή σε λειτουργία"</string> @@ -353,11 +353,11 @@ <string name="debug_view_attributes" msgid="3539609843984208216">"Ενεργοποίηση του ελέγχου χαρακτηριστικών προβολής"</string> <string name="mobile_data_always_on_summary" msgid="1112156365594371019">"Τα δεδομένα κινητής τηλεφωνίας να διατηρούνται πάντα ενεργά, ακόμα και όταν είναι ενεργό το Wi-Fi (για γρήγορη εναλλαγή δικτύου)."</string> <string name="tethering_hardware_offload_summary" msgid="7801345335142803029">"Χρήση της σύνδεσης επιτάχυνσης υλικού εάν υπάρχει"</string> - <string name="adb_warning_title" msgid="7708653449506485728">"Να επιτρέπεται ο εντοπισμός σφαλμάτων USB;"</string> - <string name="adb_warning_message" msgid="8145270656419669221">"Ο εντοπισμός σφαλμάτων USB προορίζεται μόνο για σκοπούς προγραμματισμού. Χρησιμοποιήστε τον για αντιγραφή δεδομένων μεταξύ του υπολογιστή και της συσκευής σας, για την εγκατάσταση εφαρμογών στη συσκευή σας χωρίς προειδοποίηση και για την ανάγνωση δεδομένων καταγραφής."</string> - <string name="adbwifi_warning_title" msgid="727104571653031865">"Να επιτρέπεται ο ασύρματος εντοπισμός σφαλμάτων;"</string> - <string name="adbwifi_warning_message" msgid="8005936574322702388">"Ο ασύρματος εντοπισμός σφαλμάτων προορίζεται μόνο για σκοπούς προγραμματισμού. Χρησιμοποιήστε τον για αντιγραφή δεδομένων μεταξύ του υπολογιστή και της συσκευής σας, για την εγκατάσταση εφαρμογών στη συσκευή σας χωρίς ειδοποίηση και για την ανάγνωση δεδομένων καταγραφής."</string> - <string name="adb_keys_warning_message" msgid="2968555274488101220">"Ανάκληση πρόσβασης στον εντοπισμό σφαλμάτων USB από όλους τους υπολογιστές για τους οποίους είχατε εξουσιοδότηση στο παρελθόν;"</string> + <string name="adb_warning_title" msgid="7708653449506485728">"Να επιτρέπεται η αποσφαλμάτωση USB;"</string> + <string name="adb_warning_message" msgid="8145270656419669221">"Η αποσφαλμάτωση USB προορίζεται μόνο για σκοπούς προγραμματισμού. Χρησιμοποιήστε τον για αντιγραφή δεδομένων μεταξύ του υπολογιστή και της συσκευής σας, για την εγκατάσταση εφαρμογών στη συσκευή σας χωρίς προειδοποίηση και για την ανάγνωση δεδομένων καταγραφής."</string> + <string name="adbwifi_warning_title" msgid="727104571653031865">"Να επιτρέπεται η ασύρματη αποσφαλμάτωση;"</string> + <string name="adbwifi_warning_message" msgid="8005936574322702388">"Η ασύρματη αποσφαλμάτωση προορίζεται μόνο για σκοπούς προγραμματισμού. Χρησιμοποιήστε τον για αντιγραφή δεδομένων μεταξύ του υπολογιστή και της συσκευής σας, για την εγκατάσταση εφαρμογών στη συσκευή σας χωρίς ειδοποίηση και για την ανάγνωση δεδομένων καταγραφής."</string> + <string name="adb_keys_warning_message" msgid="2968555274488101220">"Ανάκληση πρόσβασης στην αποσφαλμάτωση USB από όλους τους υπολογιστές για τους οποίους είχατε εξουσιοδότηση στο παρελθόν;"</string> <string name="dev_settings_warning_title" msgid="8251234890169074553">"Να επιτρέπεται η χρήση των ρυθμίσεων ανάπτυξης;"</string> <string name="dev_settings_warning_message" msgid="37741686486073668">"Αυτές οι ρυθμίσεις προορίζονται για χρήση κατά την ανάπτυξη. Μπορούν να προκαλέσουν προβλήματα στη λειτουργία της συσκευής και των εφαρμογών σας."</string> <string name="verify_apps_over_usb_title" msgid="6031809675604442636">"Επαλήθευση εφαρμογών μέσω USB"</string> @@ -373,14 +373,14 @@ <string name="disable_linux_terminal_disclaimer" msgid="3054320531778388231">"Εάν απενεργοποιηθεί, τα δεδομένα τερματικού Linux θα διαγραφούν"</string> <string name="hdcp_checking_title" msgid="3155692785074095986">"Έλεγχος HDCP"</string> <string name="hdcp_checking_dialog_title" msgid="7691060297616217781">"Ρύθμιση συμπεριφοράς ελέγχου HDCP"</string> - <string name="debug_debugging_category" msgid="535341063709248842">"Εντοπισμός σφαλμάτων"</string> + <string name="debug_debugging_category" msgid="535341063709248842">"Εντοπ. σφαλμ."</string> <string name="debug_app" msgid="8903350241392391766">"Επιλέξτε εφαρμογή εντοπισμού σφαλμάτων"</string> <string name="debug_app_not_set" msgid="1934083001283807188">"Δεν έχει οριστεί εφαρμογή εντοπισμού σφαλμάτων"</string> - <string name="debug_app_set" msgid="6599535090477753651">"Εφαρμογή εντοπισμού σφαλμάτων: <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="debug_app_set" msgid="6599535090477753651">"Εφαρμογή αποσφαλμάτωσης: <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> <string name="select_application" msgid="2543228890535466325">"Επιλέξτε εφαρμογή"</string> <string name="no_application" msgid="9038334538870247690">"Καμία"</string> <string name="wait_for_debugger" msgid="7461199843335409809">"Περιμένετε το εργαλείο εντοπισμού σφαλμάτων"</string> - <string name="wait_for_debugger_summary" msgid="6846330006113363286">"Αναμονή εφαρμογής για να συνδεθεί ο εντοπισμός σφαλμάτων"</string> + <string name="wait_for_debugger_summary" msgid="6846330006113363286">"Αναμονή εφαρμογής για να συνδεθεί η αποσφαλμάτωση"</string> <string name="debug_input_category" msgid="7349460906970849771">"Εισαγωγή"</string> <string name="debug_drawing_category" msgid="5066171112313666619">"Σχέδιο"</string> <string name="debug_hw_drawing_category" msgid="5830815169336975162">"Απόδοση με επιτάχυνση από υλικό εξοπλισμό"</string> @@ -419,7 +419,7 @@ <string name="window_blurs" msgid="6831008984828425106">"Θάμπωμα σε επίπεδο παραθ."</string> <string name="force_msaa" msgid="4081288296137775550">"Αναγκαστικά 4x MSAA"</string> <string name="force_msaa_summary" msgid="9070437493586769500">"Ενεργοποίηση 4x MSAA σε εφαρμογές OpenGL ES 2.0"</string> - <string name="show_non_rect_clip" msgid="7499758654867881817">"Εντοπισμός σφαλμάτων σε λειτουργίες μη ορθογώνιας περιοχής"</string> + <string name="show_non_rect_clip" msgid="7499758654867881817">"Αποσφαλμάτωση σε λειτουργίες μη ορθογώνιας περιοχής"</string> <string name="track_frame_time" msgid="522674651937771106">"Απόδοση HWUI προφίλ"</string> <string name="enable_gpu_debug_layers" msgid="4986675516188740397">"Ενεργ. επιπ. εντ. σφ. GPU"</string> <string name="enable_gpu_debug_layers_summary" msgid="4921521407377170481">"Φόρτωση επιπ. εντοπ. σφ. GPU για εφαρμ. αντιμ. σφ."</string> @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Ενσωματωμένο ηχείο"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Ήχος τηλεόρασης"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Πρόβλημα κατά τη σύνδεση. Απενεργοποιήστε τη συσκευή και ενεργοποιήστε την ξανά"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Δεν είναι δυνατή η σύνδεση"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Ενσύρματη συσκευή ήχου"</string> <string name="help_label" msgid="3528360748637781274">"Βοήθεια και σχόλια"</string> <string name="storage_category" msgid="2287342585424631813">"Αποθηκευτικός χώρος"</string> diff --git a/packages/SettingsLib/res/values-en-rAU/strings.xml b/packages/SettingsLib/res/values-en-rAU/strings.xml index e2e3851eb3fd..109558e01da7 100644 --- a/packages/SettingsLib/res/values-en-rAU/strings.xml +++ b/packages/SettingsLib/res/values-en-rAU/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Built-in speaker"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"TV audio"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Problem connecting. Turn device off and back on"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Can\'t connect"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Wired audio device"</string> <string name="help_label" msgid="3528360748637781274">"Help and feedback"</string> <string name="storage_category" msgid="2287342585424631813">"Storage"</string> diff --git a/packages/SettingsLib/res/values-en-rGB/strings.xml b/packages/SettingsLib/res/values-en-rGB/strings.xml index e2e3851eb3fd..109558e01da7 100644 --- a/packages/SettingsLib/res/values-en-rGB/strings.xml +++ b/packages/SettingsLib/res/values-en-rGB/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Built-in speaker"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"TV audio"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Problem connecting. Turn device off and back on"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Can\'t connect"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Wired audio device"</string> <string name="help_label" msgid="3528360748637781274">"Help and feedback"</string> <string name="storage_category" msgid="2287342585424631813">"Storage"</string> diff --git a/packages/SettingsLib/res/values-en-rIN/strings.xml b/packages/SettingsLib/res/values-en-rIN/strings.xml index e2e3851eb3fd..109558e01da7 100644 --- a/packages/SettingsLib/res/values-en-rIN/strings.xml +++ b/packages/SettingsLib/res/values-en-rIN/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Built-in speaker"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"TV audio"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Problem connecting. Turn device off and back on"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Can\'t connect"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Wired audio device"</string> <string name="help_label" msgid="3528360748637781274">"Help and feedback"</string> <string name="storage_category" msgid="2287342585424631813">"Storage"</string> diff --git a/packages/SettingsLib/res/values-es-rUS/strings.xml b/packages/SettingsLib/res/values-es-rUS/strings.xml index 17909f984640..216ae5757f3b 100644 --- a/packages/SettingsLib/res/values-es-rUS/strings.xml +++ b/packages/SettingsLib/res/values-es-rUS/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Bocina integrada"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Audio de la TV"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Error al establecer la conexión. Apaga el dispositivo y vuelve a encenderlo."</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"No se puede establecer la conexión"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Dispositivo de audio con cable"</string> <string name="help_label" msgid="3528360748637781274">"Ayuda y comentarios"</string> <string name="storage_category" msgid="2287342585424631813">"Almacenamiento"</string> diff --git a/packages/SettingsLib/res/values-es/strings.xml b/packages/SettingsLib/res/values-es/strings.xml index 536f0995a001..6bbff4b7061a 100644 --- a/packages/SettingsLib/res/values-es/strings.xml +++ b/packages/SettingsLib/res/values-es/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Altavoz integrado"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Audio de la televisión"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"No se ha podido conectar; reinicia el dispositivo"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"No se puede conectar"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Dispositivo de audio con cable"</string> <string name="help_label" msgid="3528360748637781274">"Ayuda y comentarios"</string> <string name="storage_category" msgid="2287342585424631813">"Almacenamiento"</string> diff --git a/packages/SettingsLib/res/values-et/strings.xml b/packages/SettingsLib/res/values-et/strings.xml index 86ccf28c65a1..19df69dc1121 100644 --- a/packages/SettingsLib/res/values-et/strings.xml +++ b/packages/SettingsLib/res/values-et/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Sisseehitatud kõlar"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Teleri heli"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Probleem ühendamisel. Lülitage seade välja ja uuesti sisse"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Ei saa ühendada"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Juhtmega heliseade"</string> <string name="help_label" msgid="3528360748637781274">"Abi ja tagasiside"</string> <string name="storage_category" msgid="2287342585424631813">"Salvestusruum"</string> diff --git a/packages/SettingsLib/res/values-eu/strings.xml b/packages/SettingsLib/res/values-eu/strings.xml index 4de40f53df0e..788febba580b 100644 --- a/packages/SettingsLib/res/values-eu/strings.xml +++ b/packages/SettingsLib/res/values-eu/strings.xml @@ -539,8 +539,7 @@ <string name="disabled_by_app_ops_text" msgid="8373595926549098012">"Ezarpen mugatuak kontrolatzen du"</string> <string name="disabled_in_phone_call_text" msgid="6568931334337318320">"Ez dago erabilgarri deiak egin bitartean"</string> <string name="disabled" msgid="8017887509554714950">"Desgaituta"</string> - <!-- no translation found for enabled (3997122818554810678) --> - <skip /> + <string name="enabled" msgid="3997122818554810678">"Gaituta"</string> <string name="external_source_trusted" msgid="1146522036773132905">"Baimenduta"</string> <string name="external_source_untrusted" msgid="5037891688911672227">"Baimendu gabe"</string> <string name="install_other_apps" msgid="3232595082023199454">"Instalatu aplikazio ezezagunak"</string> @@ -628,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Bozgorailu integratua"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Telebistako audioa"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Arazo bat izan da konektatzean. Itzali gailua eta pitz ezazu berriro."</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Ezin da konektatu"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Audio-gailu kableduna"</string> <string name="help_label" msgid="3528360748637781274">"Laguntza eta iritziak"</string> <string name="storage_category" msgid="2287342585424631813">"Biltegiratzea"</string> diff --git a/packages/SettingsLib/res/values-fa/strings.xml b/packages/SettingsLib/res/values-fa/strings.xml index f3fa38b1ded1..75cf19cacb35 100644 --- a/packages/SettingsLib/res/values-fa/strings.xml +++ b/packages/SettingsLib/res/values-fa/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"بلندگوی داخلی"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"صدای تلویزیون"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"مشکل در اتصال. دستگاه را خاموش و دوباره روشن کنید"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"اتصال برقرار نشد"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"دستگاه صوتی سیمی"</string> <string name="help_label" msgid="3528360748637781274">"راهنما و بازخورد"</string> <string name="storage_category" msgid="2287342585424631813">"فضای ذخیرهسازی"</string> diff --git a/packages/SettingsLib/res/values-fi/strings.xml b/packages/SettingsLib/res/values-fi/strings.xml index e212f0911530..7854c6762fe1 100644 --- a/packages/SettingsLib/res/values-fi/strings.xml +++ b/packages/SettingsLib/res/values-fi/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Sisäänrakennettu kaiutin"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"TV:n audio"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Yhteysvirhe. Sammuta laite ja käynnistä se uudelleen."</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Ei yhteyttä"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Langallinen äänilaite"</string> <string name="help_label" msgid="3528360748637781274">"Ohjeet ja palaute"</string> <string name="storage_category" msgid="2287342585424631813">"Tallennustila"</string> diff --git a/packages/SettingsLib/res/values-fr-rCA/strings.xml b/packages/SettingsLib/res/values-fr-rCA/strings.xml index 7c3c84408f4a..3f267e07ee9d 100644 --- a/packages/SettingsLib/res/values-fr-rCA/strings.xml +++ b/packages/SettingsLib/res/values-fr-rCA/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Haut-parleur intégré"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Sortie audio du téléviseur"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Problème de connexion. Éteignez et rallumez l\'appareil"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Connexion impossible"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Appareil audio à câble"</string> <string name="help_label" msgid="3528360748637781274">"Aide et commentaires"</string> <string name="storage_category" msgid="2287342585424631813">"Stockage"</string> diff --git a/packages/SettingsLib/res/values-fr/strings.xml b/packages/SettingsLib/res/values-fr/strings.xml index 1053e15d23c3..33c18837fecc 100644 --- a/packages/SettingsLib/res/values-fr/strings.xml +++ b/packages/SettingsLib/res/values-fr/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Haut-parleur intégré"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Audio TV"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Problème de connexion. Éteignez l\'appareil, puis rallumez-le"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Impossible de se connecter"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Appareil audio filaire"</string> <string name="help_label" msgid="3528360748637781274">"Aide et commentaires"</string> <string name="storage_category" msgid="2287342585424631813">"Stockage"</string> diff --git a/packages/SettingsLib/res/values-gl/strings.xml b/packages/SettingsLib/res/values-gl/strings.xml index 618dd62d248b..8044fd857dd4 100644 --- a/packages/SettingsLib/res/values-gl/strings.xml +++ b/packages/SettingsLib/res/values-gl/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Altofalante integrado"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Saída de audio da televisión"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Produciuse un problema coa conexión. Apaga e acende o dispositivo."</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Non se puido establecer a conexión"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Dispositivo de audio con cable"</string> <string name="help_label" msgid="3528360748637781274">"Axuda e comentarios"</string> <string name="storage_category" msgid="2287342585424631813">"Almacenamento"</string> diff --git a/packages/SettingsLib/res/values-gu/strings.xml b/packages/SettingsLib/res/values-gu/strings.xml index 9f8fc1046920..c8491e053fc2 100644 --- a/packages/SettingsLib/res/values-gu/strings.xml +++ b/packages/SettingsLib/res/values-gu/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"બિલ્ટ-ઇન સ્પીકર"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"ટીવીનો ઑડિયો"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"કનેક્ટ કરવામાં સમસ્યા આવી રહી છે. ડિવાઇસને બંધ કરીને ફરી ચાલુ કરો"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"કનેક્ટ કરી શકતા નથી"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"વાયરવાળો ઑડિયો ડિવાઇસ"</string> <string name="help_label" msgid="3528360748637781274">"સહાય અને પ્રતિસાદ"</string> <string name="storage_category" msgid="2287342585424631813">"સ્ટોરેજ"</string> diff --git a/packages/SettingsLib/res/values-hi/strings.xml b/packages/SettingsLib/res/values-hi/strings.xml index cb6e3c90e7ca..407075a6bd86 100644 --- a/packages/SettingsLib/res/values-hi/strings.xml +++ b/packages/SettingsLib/res/values-hi/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"डिवाइस में पहले से मौजूद स्पीकर"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"टीवी ऑडियो"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"कनेक्ट करने में समस्या हो रही है. डिवाइस को बंद करके चालू करें"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"कनेक्ट नहीं किया जा सकता"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"वायर वाला ऑडियो डिवाइस"</string> <string name="help_label" msgid="3528360748637781274">"सहायता और सुझाव"</string> <string name="storage_category" msgid="2287342585424631813">"डिवाइस का स्टोरेज"</string> diff --git a/packages/SettingsLib/res/values-hr/strings.xml b/packages/SettingsLib/res/values-hr/strings.xml index 10863cf62551..380fbe8c0d06 100644 --- a/packages/SettingsLib/res/values-hr/strings.xml +++ b/packages/SettingsLib/res/values-hr/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Ugrađeni zvučnik"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"TV zvuk"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Problem s povezivanjem. Isključite i ponovo uključite uređaj"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Povezivanje nije uspjelo"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Žičani audiouređaj"</string> <string name="help_label" msgid="3528360748637781274">"Pomoć i povratne informacije"</string> <string name="storage_category" msgid="2287342585424631813">"Pohrana"</string> diff --git a/packages/SettingsLib/res/values-hu/strings.xml b/packages/SettingsLib/res/values-hu/strings.xml index 104e4bca7bd0..9d08e0531df4 100644 --- a/packages/SettingsLib/res/values-hu/strings.xml +++ b/packages/SettingsLib/res/values-hu/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Beépített hangszóró"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Tévés hangkimenet"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Sikertelen csatlakozás. Kapcsolja ki az eszközt, majd kapcsolja be újra."</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Sikertelen csatlakozás"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Vezetékes audioeszköz"</string> <string name="help_label" msgid="3528360748637781274">"Súgó és visszajelzés"</string> <string name="storage_category" msgid="2287342585424631813">"Tárhely"</string> diff --git a/packages/SettingsLib/res/values-hy/strings.xml b/packages/SettingsLib/res/values-hy/strings.xml index a5a15f98b631..425cb07289e7 100644 --- a/packages/SettingsLib/res/values-hy/strings.xml +++ b/packages/SettingsLib/res/values-hy/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Ներկառուցված բարձրախոս"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Հեռուստացույցի աուդիո"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Կապի խնդիր կա: Սարքն անջատեք և նորից միացրեք:"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Չի հաջողվում միանալ"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Լարով աուդիո սարք"</string> <string name="help_label" msgid="3528360748637781274">"Օգնություն և հետադարձ կապ"</string> <string name="storage_category" msgid="2287342585424631813">"Տարածք"</string> diff --git a/packages/SettingsLib/res/values-in/strings.xml b/packages/SettingsLib/res/values-in/strings.xml index c8d7481ecd33..99b3bdca4094 100644 --- a/packages/SettingsLib/res/values-in/strings.xml +++ b/packages/SettingsLib/res/values-in/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Speaker bawaan"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Audio TV"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Ada masalah saat menghubungkan. Nonaktifkan perangkat & aktifkan kembali"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Tidak dapat terhubung"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Perangkat audio berkabel"</string> <string name="help_label" msgid="3528360748637781274">"Bantuan & masukan"</string> <string name="storage_category" msgid="2287342585424631813">"Penyimpanan"</string> diff --git a/packages/SettingsLib/res/values-is/strings.xml b/packages/SettingsLib/res/values-is/strings.xml index f85c20b4b9fb..b337f353c350 100644 --- a/packages/SettingsLib/res/values-is/strings.xml +++ b/packages/SettingsLib/res/values-is/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Innbyggður hátalari"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Sjónvarpshljóð"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Vandamál í tengingu. Slökktu og kveiktu á tækinu"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Ekki tókst að tengjast"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Snúrutengt hljómtæki"</string> <string name="help_label" msgid="3528360748637781274">"Hjálp og ábendingar"</string> <string name="storage_category" msgid="2287342585424631813">"Geymsla"</string> diff --git a/packages/SettingsLib/res/values-it/strings.xml b/packages/SettingsLib/res/values-it/strings.xml index 1b31ae012b67..8347e6a4063a 100644 --- a/packages/SettingsLib/res/values-it/strings.xml +++ b/packages/SettingsLib/res/values-it/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Altoparlante integrato"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Audio della TV"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Problema di connessione. Spegni e riaccendi il dispositivo"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Impossibile connettersi"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Dispositivo audio cablato"</string> <string name="help_label" msgid="3528360748637781274">"Guida e feedback"</string> <string name="storage_category" msgid="2287342585424631813">"Archiviazione"</string> diff --git a/packages/SettingsLib/res/values-ja/strings.xml b/packages/SettingsLib/res/values-ja/strings.xml index 78c16de0f63b..748502610376 100644 --- a/packages/SettingsLib/res/values-ja/strings.xml +++ b/packages/SettingsLib/res/values-ja/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"内蔵スピーカー"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"TV オーディオ"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"接続エラーです。デバイスを OFF にしてから ON に戻してください"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"接続できません"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"有線オーディオ デバイス"</string> <string name="help_label" msgid="3528360748637781274">"ヘルプとフィードバック"</string> <string name="storage_category" msgid="2287342585424631813">"ストレージ"</string> diff --git a/packages/SettingsLib/res/values-ka/strings.xml b/packages/SettingsLib/res/values-ka/strings.xml index a7599bedd83d..fe7e65c6f5f0 100644 --- a/packages/SettingsLib/res/values-ka/strings.xml +++ b/packages/SettingsLib/res/values-ka/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"ჩაშენებული დინამიკი"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"TV Audio"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"დაკავშირებისას წარმოიქმნა პრობლემა. გამორთეთ და კვლავ ჩართეთ მოწყობილობა"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"დაკავშირება შეუძლებელია"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"სადენიანი აუდიო მოწყობილობა"</string> <string name="help_label" msgid="3528360748637781274">"დახმარება და გამოხმაურება"</string> <string name="storage_category" msgid="2287342585424631813">"საცავი"</string> diff --git a/packages/SettingsLib/res/values-km/strings.xml b/packages/SettingsLib/res/values-km/strings.xml index b8a8355a7249..6053ff70512d 100644 --- a/packages/SettingsLib/res/values-km/strings.xml +++ b/packages/SettingsLib/res/values-km/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"ឧបករណ៍សំឡេងមកជាមួយ"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"សំឡេងទូរទស្សន៍"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"មានបញ្ហាក្នុងការភ្ជាប់។ បិទ រួចបើកឧបករណ៍វិញ"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"មិនអាចភ្ជាប់បានទេ"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"ឧបករណ៍សំឡេងប្រើខ្សែ"</string> <string name="help_label" msgid="3528360748637781274">"ជំនួយ និងមតិកែលម្អ"</string> <string name="storage_category" msgid="2287342585424631813">"ទំហំផ្ទុក"</string> diff --git a/packages/SettingsLib/res/values-kn/strings.xml b/packages/SettingsLib/res/values-kn/strings.xml index 4a8a4135e98e..61b235e805ea 100644 --- a/packages/SettingsLib/res/values-kn/strings.xml +++ b/packages/SettingsLib/res/values-kn/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"ಅಂತರ್ ನಿರ್ಮಿತ ಧ್ವನಿ ವರ್ಧಕ"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"TV ಆಡಿಯೊ"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"ಕನೆಕ್ಟ್ ಮಾಡುವಾಗ ಸಮಸ್ಯೆ ಎದುರಾಗಿದೆ ಸಾಧನವನ್ನು ಆಫ್ ಮಾಡಿ ಹಾಗೂ ನಂತರ ಪುನಃ ಆನ್ ಮಾಡಿ"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"ಕನೆಕ್ಟ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"ವೈರ್ ಹೊಂದಿರುವ ಆಡಿಯೋ ಸಾಧನ"</string> <string name="help_label" msgid="3528360748637781274">"ಸಹಾಯ ಮತ್ತು ಪ್ರತಿಕ್ರಿಯೆ"</string> <string name="storage_category" msgid="2287342585424631813">"ಸಂಗ್ರಹಣೆ"</string> diff --git a/packages/SettingsLib/res/values-ko/strings.xml b/packages/SettingsLib/res/values-ko/strings.xml index ee99bebf7663..d0a583bd8ede 100644 --- a/packages/SettingsLib/res/values-ko/strings.xml +++ b/packages/SettingsLib/res/values-ko/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"내장 스피커"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"TV 오디오"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"연결 중에 문제가 발생했습니다. 기기를 껐다가 다시 켜 보세요."</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"연결할 수 없습니다."</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"유선 오디오 기기"</string> <string name="help_label" msgid="3528360748637781274">"고객센터"</string> <string name="storage_category" msgid="2287342585424631813">"저장용량"</string> diff --git a/packages/SettingsLib/res/values-ky/strings.xml b/packages/SettingsLib/res/values-ky/strings.xml index 5a8a66f7fee9..c68fb70ff3bd 100644 --- a/packages/SettingsLib/res/values-ky/strings.xml +++ b/packages/SettingsLib/res/values-ky/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Алдын ала орнотулган динамик"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Сыналгы аудиосу"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Туташууда маселе келип чыкты. Түзмөктү өчүрүп, кайра күйгүзүп көрүңүз"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Туташпай жатат"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Зымдуу аудио түзмөк"</string> <string name="help_label" msgid="3528360748637781274">"Жардам/Пикир билдирүү"</string> <string name="storage_category" msgid="2287342585424631813">"Сактагыч"</string> diff --git a/packages/SettingsLib/res/values-lo/strings.xml b/packages/SettingsLib/res/values-lo/strings.xml index c60c04d668e8..c4e25f8eb9fb 100644 --- a/packages/SettingsLib/res/values-lo/strings.xml +++ b/packages/SettingsLib/res/values-lo/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"ລຳໂພງທີ່ມີໃນຕົວ"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"ສຽງນີ້"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"ເກີດບັນຫາໃນການເຊື່ອມຕໍ່. ປິດອຸປະກອນແລ້ວເປີດກັບຄືນມາໃໝ່"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"ບໍ່ສາມາດເຊື່ອມຕໍ່ໄດ້"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"ອຸປະກອນສຽງແບບມີສາຍ"</string> <string name="help_label" msgid="3528360748637781274">"ຊ່ວຍເຫຼືອ ແລະ ຕິຊົມ"</string> <string name="storage_category" msgid="2287342585424631813">"ບ່ອນເກັບຂໍ້ມູນ"</string> diff --git a/packages/SettingsLib/res/values-lt/strings.xml b/packages/SettingsLib/res/values-lt/strings.xml index 6467e89840e2..0379d5c636ea 100644 --- a/packages/SettingsLib/res/values-lt/strings.xml +++ b/packages/SettingsLib/res/values-lt/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Įtaisytas garsiakalbis"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"TV garso įrašas"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Prisijungiant kilo problema. Išjunkite įrenginį ir vėl jį įjunkite"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Nepavyksta prisijungti"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Laidinis garso įrenginys"</string> <string name="help_label" msgid="3528360748637781274">"Pagalba ir atsiliepimai"</string> <string name="storage_category" msgid="2287342585424631813">"Saugykla"</string> diff --git a/packages/SettingsLib/res/values-mk/strings.xml b/packages/SettingsLib/res/values-mk/strings.xml index 71d9b76e2f38..a1812490c94f 100644 --- a/packages/SettingsLib/res/values-mk/strings.xml +++ b/packages/SettingsLib/res/values-mk/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Вграден звучник"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Аудио на телевизор"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Проблем со поврзување. Исклучете го уредот и повторно вклучете го"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Не може да се поврзе"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Жичен аудиоуред"</string> <string name="help_label" msgid="3528360748637781274">"Помош и повратни информации"</string> <string name="storage_category" msgid="2287342585424631813">"Простор"</string> diff --git a/packages/SettingsLib/res/values-ml/strings.xml b/packages/SettingsLib/res/values-ml/strings.xml index d9c9629a5d40..b1d1d4fc2c02 100644 --- a/packages/SettingsLib/res/values-ml/strings.xml +++ b/packages/SettingsLib/res/values-ml/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"ബിൽട്ട്-ഇൻ സ്പീക്കർ"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"ടിവി ഓഡിയോ"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"കണക്റ്റ് ചെയ്യുന്നതിൽ പ്രശ്നമുണ്ടായി. ഉപകരണം ഓഫാക്കി വീണ്ടും ഓണാക്കുക"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"കണക്റ്റ് ചെയ്യാനാകുന്നില്ല"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"വയർ മുഖേന ബന്ധിപ്പിച്ച ഓഡിയോ ഉപകരണം"</string> <string name="help_label" msgid="3528360748637781274">"സഹായവും ഫീഡ്ബാക്കും"</string> <string name="storage_category" msgid="2287342585424631813">"സ്റ്റോറേജ്"</string> diff --git a/packages/SettingsLib/res/values-mn/strings.xml b/packages/SettingsLib/res/values-mn/strings.xml index 829b1d7181df..9b128bd55bfe 100644 --- a/packages/SettingsLib/res/values-mn/strings.xml +++ b/packages/SettingsLib/res/values-mn/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Бүрэлдэхүүн чанга яригч"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"ТВ-ийн аудио"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Холбогдоход асуудал гарлаа. Төхөөрөмжийг унтраагаад дахин асаана уу"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Холбогдох боломжгүй байна"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Утастай аудио төхөөрөмж"</string> <string name="help_label" msgid="3528360748637781274">"Тусламж, санал хүсэлт"</string> <string name="storage_category" msgid="2287342585424631813">"Хадгалах сан"</string> diff --git a/packages/SettingsLib/res/values-mr/strings.xml b/packages/SettingsLib/res/values-mr/strings.xml index 8207174f0a0f..7c8c6aac2296 100644 --- a/packages/SettingsLib/res/values-mr/strings.xml +++ b/packages/SettingsLib/res/values-mr/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"बिल्ट-इन स्पीकर"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"टीव्ही ऑडिओ"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"कनेक्ट करण्यात समस्या आली. डिव्हाइस बंद करा आणि नंतर सुरू करा"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"कनेक्ट करू शकत नाही"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"वायर असलेले ऑडिओ डिव्हाइस"</string> <string name="help_label" msgid="3528360748637781274">"मदत आणि फीडबॅक"</string> <string name="storage_category" msgid="2287342585424631813">"स्टोरेज"</string> diff --git a/packages/SettingsLib/res/values-ms/strings.xml b/packages/SettingsLib/res/values-ms/strings.xml index ea3d78e4aa93..52d1d5ac849d 100644 --- a/packages/SettingsLib/res/values-ms/strings.xml +++ b/packages/SettingsLib/res/values-ms/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Pembesar suara terbina dalam"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Audio TV"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Masalah penyambungan. Matikan & hidupkan kembali peranti"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Tidak dapat disambungkan"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Peranti audio berwayar"</string> <string name="help_label" msgid="3528360748637781274">"Bantuan & maklum balas"</string> <string name="storage_category" msgid="2287342585424631813">"Storan"</string> diff --git a/packages/SettingsLib/res/values-my/strings.xml b/packages/SettingsLib/res/values-my/strings.xml index c484a741bc2f..e3d1005c24eb 100644 --- a/packages/SettingsLib/res/values-my/strings.xml +++ b/packages/SettingsLib/res/values-my/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"အသင့်ပါ စပီကာ"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"TV အသံ"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"ချိတ်ဆက်ရာတွင် ပြဿနာရှိပါသည်။ စက်ကိုပိတ်ပြီး ပြန်ဖွင့်ပါ"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"ချိတ်ဆက်၍မရပါ"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"ကြိုးတပ် အသံစက်ပစ္စည်း"</string> <string name="help_label" msgid="3528360748637781274">"အကူအညီနှင့် အကြံပြုချက်"</string> <string name="storage_category" msgid="2287342585424631813">"သိုလှောင်ခန်း"</string> diff --git a/packages/SettingsLib/res/values-nb/strings.xml b/packages/SettingsLib/res/values-nb/strings.xml index 3c06c685aa7b..9b2fbd8184c0 100644 --- a/packages/SettingsLib/res/values-nb/strings.xml +++ b/packages/SettingsLib/res/values-nb/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Innebygd høyttaler"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"TV-lyd"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Tilkoblingsproblemer. Slå enheten av og på igjen"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Kan ikke koble til"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Lydenhet med kabel"</string> <string name="help_label" msgid="3528360748637781274">"Hjelp og tilbakemelding"</string> <string name="storage_category" msgid="2287342585424631813">"Lagring"</string> diff --git a/packages/SettingsLib/res/values-ne/strings.xml b/packages/SettingsLib/res/values-ne/strings.xml index fe743e8330fb..005f0cd3738d 100644 --- a/packages/SettingsLib/res/values-ne/strings.xml +++ b/packages/SettingsLib/res/values-ne/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"अन्तर्निर्मित स्पिकर"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"टिभीको अडियो"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"जोड्ने क्रममा समस्या भयो। यन्त्रलाई निष्क्रिय पारेर फेरि अन गर्नुहोस्"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"कनेक्ट गर्न सकिएन"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"तारयुक्त अडियो यन्त्र"</string> <string name="help_label" msgid="3528360748637781274">"मद्दत र प्रतिक्रिया"</string> <string name="storage_category" msgid="2287342585424631813">"भण्डारण"</string> diff --git a/packages/SettingsLib/res/values-nl/strings.xml b/packages/SettingsLib/res/values-nl/strings.xml index f49829f2e834..ffde98a5f303 100644 --- a/packages/SettingsLib/res/values-nl/strings.xml +++ b/packages/SettingsLib/res/values-nl/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Ingebouwde speaker"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Tv-audio"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Probleem bij verbinding maken. Zet het apparaat uit en weer aan."</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Kan geen verbinding maken"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Bedraad audioapparaat"</string> <string name="help_label" msgid="3528360748637781274">"Hulp en feedback"</string> <string name="storage_category" msgid="2287342585424631813">"Opslag"</string> diff --git a/packages/SettingsLib/res/values-or/strings.xml b/packages/SettingsLib/res/values-or/strings.xml index ed202fae1b7a..0ba9cb6a75c2 100644 --- a/packages/SettingsLib/res/values-or/strings.xml +++ b/packages/SettingsLib/res/values-or/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"ବିଲ୍ଟ-ଇନ ସ୍ପିକର"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"ଟିଭି ଅଡିଓ"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"ସଂଯୋଗ କରିବାରେ ସମସ୍ୟା ହେଉଛି। ଡିଭାଇସ୍ ବନ୍ଦ କରି ପୁଣି ଚାଲୁ କରନ୍ତୁ"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"କନେକ୍ଟ କରାଯାଇପାରିବ ନାହିଁ"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"ତାରଯୁକ୍ତ ଅଡିଓ ଡିଭାଇସ୍"</string> <string name="help_label" msgid="3528360748637781274">"ସାହାଯ୍ୟ ଓ ମତାମତ"</string> <string name="storage_category" msgid="2287342585424631813">"ଷ୍ଟୋରେଜ"</string> diff --git a/packages/SettingsLib/res/values-pa/strings.xml b/packages/SettingsLib/res/values-pa/strings.xml index ee0f761e00f0..08106e469bfd 100644 --- a/packages/SettingsLib/res/values-pa/strings.xml +++ b/packages/SettingsLib/res/values-pa/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"ਬਿਲਟ-ਇਨ ਸਪੀਕਰ"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"ਟੀਵੀ ਆਡੀਓ"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"ਕਨੈਕਟ ਕਰਨ ਵਿੱਚ ਸਮੱਸਿਆ ਆਈ। ਡੀਵਾਈਸ ਨੂੰ ਬੰਦ ਕਰਕੇ ਵਾਪਸ ਚਾਲੂ ਕਰੋ"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"ਕਨੈਕਟ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"ਤਾਰ ਵਾਲਾ ਆਡੀਓ ਡੀਵਾਈਸ"</string> <string name="help_label" msgid="3528360748637781274">"ਮਦਦ ਅਤੇ ਵਿਚਾਰ"</string> <string name="storage_category" msgid="2287342585424631813">"ਸਟੋਰੇਜ"</string> diff --git a/packages/SettingsLib/res/values-pl/strings.xml b/packages/SettingsLib/res/values-pl/strings.xml index fec5bf1469d8..d8d2b0befde5 100644 --- a/packages/SettingsLib/res/values-pl/strings.xml +++ b/packages/SettingsLib/res/values-pl/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Wbudowany głośnik"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Audio z telewizora"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Problem z połączeniem. Wyłącz i ponownie włącz urządzenie"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Nie można się połączyć"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Przewodowe urządzenie audio"</string> <string name="help_label" msgid="3528360748637781274">"Pomoc i opinie"</string> <string name="storage_category" msgid="2287342585424631813">"Pamięć wewnętrzna"</string> diff --git a/packages/SettingsLib/res/values-pt-rPT/strings.xml b/packages/SettingsLib/res/values-pt-rPT/strings.xml index 3486037ba240..194af5b03337 100644 --- a/packages/SettingsLib/res/values-pt-rPT/strings.xml +++ b/packages/SettingsLib/res/values-pt-rPT/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Altifalante integrado"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Áudio da TV"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Problema ao ligar. Desligue e volte a ligar o dispositivo."</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Não é possível estabelecer ligação"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Dispositivo de áudio com fios"</string> <string name="help_label" msgid="3528360748637781274">"Ajuda e feedback"</string> <string name="storage_category" msgid="2287342585424631813">"Armazenamento"</string> diff --git a/packages/SettingsLib/res/values-ro/strings.xml b/packages/SettingsLib/res/values-ro/strings.xml index 8965a0cfdd2f..7d07d5438363 100644 --- a/packages/SettingsLib/res/values-ro/strings.xml +++ b/packages/SettingsLib/res/values-ro/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Difuzor încorporat"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Audio TV"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Problemă la conectare. Oprește și repornește dispozitivul."</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Nu se poate conecta"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Dispozitiv audio cu fir"</string> <string name="help_label" msgid="3528360748637781274">"Ajutor și feedback"</string> <string name="storage_category" msgid="2287342585424631813">"Stocare"</string> diff --git a/packages/SettingsLib/res/values-ru/strings.xml b/packages/SettingsLib/res/values-ru/strings.xml index 2da67085adf6..55ff696f418f 100644 --- a/packages/SettingsLib/res/values-ru/strings.xml +++ b/packages/SettingsLib/res/values-ru/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Встроенный динамик"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Аудиовыход телевизора"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Ошибка подключения. Выключите и снова включите устройство."</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Ошибка подключения"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Проводное аудиоустройство"</string> <string name="help_label" msgid="3528360748637781274">"Справка/отзыв"</string> <string name="storage_category" msgid="2287342585424631813">"Хранилище"</string> diff --git a/packages/SettingsLib/res/values-si/strings.xml b/packages/SettingsLib/res/values-si/strings.xml index 1c57acd16887..4546f4b75ebe 100644 --- a/packages/SettingsLib/res/values-si/strings.xml +++ b/packages/SettingsLib/res/values-si/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"එකට තැනූ ශබ්දවාහිනීය"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"රූපවාහිනී ශ්රව්ය"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"සම්බන්ධ කිරීමේ ගැටලුවකි උපාංගය ක්රියාවිරහිත කර & ආපසු ක්රියාත්මක කරන්න"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"සම්බන්ධ විය නොහැක"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"රැහැන්ගත කළ ඕඩියෝ උපාංගය"</string> <string name="help_label" msgid="3528360748637781274">"උදවු & ප්රතිපෝෂණ"</string> <string name="storage_category" msgid="2287342585424631813">"ගබඩාව"</string> diff --git a/packages/SettingsLib/res/values-sk/strings.xml b/packages/SettingsLib/res/values-sk/strings.xml index 5534ed081057..7578d063ebd4 100644 --- a/packages/SettingsLib/res/values-sk/strings.xml +++ b/packages/SettingsLib/res/values-sk/strings.xml @@ -627,11 +627,10 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Vstavaný reproduktor"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Zvuk televízora"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Pri pripájaní sa vyskytol problém. Zariadenie vypnite a znova zapnite."</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Nedá sa pripojiť"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Audio zariadenie s káblom"</string> <string name="help_label" msgid="3528360748637781274">"Pomocník a spätná väzba"</string> - <string name="storage_category" msgid="2287342585424631813">"Priestor"</string> + <string name="storage_category" msgid="2287342585424631813">"Ukladací priestor"</string> <string name="shared_data_title" msgid="1017034836800864953">"Zdieľané údaje"</string> <string name="shared_data_summary" msgid="5516326713822885652">"Zobrazenie a úprava zdieľaných údajov"</string> <string name="shared_data_no_blobs_text" msgid="3108114670341737434">"Pre tohto používateľa nie sú k dispozícii žiadne zdieľané údaje."</string> diff --git a/packages/SettingsLib/res/values-sl/strings.xml b/packages/SettingsLib/res/values-sl/strings.xml index be7b11587468..4b268cb7e223 100644 --- a/packages/SettingsLib/res/values-sl/strings.xml +++ b/packages/SettingsLib/res/values-sl/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Vgrajen zvočnik"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Zvok televizorja"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Težava pri povezovanju. Napravo izklopite in znova vklopite."</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Povezave ni mogoče vzpostaviti"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Žična zvočna naprava"</string> <string name="help_label" msgid="3528360748637781274">"Pomoč in povratne informacije"</string> <string name="storage_category" msgid="2287342585424631813">"Shramba"</string> diff --git a/packages/SettingsLib/res/values-sq/strings.xml b/packages/SettingsLib/res/values-sq/strings.xml index 06da1722ad75..a296f87e1ed7 100644 --- a/packages/SettingsLib/res/values-sq/strings.xml +++ b/packages/SettingsLib/res/values-sq/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Altoparlanti i integruar"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Audioja e televizorit"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Problem me lidhjen. Fike dhe ndize përsëri pajisjen"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Nuk mund të lidhet"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Pajisja audio me tel"</string> <string name="help_label" msgid="3528360748637781274">"Ndihma dhe komentet"</string> <string name="storage_category" msgid="2287342585424631813">"Hapësira ruajtëse"</string> diff --git a/packages/SettingsLib/res/values-sr/strings.xml b/packages/SettingsLib/res/values-sr/strings.xml index 63d1502c7809..defea3a5709f 100644 --- a/packages/SettingsLib/res/values-sr/strings.xml +++ b/packages/SettingsLib/res/values-sr/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Уграђени звучник"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Звук ТВ-а"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Проблем при повезивању. Искључите уређај, па га поново укључите"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Повезивање није успело"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Жичани аудио уређај"</string> <string name="help_label" msgid="3528360748637781274">"Помоћ и повратне информације"</string> <string name="storage_category" msgid="2287342585424631813">"Меморијски простор"</string> diff --git a/packages/SettingsLib/res/values-sv/strings.xml b/packages/SettingsLib/res/values-sv/strings.xml index 131cca795bb2..d03b99b24414 100644 --- a/packages/SettingsLib/res/values-sv/strings.xml +++ b/packages/SettingsLib/res/values-sv/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Inbyggd högtalare"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Tv-ljud"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Det gick inte att ansluta. Stäng av enheten och slå på den igen"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Det går inte att ansluta"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Ljudenhet med kabelanslutning"</string> <string name="help_label" msgid="3528360748637781274">"Hjälp och feedback"</string> <string name="storage_category" msgid="2287342585424631813">"Lagring"</string> diff --git a/packages/SettingsLib/res/values-sw/strings.xml b/packages/SettingsLib/res/values-sw/strings.xml index 1cb95a7ccde6..0755ce8b429e 100644 --- a/packages/SettingsLib/res/values-sw/strings.xml +++ b/packages/SettingsLib/res/values-sw/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Spika iliyojumuishwa"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Sauti ya Televisheni"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Kuna tatizo la kuunganisha kwenye Intaneti. Zima kisha uwashe kifaa"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Imeshindwa kuunganisha"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Kifaa cha sauti kinachotumia waya"</string> <string name="help_label" msgid="3528360748637781274">"Usaidizi na maoni"</string> <string name="storage_category" msgid="2287342585424631813">"Hifadhi"</string> diff --git a/packages/SettingsLib/res/values-ta/strings.xml b/packages/SettingsLib/res/values-ta/strings.xml index cc8a36fac3ce..a67f35edc338 100644 --- a/packages/SettingsLib/res/values-ta/strings.xml +++ b/packages/SettingsLib/res/values-ta/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"உள்ளமைந்த ஸ்பீக்கர்"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"டிவி ஆடியோ"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"இணைப்பதில் சிக்கல். சாதனத்தை ஆஃப் செய்து மீண்டும் ஆன் செய்யவும்"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"இணைக்க முடியவில்லை"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"வயருடன்கூடிய ஆடியோ சாதனம்"</string> <string name="help_label" msgid="3528360748637781274">"உதவியும் கருத்தும்"</string> <string name="storage_category" msgid="2287342585424631813">"சேமிப்பகம்"</string> diff --git a/packages/SettingsLib/res/values-te/strings.xml b/packages/SettingsLib/res/values-te/strings.xml index ccb5aa8cd140..ffca18d1a3f9 100644 --- a/packages/SettingsLib/res/values-te/strings.xml +++ b/packages/SettingsLib/res/values-te/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"బిల్ట్-ఇన్ స్పీకర్"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"టీవీ ఆడియో"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"కనెక్ట్ చేయడంలో సమస్య ఉంది. పరికరాన్ని ఆఫ్ చేసి, ఆపై తిరిగి ఆన్ చేయండి"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"కనెక్ట్ చేయడం సాధ్యపడలేదు"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"వైర్ గల ఆడియో పరికరం"</string> <string name="help_label" msgid="3528360748637781274">"సహాయం & ఫీడ్బ్యాక్"</string> <string name="storage_category" msgid="2287342585424631813">"స్టోరేజ్"</string> diff --git a/packages/SettingsLib/res/values-th/strings.xml b/packages/SettingsLib/res/values-th/strings.xml index 08d2017e8418..fce95ac055d5 100644 --- a/packages/SettingsLib/res/values-th/strings.xml +++ b/packages/SettingsLib/res/values-th/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"ลำโพงในตัว"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"เสียงจากทีวี"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"เกิดปัญหาในการเชื่อมต่อ ปิดอุปกรณ์แล้วเปิดใหม่อีกครั้ง"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"เชื่อมต่อไม่ได้"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"อุปกรณ์เสียงแบบมีสาย"</string> <string name="help_label" msgid="3528360748637781274">"ความช่วยเหลือและความคิดเห็น"</string> <string name="storage_category" msgid="2287342585424631813">"พื้นที่เก็บข้อมูล"</string> diff --git a/packages/SettingsLib/res/values-tl/strings.xml b/packages/SettingsLib/res/values-tl/strings.xml index 14069b9db21d..011c8d7127cb 100644 --- a/packages/SettingsLib/res/values-tl/strings.xml +++ b/packages/SettingsLib/res/values-tl/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Built-in na speaker"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Audio ng TV"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Nagkaproblema sa pagkonekta. I-off at pagkatapos ay i-on ang device"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Hindi makakonekta"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Wired na audio device"</string> <string name="help_label" msgid="3528360748637781274">"Tulong at feedback"</string> <string name="storage_category" msgid="2287342585424631813">"Storage"</string> diff --git a/packages/SettingsLib/res/values-tr/strings.xml b/packages/SettingsLib/res/values-tr/strings.xml index 70210030d7a9..42852306fae5 100644 --- a/packages/SettingsLib/res/values-tr/strings.xml +++ b/packages/SettingsLib/res/values-tr/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Dahili hoparlör"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"TV Sesi"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Bağlanırken sorun oluştu. Cihazı kapatıp tekrar açın"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Bağlanılamıyor"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Kablolu ses cihazı"</string> <string name="help_label" msgid="3528360748637781274">"Yardım ve geri bildirim"</string> <string name="storage_category" msgid="2287342585424631813">"Depolama"</string> diff --git a/packages/SettingsLib/res/values-ur/strings.xml b/packages/SettingsLib/res/values-ur/strings.xml index a0ff95a579b0..48aa063feb34 100644 --- a/packages/SettingsLib/res/values-ur/strings.xml +++ b/packages/SettingsLib/res/values-ur/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"پہلے سے شامل اسپیکر"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"TV آڈیو"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"منسلک کرنے میں مسئلہ پیش آ گیا۔ آلہ کو آف اور بیک آن کریں"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"منسلک نہیں ہو سکتا"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"وائرڈ آڈیو آلہ"</string> <string name="help_label" msgid="3528360748637781274">"مدد اور تاثرات"</string> <string name="storage_category" msgid="2287342585424631813">"اسٹوریج"</string> diff --git a/packages/SettingsLib/res/values-uz/strings.xml b/packages/SettingsLib/res/values-uz/strings.xml index 0a6588511647..8969c0d84f17 100644 --- a/packages/SettingsLib/res/values-uz/strings.xml +++ b/packages/SettingsLib/res/values-uz/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Ichki karnay"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"TV Audio"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Ulanishda muammo yuz berdi. Qurilmani oʻchiring va yoqing"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Ulanmadi"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Simli audio qurilma"</string> <string name="help_label" msgid="3528360748637781274">"Yordam/fikr-mulohaza"</string> <string name="storage_category" msgid="2287342585424631813">"Xotira"</string> diff --git a/packages/SettingsLib/res/values-vi/strings.xml b/packages/SettingsLib/res/values-vi/strings.xml index 5e1e3dad0034..e45661721a0b 100644 --- a/packages/SettingsLib/res/values-vi/strings.xml +++ b/packages/SettingsLib/res/values-vi/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Loa tích hợp"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Âm thanh TV"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Sự cố kết nối. Hãy tắt thiết bị rồi bật lại"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Không kết nối được"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Thiết bị âm thanh có dây"</string> <string name="help_label" msgid="3528360748637781274">"Trợ giúp và phản hồi"</string> <string name="storage_category" msgid="2287342585424631813">"Bộ nhớ"</string> diff --git a/packages/SettingsLib/res/values-zh-rCN/strings.xml b/packages/SettingsLib/res/values-zh-rCN/strings.xml index 6bf27b35d4fe..6fe9e2a2ecfc 100644 --- a/packages/SettingsLib/res/values-zh-rCN/strings.xml +++ b/packages/SettingsLib/res/values-zh-rCN/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"内置扬声器"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"电视音频"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"连接时遇到问题。请关闭并重新开启设备"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"无法连接"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"有线音频设备"</string> <string name="help_label" msgid="3528360748637781274">"帮助和反馈"</string> <string name="storage_category" msgid="2287342585424631813">"存储空间"</string> diff --git a/packages/SettingsLib/res/values-zh-rHK/strings.xml b/packages/SettingsLib/res/values-zh-rHK/strings.xml index 9f96b5bf44b1..23ee3c98aa03 100644 --- a/packages/SettingsLib/res/values-zh-rHK/strings.xml +++ b/packages/SettingsLib/res/values-zh-rHK/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"內置喇叭"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"電視音訊"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"無法連接,請關閉裝置然後重新開機"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"無法連線"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"有線音響裝置"</string> <string name="help_label" msgid="3528360748637781274">"說明與意見反映"</string> <string name="storage_category" msgid="2287342585424631813">"儲存空間"</string> diff --git a/packages/SettingsLib/res/values-zh-rTW/strings.xml b/packages/SettingsLib/res/values-zh-rTW/strings.xml index e5f29e904e0a..32151eeabd78 100644 --- a/packages/SettingsLib/res/values-zh-rTW/strings.xml +++ b/packages/SettingsLib/res/values-zh-rTW/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"內建喇叭"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"電視音訊"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"無法連線,請關閉裝置後再重新開啟"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"無法連線"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"有線音訊裝置"</string> <string name="help_label" msgid="3528360748637781274">"說明與意見回饋"</string> <string name="storage_category" msgid="2287342585424631813">"儲存空間"</string> diff --git a/packages/SettingsLib/res/values-zu/strings.xml b/packages/SettingsLib/res/values-zu/strings.xml index c587678056e8..e4f3c573a275 100644 --- a/packages/SettingsLib/res/values-zu/strings.xml +++ b/packages/SettingsLib/res/values-zu/strings.xml @@ -627,8 +627,7 @@ <string name="tv_media_transfer_internal_speakers" msgid="4662765121700213785">"Isipikha esakhelwe ngaphakathi"</string> <string name="tv_media_transfer_hdmi_title" msgid="6715658310934507444">"Umsindo we-TV"</string> <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Inkinga yokuxhumeka. Vala idivayisi futhi uphinde uyivule"</string> - <!-- no translation found for bluetooth_key_missing_subtext (1003639333895028298) --> - <skip /> + <string name="bluetooth_key_missing_subtext" msgid="1003639333895028298">"Ayikwazi ukuxhuma"</string> <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Idivayisi yomsindo enentambo"</string> <string name="help_label" msgid="3528360748637781274">"Usizo nempendulo"</string> <string name="storage_category" msgid="2287342585424631813">"Isitoreji"</string> diff --git a/packages/SettingsLib/res/xml/timezones.xml b/packages/SettingsLib/res/xml/timezones.xml index 6a8d7802f9fd..4cea32aa05f9 100644 --- a/packages/SettingsLib/res/xml/timezones.xml +++ b/packages/SettingsLib/res/xml/timezones.xml @@ -35,6 +35,7 @@ <timezone id="Europe/Brussels"></timezone> <timezone id="Europe/Madrid"></timezone> <timezone id="Europe/Sarajevo"></timezone> + <timezone id="Europe/Warsaw"></timezone> <timezone id="Africa/Windhoek"></timezone> <timezone id="Africa/Brazzaville"></timezone> <timezone id="Asia/Amman"></timezone> diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java index 4de64769b425..f89bd9c43a37 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java @@ -77,6 +77,10 @@ public class RestrictedLockUtilsInternal extends RestrictedLockUtils { private static final String ROLE_DEVICE_LOCK_CONTROLLER = "android.app.role.SYSTEM_FINANCED_DEVICE_CONTROLLER"; + //TODO(b/378931989): Switch to android.app.admin.DevicePolicyIdentifiers.MEMORY_TAGGING_POLICY + //when the appropriate flag is launched. + private static final String MEMORY_TAGGING_POLICY = "memoryTagging"; + /** * @return drawables for displaying with settings that are locked by a device admin. */ @@ -244,14 +248,23 @@ public class RestrictedLockUtilsInternal extends RestrictedLockUtils { */ public static EnforcedAdmin checkIfKeyguardFeaturesDisabled(Context context, int keyguardFeatures, final @UserIdInt int userId) { - final LockSettingCheck check = (dpm, admin, checkUser) -> { - int effectiveFeatures = dpm.getKeyguardDisabledFeatures(admin, checkUser); - if (checkUser != userId) { - effectiveFeatures &= PROFILE_KEYGUARD_FEATURES_AFFECT_OWNER; - } - return (effectiveFeatures & keyguardFeatures) != KEYGUARD_DISABLE_FEATURES_NONE; - }; - if (UserManager.get(context).getUserInfo(userId).isManagedProfile()) { + UserInfo userInfo = UserManager.get(context).getUserInfo(userId); + if (userInfo == null) { + Log.w(LOG_TAG, "User " + userId + " does not exist"); + return null; + } + + final LockSettingCheck check = + (dpm, admin, checkUser) -> { + int effectiveFeatures = dpm.getKeyguardDisabledFeatures(admin, checkUser); + if (checkUser != userId) { + // This case is reached when {@code checkUser} is a managed profile and + // {@code userId} is the parent user. + effectiveFeatures &= PROFILE_KEYGUARD_FEATURES_AFFECT_OWNER; + } + return (effectiveFeatures & keyguardFeatures) != KEYGUARD_DISABLE_FEATURES_NONE; + }; + if (userInfo.isManagedProfile()) { DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE); return findEnforcedAdmin(dpm.getActiveAdminsAsUser(userId), dpm, userId, check); @@ -838,14 +851,13 @@ public class RestrictedLockUtilsInternal extends RestrictedLockUtils { if (dpm.getMtePolicy() == MTE_NOT_CONTROLLED_BY_POLICY) { return null; } - EnforcedAdmin admin = - RestrictedLockUtils.getProfileOrDeviceOwner( - context, context.getUser()); - if (admin != null) { - return admin; + EnforcingAdmin enforcingAdmin = context.getSystemService(DevicePolicyManager.class) + .getEnforcingAdmin(context.getUserId(), MEMORY_TAGGING_POLICY); + if (enforcingAdmin == null) { + Log.w(LOG_TAG, "MTE is controlled by policy but could not find enforcing admin."); } - int profileId = getManagedProfileId(context, context.getUserId()); - return RestrictedLockUtils.getProfileOrDeviceOwner(context, UserHandle.of(profileId)); + + return EnforcedAdmin.createDefaultEnforcedAdminWithRestriction(MEMORY_TAGGING_POLICY); } /** diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index edec2e427315..78b307e7b816 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -25,6 +25,7 @@ import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothCsipSetCoordinator; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHearingAid; +import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; import android.content.Context; @@ -58,6 +59,7 @@ import com.android.settingslib.flags.Flags; import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.widget.AdaptiveOutlineDrawable; +import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -75,6 +77,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; +import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -396,6 +399,9 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } } Log.d(TAG, "Disconnect " + this); + if (Flags.enableLeAudioSharing()) { + removeBroadcastSource(ImmutableSet.of(mDevice)); + } mDevice.disconnect(); } // Disconnect PBAP server in case its connected @@ -608,6 +614,16 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> final BluetoothDevice dev = mDevice; if (dev != null) { mUnpairing = true; + if (Flags.enableLeAudioSharing()) { + Set<BluetoothDevice> devicesToRemoveSource = new HashSet<>(); + devicesToRemoveSource.add(dev); + if (getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { + for (CachedBluetoothDevice member : getMemberDevice()) { + devicesToRemoveSource.add(member.getDevice()); + } + } + removeBroadcastSource(devicesToRemoveSource); + } final boolean successful = dev.removeBond(); if (successful) { releaseLruCache(); @@ -622,6 +638,25 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } } + @WorkerThread + private void removeBroadcastSource(Set<BluetoothDevice> devices) { + if (mProfileManager == null || devices.isEmpty()) return; + LocalBluetoothLeBroadcast broadcast = mProfileManager.getLeAudioBroadcastProfile(); + LocalBluetoothLeBroadcastAssistant assistant = + mProfileManager.getLeAudioBroadcastAssistantProfile(); + if (broadcast != null && assistant != null && broadcast.isEnabled(null)) { + for (BluetoothDevice device : devices) { + for (BluetoothLeBroadcastReceiveState state : assistant.getAllSources(device)) { + if (BluetoothUtils.D) { + Log.d(TAG, "Remove broadcast source " + state.getBroadcastId() + + " from device " + device.getAnonymizedAddress()); + } + assistant.removeSource(device, state.getSourceId()); + } + } + } + } + public int getProfileConnectionState(LocalBluetoothProfile profile) { return profile != null ? profile.getConnectionStatus(mDevice) @@ -1778,9 +1813,55 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> @Nullable private BatteryLevelsInfo getBatteryOfLeAudioDeviceComponents() { - // TODO(b/397847825): Implement the logic to get battery of LE audio device components. - return null; + LeAudioProfile leAudio = mProfileManager.getLeAudioProfile(); + if (leAudio == null) { + return null; + } + int leftBattery = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + int rightBattery = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + int overallBattery = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + + Set<BluetoothDevice> allDevices = + Stream.concat( + mMemberDevices.stream().map(CachedBluetoothDevice::getDevice), + Stream.of(mDevice)) + .collect(Collectors.toSet()); + for (BluetoothDevice device : allDevices) { + int battery = device.getBatteryLevel(); + if (battery <= BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { + continue; + } + int deviceId = leAudio.getAudioLocation(device); + boolean isLeft = (deviceId & LeAudioProfile.LEFT_DEVICE_ID) != 0; + boolean isRight = (deviceId & LeAudioProfile.RIGHT_DEVICE_ID) != 0; + boolean isLeftRight = isLeft && isRight; + // We should expect only one device assign to one side, but if it happens, + // we don't care which one. + if (isLeftRight) { + overallBattery = battery; + } else if (isLeft) { + leftBattery = battery; + } else if (isRight) { + rightBattery = battery; + } + } + overallBattery = getMinBatteryLevels( + Arrays.stream(new int[]{leftBattery, rightBattery, overallBattery})); + + Log.d(TAG, "Acquired battery info from Bluetooth service for le audio device " + + mDevice.getAnonymizedAddress() + + " left battery: " + leftBattery + + " right battery: " + rightBattery + + " overall battery: " + overallBattery); + return overallBattery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN + ? new BatteryLevelsInfo( + leftBattery, + rightBattery, + BluetoothDevice.BATTERY_LEVEL_UNKNOWN, + overallBattery) + : null; } + private CharSequence getTvBatterySummary(int mainBattery, int leftBattery, int rightBattery, int lowBatteryColorRes) { // Since there doesn't seem to be a way to use format strings to add the diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java index e46574cef917..2ec25a17b0ac 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java @@ -45,6 +45,27 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; public class LeAudioProfile implements LocalBluetoothProfile { + public static final int LEFT_DEVICE_ID = BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT + | BluetoothLeAudio.AUDIO_LOCATION_BACK_LEFT + | BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT_OF_CENTER + | BluetoothLeAudio.AUDIO_LOCATION_SIDE_LEFT + | BluetoothLeAudio.AUDIO_LOCATION_TOP_FRONT_LEFT + | BluetoothLeAudio.AUDIO_LOCATION_TOP_BACK_LEFT + | BluetoothLeAudio.AUDIO_LOCATION_TOP_SIDE_LEFT + | BluetoothLeAudio.AUDIO_LOCATION_BOTTOM_FRONT_LEFT + | BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT_WIDE + | BluetoothLeAudio.AUDIO_LOCATION_LEFT_SURROUND; + public static final int RIGHT_DEVICE_ID = BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT + | BluetoothLeAudio.AUDIO_LOCATION_BACK_RIGHT + | BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT_OF_CENTER + | BluetoothLeAudio.AUDIO_LOCATION_SIDE_RIGHT + | BluetoothLeAudio.AUDIO_LOCATION_TOP_FRONT_RIGHT + | BluetoothLeAudio.AUDIO_LOCATION_TOP_BACK_RIGHT + | BluetoothLeAudio.AUDIO_LOCATION_TOP_SIDE_RIGHT + | BluetoothLeAudio.AUDIO_LOCATION_BOTTOM_FRONT_RIGHT + | BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT_WIDE + | BluetoothLeAudio.AUDIO_LOCATION_RIGHT_SURROUND; + private static final String TAG = "LeAudioProfile"; private static boolean DEBUG = true; diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java index b0f379605f5e..3ec4bb80b9cf 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java @@ -107,6 +107,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { public static final int BROADCAST_STATE_UNKNOWN = 0; public static final int BROADCAST_STATE_ON = 1; public static final int BROADCAST_STATE_OFF = 2; + private static final int BROADCAST_NAME_PREFIX_MAX_LENGTH = 27; @Retention(RetentionPolicy.SOURCE) @IntDef( @@ -1116,13 +1117,17 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { private String getDefaultValueOfBroadcastName() { // set the default value; int postfix = ThreadLocalRandom.current().nextInt(DEFAULT_CODE_MIN, DEFAULT_CODE_MAX); - return BluetoothAdapter.getDefaultAdapter().getName() + UNDERLINE + postfix; + String name = BluetoothAdapter.getDefaultAdapter().getName(); + return (name.length() < BROADCAST_NAME_PREFIX_MAX_LENGTH ? name : name.substring(0, + BROADCAST_NAME_PREFIX_MAX_LENGTH)) + UNDERLINE + postfix; } private String getDefaultValueOfProgramInfo() { // set the default value; int postfix = ThreadLocalRandom.current().nextInt(DEFAULT_CODE_MIN, DEFAULT_CODE_MAX); - return BluetoothAdapter.getDefaultAdapter().getName() + UNDERLINE + postfix; + String name = BluetoothAdapter.getDefaultAdapter().getName(); + return (name.length() < BROADCAST_NAME_PREFIX_MAX_LENGTH ? name : name.substring(0, + BROADCAST_NAME_PREFIX_MAX_LENGTH)) + UNDERLINE + postfix; } private byte[] getDefaultValueOfBroadcastCode() { diff --git a/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java b/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java index 985599c952d1..0d5f66794e3b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java @@ -97,6 +97,12 @@ public class DisplayDensityUtils { @Nullable private final int[] mValues; + /** + * The density values before rounding to an integer. + */ + @Nullable + private final float[] mFloatValues; + private final int mDefaultDensity; private final int mCurrentIndex; @@ -124,6 +130,7 @@ public class DisplayDensityUtils { Log.w(LOG_TAG, "Cannot fetch display info for the default display"); mEntries = null; mValues = null; + mFloatValues = null; mDefaultDensity = 0; mCurrentIndex = -1; return; @@ -154,6 +161,7 @@ public class DisplayDensityUtils { Log.w(LOG_TAG, "No display satisfies the predicate"); mEntries = null; mValues = null; + mFloatValues = null; mDefaultDensity = 0; mCurrentIndex = -1; return; @@ -165,6 +173,7 @@ public class DisplayDensityUtils { Log.w(LOG_TAG, "Cannot fetch default density for display " + idOfSmallestDisplay); mEntries = null; mValues = null; + mFloatValues = null; mDefaultDensity = 0; mCurrentIndex = -1; return; @@ -197,18 +206,25 @@ public class DisplayDensityUtils { String[] entries = new String[1 + numSmaller + numLarger]; int[] values = new int[entries.length]; + float[] valuesFloat = new float[entries.length]; int curIndex = 0; if (numSmaller > 0) { final float interval = (1 - minScale) / numSmaller; for (int i = numSmaller - 1; i >= 0; i--) { + // Save the float density value before rounding to be used to set the density ratio + // of overridden density to default density in WM. + final float densityFloat = defaultDensity * (1 - (i + 1) * interval); // Round down to a multiple of 2 by truncating the low bit. - final int density = ((int) (defaultDensity * (1 - (i + 1) * interval))) & ~1; + // LINT.IfChange + final int density = ((int) densityFloat) & ~1; + // LINT.ThenChange(/services/core/java/com/android/server/wm/DisplayContent.java:getBaseDensityFromRatio) if (currentDensity == density) { currentDensityIndex = curIndex; } - entries[curIndex] = res.getString(SUMMARIES_SMALLER[i]); values[curIndex] = density; + valuesFloat[curIndex] = densityFloat; + entries[curIndex] = res.getString(SUMMARIES_SMALLER[i]); curIndex++; } } @@ -217,18 +233,25 @@ public class DisplayDensityUtils { currentDensityIndex = curIndex; } values[curIndex] = defaultDensity; + valuesFloat[curIndex] = (float) defaultDensity; entries[curIndex] = res.getString(SUMMARY_DEFAULT); curIndex++; if (numLarger > 0) { final float interval = (maxScale - 1) / numLarger; for (int i = 0; i < numLarger; i++) { + // Save the float density value before rounding to be used to set the density ratio + // of overridden density to default density in WM. + final float densityFloat = defaultDensity * (1 + (i + 1) * interval); // Round down to a multiple of 2 by truncating the low bit. - final int density = ((int) (defaultDensity * (1 + (i + 1) * interval))) & ~1; + // LINT.IfChange + final int density = ((int) densityFloat) & ~1; + // LINT.ThenChange(/services/core/java/com/android/server/wm/DisplayContent.java:getBaseDensityFromRatio) if (currentDensity == density) { currentDensityIndex = curIndex; } values[curIndex] = density; + valuesFloat[curIndex] = densityFloat; entries[curIndex] = res.getString(SUMMARIES_LARGER[i]); curIndex++; } @@ -244,6 +267,9 @@ public class DisplayDensityUtils { values = Arrays.copyOf(values, newLength); values[curIndex] = currentDensity; + valuesFloat = Arrays.copyOf(valuesFloat, newLength); + valuesFloat[curIndex] = (float) currentDensity; + entries = Arrays.copyOf(entries, newLength); entries[curIndex] = res.getString(SUMMARY_CUSTOM, currentDensity); @@ -254,6 +280,7 @@ public class DisplayDensityUtils { mCurrentIndex = displayIndex; mEntries = entries; mValues = values; + mFloatValues = valuesFloat; } @Nullable @@ -348,7 +375,14 @@ public class DisplayDensityUtils { } final IWindowManager wm = WindowManagerGlobal.getWindowManagerService(); - wm.setForcedDisplayDensityForUser(displayId, mValues[index], userId); + // Only set the ratio for external displays as Settings uses + // ScreenResolutionFragment to handle density update for internal display. + if (info.type == Display.TYPE_EXTERNAL) { + wm.setForcedDisplayDensityRatio(displayId, + mFloatValues[index] / mDefaultDensity, userId); + } else { + wm.setForcedDisplayDensityForUser(displayId, mValues[index], userId); + } } } catch (RemoteException exc) { Log.w(LOG_TAG, "Unable to save forced display density setting"); diff --git a/packages/SettingsLib/src/com/android/settingslib/display/OWNERS b/packages/SettingsLib/src/com/android/settingslib/display/OWNERS new file mode 100644 index 000000000000..aafc2f79611b --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/display/OWNERS @@ -0,0 +1,5 @@ +# Default reviewers for this and subdirectories. +pbdr@google.com +ebrukurnaz@google.com +lihongyu@google.com +wilczynskip@google.com diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt index 2ed437c94439..84d61fc86073 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt @@ -487,7 +487,7 @@ open class WifiUtils { context, lifecycleOwner.lifecycleScope, ssid, - WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW, + WindowManager.LayoutParams.TYPE_APPLICATION, { intent -> context.startActivity(intent) }, onAllowed ) @@ -501,6 +501,7 @@ open class WifiUtils { dialogWindowType: Int, onStartActivity: (intent: Intent) -> Unit, onAllowed: () -> Unit, + onStartAapmActivity: (intent: Intent) -> Unit = onStartActivity, ): Job = coroutineScope.launch { val wifiManager = context.getSystemService(WifiManager::class.java) ?: return@launch @@ -510,9 +511,9 @@ open class WifiUtils { AdvancedProtectionManager.FEATURE_ID_DISALLOW_WEP, AdvancedProtectionManager.SUPPORT_DIALOG_TYPE_BLOCKED_INTERACTION) intent.putExtra(DIALOG_WINDOW_TYPE, dialogWindowType) - onStartActivity(intent) + withContext(Dispatchers.Main) { onStartAapmActivity(intent) } } else if (wifiManager.isWepSupported == true && wifiManager.queryWepAllowed()) { - onAllowed() + withContext(Dispatchers.Main) { onAllowed() } } else { val intent = Intent(Intent.ACTION_MAIN).apply { component = ComponentName( @@ -522,7 +523,7 @@ open class WifiUtils { putExtra(DIALOG_WINDOW_TYPE, dialogWindowType) putExtra(SSID, ssid) }.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - onStartActivity(intent) + withContext(Dispatchers.Main) { onStartActivity(intent) } } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedLockUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedLockUtilsTest.java index 785bcbf5a91c..71e11ba55850 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedLockUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedLockUtilsTest.java @@ -458,6 +458,16 @@ public class RestrictedLockUtilsTest { assertThat(intentCaptor.getValue().getExtra(EXTRA_RESTRICTION)).isNull(); } + /** See b/386971405. Ensure that the code does not crash when the user is not found. */ + @Test + public void checkIfKeyguardFeaturesDisabled_returnsNull_whenUserDoesNotExist() { + when(mUserManager.getUserInfo(mUserId)).thenReturn(null); + assertThat( + RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled( + mContext, KEYGUARD_DISABLE_FINGERPRINT, mUserId)) + .isNull(); + } + private UserInfo setUpUser(int userId, ComponentName[] admins) { UserInfo userInfo = new UserInfo(userId, "primary", 0); when(mUserManager.getUserInfo(userId)).thenReturn(userInfo); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java index b4384b74ccbe..146b66737e83 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java @@ -2357,6 +2357,78 @@ public class CachedBluetoothDeviceTest { Integer.parseInt(TWS_BATTERY_LEFT)); } + @Test + public void getBatteryLevelsInfo_leAudioDeviceWithBattery_returnBatteryLevelsInfo() { + when(mDevice.getMetadata(BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)).thenReturn( + "false".getBytes()); + when(mProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile); + updateProfileStatus(mLeAudioProfile, BluetoothProfile.STATE_CONNECTED); + mCachedDevice.addMemberDevice(mSubCachedDevice); + when(mLeAudioProfile.getAudioLocation(mSubDevice)).thenReturn( + BluetoothLeAudio.AUDIO_LOCATION_BACK_LEFT); + when(mSubDevice.getBatteryLevel()).thenReturn(Integer.parseInt(TWS_BATTERY_LEFT)); + when(mLeAudioProfile.getAudioLocation(mDevice)).thenReturn( + BluetoothLeAudio.AUDIO_LOCATION_SIDE_RIGHT); + when(mDevice.getBatteryLevel()).thenReturn(Integer.parseInt(TWS_BATTERY_RIGHT)); + + BatteryLevelsInfo batteryLevelsInfo = mCachedDevice.getBatteryLevelsInfo(); + + assertThat(batteryLevelsInfo.getLeftBatteryLevel()).isEqualTo( + Integer.parseInt(TWS_BATTERY_LEFT)); + assertThat(batteryLevelsInfo.getRightBatteryLevel()).isEqualTo( + Integer.parseInt(TWS_BATTERY_RIGHT)); + assertThat(batteryLevelsInfo.getCaseBatteryLevel()).isEqualTo( + BluetoothDevice.BATTERY_LEVEL_UNKNOWN); + assertThat(batteryLevelsInfo.getOverallBatteryLevel()).isEqualTo( + Integer.parseInt(TWS_BATTERY_LEFT)); + } + + @Test + public void disconnect_removeBroadcastSource() { + when(mCachedDevice.getGroupId()).thenReturn(1); + when(mSubCachedDevice.getGroupId()).thenReturn(1); + mCachedDevice.addMemberDevice(mSubCachedDevice); + LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class); + LocalBluetoothLeBroadcastAssistant assistant = mock( + LocalBluetoothLeBroadcastAssistant.class); + when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast); + when(mProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant); + when(broadcast.isEnabled(null)).thenReturn(true); + BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); + when(state.getSourceId()).thenReturn(1); + when(assistant.getAllSources(mDevice)).thenReturn(ImmutableList.of(state)); + when(assistant.getAllSources(mSubDevice)).thenReturn(ImmutableList.of(state)); + + mCachedDevice.disconnect(); + verify(assistant).removeSource(mDevice, /* sourceId= */1); + verify(assistant).removeSource(mSubDevice, /* sourceId= */1); + verify(mDevice).disconnect(); + verify(mSubDevice).disconnect(); + } + + @Test + public void unpair_removeBroadcastSource() { + when(mCachedDevice.getGroupId()).thenReturn(1); + when(mSubCachedDevice.getGroupId()).thenReturn(1); + mCachedDevice.addMemberDevice(mSubCachedDevice); + when(mCachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); + LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class); + LocalBluetoothLeBroadcastAssistant assistant = mock( + LocalBluetoothLeBroadcastAssistant.class); + when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast); + when(mProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant); + when(broadcast.isEnabled(null)).thenReturn(true); + BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); + when(state.getSourceId()).thenReturn(1); + when(assistant.getAllSources(mDevice)).thenReturn(ImmutableList.of(state)); + when(assistant.getAllSources(mSubDevice)).thenReturn(ImmutableList.of(state)); + + mCachedDevice.unpair(); + verify(assistant).removeSource(mDevice, /* sourceId= */1); + verify(assistant).removeSource(mSubDevice, /* sourceId= */1); + verify(mDevice).removeBond(); + } + private void updateProfileStatus(LocalBluetoothProfile profile, int status) { doReturn(status).when(profile).getConnectionStatus(mDevice); mCachedDevice.onProfileStateChanged(profile, status); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java index 7f4bdaeac855..83471ae9513e 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java @@ -18,6 +18,7 @@ package com.android.settingslib.widget; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.robolectric.Robolectric.setupActivity; @@ -25,6 +26,8 @@ import static org.robolectric.Shadows.shadowOf; import android.app.Activity; import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Configuration; import android.graphics.ColorFilter; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; @@ -38,24 +41,34 @@ import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.ColorRes; +import androidx.annotation.NonNull; import androidx.preference.PreferenceViewHolder; -import com.android.settingslib.testutils.OverpoweredReflectionHelper; import com.android.settingslib.widget.preference.banner.R; +import com.google.android.material.button.MaterialButton; + import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; import org.robolectric.shadows.ShadowDrawable; import org.robolectric.shadows.ShadowTouchDelegate; import org.robolectric.util.ReflectionHelpers; -@Ignore("b/359066481") +import java.util.List; + @RunWith(RobolectricTestRunner.class) +@Config(shadows = {BannerMessagePreferenceTest.ShadowSettingsThemeHelper.class}) public class BannerMessagePreferenceTest { private Context mContext; @@ -66,14 +79,23 @@ public class BannerMessagePreferenceTest { private boolean mClickListenerCalled = false; private final View.OnClickListener mClickListener = v -> mClickListenerCalled = true; private final int mMinimumTargetSize = - RuntimeEnvironment.application.getResources() - .getDimensionPixelSize(com.android.settingslib.widget.theme.R.dimen.settingslib_preferred_minimum_touch_target); + RuntimeEnvironment.application + .getResources() + .getDimensionPixelSize( + com.android.settingslib.widget.theme.R.dimen + .settingslib_preferred_minimum_touch_target); - private static final int TEST_STRING_RES_ID = - R.string.accessibility_banner_message_dismiss; + private static final int TEST_STRING_RES_ID = R.string.accessibility_banner_message_dismiss; + + @Mock private View mMockBackgroundView; + @Mock private Drawable mMockCardBackground; + @Mock private MaterialButton mMockPositiveBtn; + @Mock private MaterialButton mMockNegativeBtn; @Before public void setUp() { + MockitoAnnotations.initMocks(this); + ShadowSettingsThemeHelper.setExpressiveTheme(false); mContext = RuntimeEnvironment.application; mClickListenerCalled = false; mBannerPreference = new BannerMessagePreference(mContext); @@ -90,6 +112,7 @@ public class BannerMessagePreferenceTest { .isEqualTo("test"); } + @Ignore("b/359066481") @Test public void onBindViewHolder_andOnLayoutView_dismissButtonTouchDelegate_isCorrectSize() { assumeAndroidS(); @@ -155,9 +178,8 @@ public class BannerMessagePreferenceTest { @Test public void onBindViewHolder_whenAtLeastS_whenSubtitleXmlAttribute_shouldSetSubtitle() { assumeAndroidS(); - AttributeSet mAttributeSet = Robolectric.buildAttributeSet() - .addAttribute(R.attr.subtitle, "Test") - .build(); + AttributeSet mAttributeSet = + Robolectric.buildAttributeSet().addAttribute(R.attr.subtitle, "Test").build(); mBannerPreference = new BannerMessagePreference(mContext, mAttributeSet); mBannerPreference.onBindViewHolder(mHolder); @@ -185,8 +207,7 @@ public class BannerMessagePreferenceTest { ImageView mIcon = mRootView.findViewById(R.id.banner_icon); ShadowDrawable shadowDrawable = shadowOf(mIcon.getDrawable()); - assertThat(shadowDrawable.getCreatedFromResId()) - .isEqualTo(R.drawable.settingslib_ic_cross); + assertThat(shadowDrawable.getCreatedFromResId()).isEqualTo(R.drawable.settingslib_ic_cross); } @Test @@ -207,6 +228,7 @@ public class BannerMessagePreferenceTest { Button mPositiveButton = mRootView.findViewById(R.id.banner_positive_btn); assertThat(mPositiveButton.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mPositiveButton.getText()).isEqualTo(mContext.getString(TEST_STRING_RES_ID)); } @@ -218,6 +240,7 @@ public class BannerMessagePreferenceTest { Button mNegativeButton = mRootView.findViewById(R.id.banner_negative_btn); assertThat(mNegativeButton.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mNegativeButton.getText()).isEqualTo(mContext.getString(TEST_STRING_RES_ID)); } @@ -359,8 +382,6 @@ public class BannerMessagePreferenceTest { @Test public void onBindViewHolder_whenAtLeastS_whenAttentionUnset_setsHighTheme() { assumeAndroidS(); - Drawable mCardBackgroundSpy = spy(mRootView.getBackground()); - mRootView.setBackground(mCardBackgroundSpy); mBannerPreference.onBindViewHolder(mHolder); @@ -370,17 +391,15 @@ public class BannerMessagePreferenceTest { .isEqualTo(getColorId(R.color.banner_accent_attention_high)); assertThat(getButtonColor(R.id.banner_negative_btn)) .isEqualTo(getColorId(R.color.banner_accent_attention_high)); - verify(mCardBackgroundSpy).setTint(getColorId(R.color.banner_background_attention_high)); + + verify(mMockCardBackground).setTint(getColorId(R.color.banner_background_attention_high)); } @Test public void onBindViewHolder_whenAtLeastS_whenAttentionHighByXML_setsHighTheme() { assumeAndroidS(); - Drawable mCardBackgroundSpy = spy(mRootView.getBackground()); - mRootView.setBackground(mCardBackgroundSpy); - AttributeSet mAttributeSet = Robolectric.buildAttributeSet() - .addAttribute(R.attr.attentionLevel, "high") - .build(); + AttributeSet mAttributeSet = + Robolectric.buildAttributeSet().addAttribute(R.attr.attentionLevel, "high").build(); mBannerPreference = new BannerMessagePreference(mContext, mAttributeSet); mBannerPreference.onBindViewHolder(mHolder); @@ -391,17 +410,17 @@ public class BannerMessagePreferenceTest { .isEqualTo(getColorId(R.color.banner_accent_attention_high)); assertThat(getButtonColor(R.id.banner_negative_btn)) .isEqualTo(getColorId(R.color.banner_accent_attention_high)); - verify(mCardBackgroundSpy).setTint(getColorId(R.color.banner_background_attention_high)); + + verify(mMockCardBackground).setTint(getColorId(R.color.banner_background_attention_high)); } @Test public void onBindViewHolder_whenAtLeastS_whenAttentionMediumByXML_setsMediumTheme() { assumeAndroidS(); - Drawable mCardBackgroundSpy = spy(mRootView.getBackground()); - mRootView.setBackground(mCardBackgroundSpy); - AttributeSet mAttributeSet = Robolectric.buildAttributeSet() - .addAttribute(R.attr.attentionLevel, "medium") - .build(); + AttributeSet mAttributeSet = + Robolectric.buildAttributeSet() + .addAttribute(R.attr.attentionLevel, "medium") + .build(); mBannerPreference = new BannerMessagePreference(mContext, mAttributeSet); mBannerPreference.onBindViewHolder(mHolder); @@ -412,17 +431,15 @@ public class BannerMessagePreferenceTest { .isEqualTo(getColorId(R.color.banner_accent_attention_medium)); assertThat(getButtonColor(R.id.banner_negative_btn)) .isEqualTo(getColorId(R.color.banner_accent_attention_medium)); - verify(mCardBackgroundSpy).setTint(getColorId(R.color.banner_background_attention_medium)); + + verify(mMockCardBackground).setTint(getColorId(R.color.banner_background_attention_medium)); } @Test public void onBindViewHolder_whenAtLeastS_whenAttentionLowByXML_setsLowTheme() { assumeAndroidS(); - Drawable mCardBackgroundSpy = spy(mRootView.getBackground()); - mRootView.setBackground(mCardBackgroundSpy); - AttributeSet mAttributeSet = Robolectric.buildAttributeSet() - .addAttribute(R.attr.attentionLevel, "low") - .build(); + AttributeSet mAttributeSet = + Robolectric.buildAttributeSet().addAttribute(R.attr.attentionLevel, "low").build(); mBannerPreference = new BannerMessagePreference(mContext, mAttributeSet); mBannerPreference.onBindViewHolder(mHolder); @@ -433,14 +450,13 @@ public class BannerMessagePreferenceTest { .isEqualTo(getColorId(R.color.banner_accent_attention_low)); assertThat(getButtonColor(R.id.banner_negative_btn)) .isEqualTo(getColorId(R.color.banner_accent_attention_low)); - verify(mCardBackgroundSpy).setTint(getColorId(R.color.banner_background_attention_low)); + + verify(mMockCardBackground).setTint(getColorId(R.color.banner_background_attention_low)); } @Test public void setAttentionLevel_whenAtLeastS_whenHighAttention_setsHighTheme() { assumeAndroidS(); - Drawable mCardBackgroundSpy = spy(mRootView.getBackground()); - mRootView.setBackground(mCardBackgroundSpy); mBannerPreference.setAttentionLevel(BannerMessagePreference.AttentionLevel.HIGH); mBannerPreference.onBindViewHolder(mHolder); @@ -451,14 +467,44 @@ public class BannerMessagePreferenceTest { .isEqualTo(getColorId(R.color.banner_accent_attention_high)); assertThat(getButtonColor(R.id.banner_negative_btn)) .isEqualTo(getColorId(R.color.banner_accent_attention_high)); - verify(mCardBackgroundSpy).setTint(getColorId(R.color.banner_background_attention_high)); + + verify(mMockCardBackground).setTint(getColorId(R.color.banner_background_attention_high)); } @Test - public void setAttentionLevel_whenAtLeastS_whenMedAttention_setsMediumTheme() { + public void setAttentionLevel_whenAtLeastS_whenHighAttentionAndExpressiveTheme_setsBtnTheme() { + setExpressiveTheme(true); + assumeAndroidS(); + assertThat(SettingsThemeHelper.isExpressiveTheme(mContext)).isTrue(); + assertThat(SettingsThemeHelper.isExpressiveTheme(mContext)).isTrue(); + doReturn(mMockPositiveBtn).when(mHolder).findViewById(R.id.banner_positive_btn); + doReturn(mMockNegativeBtn).when(mHolder).findViewById(R.id.banner_negative_btn); + assertThat(SettingsThemeHelper.isExpressiveTheme(mContext)).isTrue(); + mBannerPreference.setAttentionLevel(BannerMessagePreference.AttentionLevel.HIGH); + final ArgumentCaptor<ColorStateList> captor = ArgumentCaptor.forClass(ColorStateList.class); + ColorStateList filledBtnBackground = + getColorStateList(R.color.settingslib_banner_button_background_high); + ColorStateList filledBtnTextColor = + getColorStateList(R.color.settingslib_banner_filled_button_content_high); + ColorStateList outlineBtnTextColor = + getColorStateList(R.color.settingslib_banner_outline_button_content); + + mBannerPreference.onBindViewHolder(mHolder); + + verify(mMockPositiveBtn).setBackgroundTintList(captor.capture()); + verify(mMockPositiveBtn).setTextColor(captor.capture()); + verify(mMockNegativeBtn).setStrokeColor(captor.capture()); + verify(mMockNegativeBtn).setTextColor(captor.capture()); + List<ColorStateList> colors = captor.getAllValues(); + assertThat(colors.get(0).getColors()).isEqualTo(filledBtnBackground.getColors()); + assertThat(colors.get(1).getColors()).isEqualTo(filledBtnTextColor.getColors()); + assertThat(colors.get(2).getColors()).isEqualTo(filledBtnBackground.getColors()); + assertThat(colors.get(3).getColors()).isEqualTo(outlineBtnTextColor.getColors()); + } + + @Test + public void setAttentionLevel_whenAtLeastS_whenMedAttention_setsBtnMediumTheme() { assumeAndroidS(); - Drawable mCardBackgroundSpy = spy(mRootView.getBackground()); - mRootView.setBackground(mCardBackgroundSpy); mBannerPreference.setAttentionLevel(BannerMessagePreference.AttentionLevel.MEDIUM); mBannerPreference.onBindViewHolder(mHolder); @@ -469,14 +515,42 @@ public class BannerMessagePreferenceTest { .isEqualTo(getColorId(R.color.banner_accent_attention_medium)); assertThat(getButtonColor(R.id.banner_negative_btn)) .isEqualTo(getColorId(R.color.banner_accent_attention_medium)); - verify(mCardBackgroundSpy).setTint(getColorId(R.color.banner_background_attention_medium)); + + verify(mMockCardBackground).setTint(getColorId(R.color.banner_background_attention_medium)); + } + + @Test + public void setAttentionLevel_whenAtLeastS_whenMedAttentionAndExpressiveTheme_setsBtnTheme() { + setExpressiveTheme(true); + mContext.getResources().getConfiguration().uiMode = Configuration.UI_MODE_NIGHT_NO; + assumeAndroidS(); + doReturn(mMockPositiveBtn).when(mHolder).findViewById(R.id.banner_positive_btn); + doReturn(mMockNegativeBtn).when(mHolder).findViewById(R.id.banner_negative_btn); + mBannerPreference.setAttentionLevel(BannerMessagePreference.AttentionLevel.MEDIUM); + final ArgumentCaptor<ColorStateList> captor = ArgumentCaptor.forClass(ColorStateList.class); + ColorStateList filledBtnBackground = + getColorStateList(R.color.settingslib_banner_button_background_medium); + ColorStateList filledBtnTextColor = + getColorStateList(R.color.settingslib_banner_filled_button_content_medium); + ColorStateList outlineBtnTextColor = + getColorStateList(R.color.settingslib_banner_outline_button_content); + + mBannerPreference.onBindViewHolder(mHolder); + + verify(mMockPositiveBtn).setBackgroundTintList(captor.capture()); + verify(mMockPositiveBtn).setTextColor(captor.capture()); + verify(mMockNegativeBtn).setStrokeColor(captor.capture()); + verify(mMockNegativeBtn).setTextColor(captor.capture()); + List<ColorStateList> colors = captor.getAllValues(); + assertThat(colors.get(0).getColors()).isEqualTo(filledBtnBackground.getColors()); + assertThat(colors.get(1).getColors()).isEqualTo(filledBtnTextColor.getColors()); + assertThat(colors.get(2).getColors()).isEqualTo(filledBtnBackground.getColors()); + assertThat(colors.get(3).getColors()).isEqualTo(outlineBtnTextColor.getColors()); } @Test public void setAttentionLevel_whenAtLeastS_whenLowAttention_setsLowTheme() { assumeAndroidS(); - Drawable mCardBackgroundSpy = spy(mRootView.getBackground()); - mRootView.setBackground(mCardBackgroundSpy); mBannerPreference.setAttentionLevel(BannerMessagePreference.AttentionLevel.LOW); mBannerPreference.onBindViewHolder(mHolder); @@ -487,7 +561,37 @@ public class BannerMessagePreferenceTest { .isEqualTo(getColorId(R.color.banner_accent_attention_low)); assertThat(getButtonColor(R.id.banner_negative_btn)) .isEqualTo(getColorId(R.color.banner_accent_attention_low)); - verify(mCardBackgroundSpy).setTint(getColorId(R.color.banner_background_attention_low)); + verify(mMockCardBackground).setTint(getColorId(R.color.banner_background_attention_low)); + } + + @Test + public void + setAttentionLevel_whenAtLeastS_whenNormalAttentionAndExpressiveTheme_setsBtnTheme() { + setExpressiveTheme(true); + mContext.getResources().getConfiguration().uiMode = Configuration.UI_MODE_NIGHT_NO; + assumeAndroidS(); + doReturn(mMockPositiveBtn).when(mHolder).findViewById(R.id.banner_positive_btn); + doReturn(mMockNegativeBtn).when(mHolder).findViewById(R.id.banner_negative_btn); + mBannerPreference.setAttentionLevel(BannerMessagePreference.AttentionLevel.NORMAL); + final ArgumentCaptor<ColorStateList> captor = ArgumentCaptor.forClass(ColorStateList.class); + ColorStateList filledBtnBackground = + getColorStateList(R.color.settingslib_banner_button_background_normal); + ColorStateList filledBtnTextColor = + getColorStateList(R.color.settingslib_banner_filled_button_content_normal); + ColorStateList outlineBtnStrokeColor = + getColorStateList(R.color.settingslib_banner_outline_button_stroke_normal); + + mBannerPreference.onBindViewHolder(mHolder); + + verify(mMockPositiveBtn).setBackgroundTintList(captor.capture()); + verify(mMockPositiveBtn).setTextColor(captor.capture()); + verify(mMockNegativeBtn).setStrokeColor(captor.capture()); + verify(mMockNegativeBtn).setTextColor(captor.capture()); + List<ColorStateList> colors = captor.getAllValues(); + assertThat(colors.get(0).getColors()).isEqualTo(filledBtnBackground.getColors()); + assertThat(colors.get(1).getColors()).isEqualTo(filledBtnTextColor.getColors()); + assertThat(colors.get(2).getColors()).isEqualTo(outlineBtnStrokeColor.getColors()); + assertThat(colors.get(3).getColors()).isEqualTo(filledBtnBackground.getColors()); } private int getButtonColor(int buttonResId) { @@ -495,6 +599,11 @@ public class BannerMessagePreferenceTest { return mButton.getTextColors().getDefaultColor(); } + private ColorStateList getButtonTextColor(int buttonResId) { + Button mButton = mRootView.findViewById(buttonResId); + return mButton.getTextColors(); + } + private ColorFilter getColorFilter(@ColorRes int colorResId) { return new PorterDuffColorFilter(getColorId(colorResId), PorterDuff.Mode.SRC_IN); } @@ -503,28 +612,57 @@ public class BannerMessagePreferenceTest { return mContext.getResources().getColor(colorResId, mContext.getTheme()); } + private ColorStateList getColorStateList(@ColorRes int colorResId) { + return mContext.getResources().getColorStateList(colorResId, mContext.getTheme()); + } + private void assumeAndroidR() { ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 30); ReflectionHelpers.setStaticField(Build.VERSION.class, "CODENAME", "R"); - OverpoweredReflectionHelper - .setStaticField(BannerMessagePreference.class, "IS_AT_LEAST_S", false); - // Reset view holder to use correct layout. - } - + // Refresh the static final field IS_AT_LEAST_S + mBannerPreference = new BannerMessagePreference(mContext); + setUpViewHolder(); + } private void assumeAndroidS() { ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 31); ReflectionHelpers.setStaticField(Build.VERSION.class, "CODENAME", "S"); - OverpoweredReflectionHelper - .setStaticField(BannerMessagePreference.class, "IS_AT_LEAST_S", true); - // Re-inflate view to update layout. + + // Refresh the static final field IS_AT_LEAST_S + mBannerPreference = new BannerMessagePreference(mContext); setUpViewHolder(); } + private void setExpressiveTheme(boolean isExpressiveTheme) { + ShadowSettingsThemeHelper.setExpressiveTheme(isExpressiveTheme); + assertThat(SettingsThemeHelper.isExpressiveTheme(mContext)).isEqualTo(isExpressiveTheme); + if (isExpressiveTheme) { + doReturn(mContext).when(mMockPositiveBtn).getContext(); + doReturn(mContext).when(mMockNegativeBtn).getContext(); + } + } + private void setUpViewHolder() { mRootView = View.inflate(mContext, mBannerPreference.getLayoutResource(), null /* parent */); - mHolder = PreferenceViewHolder.createInstanceForTests(mRootView); + mHolder = spy(PreferenceViewHolder.createInstanceForTests(mRootView)); + doReturn(mMockBackgroundView).when(mHolder).findViewById(R.id.banner_background); + doReturn(mMockCardBackground).when(mMockBackgroundView).getBackground(); + } + + @Implements(SettingsThemeHelper.class) + public static class ShadowSettingsThemeHelper { + private static boolean sIsExpressiveTheme; + + /** Shadow implementation of isExpressiveTheme */ + @Implementation + public static boolean isExpressiveTheme(@NonNull Context context) { + return sIsExpressiveTheme; + } + + static void setExpressiveTheme(boolean isExpressiveTheme) { + sIsExpressiveTheme = isExpressiveTheme; + } } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java index d8b6707b9118..97473fffaeb1 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java @@ -50,6 +50,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; @@ -211,10 +212,12 @@ public class WifiUtilsTest { WifiUtils.InternetIconInjector iconInjector = new WifiUtils.InternetIconInjector(mContext); for (int level = 0; level <= 4; level++) { + Mockito.reset(mContext); iconInjector.getIcon(false /* noInternet */, level); verify(mContext).getDrawable( WifiUtils.getInternetIconResource(level, false /* noInternet */)); + Mockito.reset(mContext); iconInjector.getIcon(true /* noInternet */, level); verify(mContext).getDrawable( WifiUtils.getInternetIconResource(level, true /* noInternet */)); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java index 527a1f16a84f..5bbfdf7bab81 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java @@ -672,6 +672,7 @@ public class SettingsHelper { public static LocaleList resolveLocales(LocaleList restore, LocaleList current, String[] supportedLocales) { final HashMap<Locale, Locale> allLocales = new HashMap<>(supportedLocales.length); + final HashSet<String> existingLanguageAndScript = new HashSet<>(); for (String supportedLocaleStr : supportedLocales) { final Locale locale = Locale.forLanguageTag(supportedLocaleStr); allLocales.put(toFullLocale(locale), locale); @@ -679,30 +680,26 @@ public class SettingsHelper { // After restoring to reset locales, need to get extensions from restored locale. Get the // first restored locale to check its extension. - final Locale restoredLocale = restore.isEmpty() + final Locale firstRestoredLocale = restore.isEmpty() ? Locale.ROOT : restore.get(0); final ArrayList<Locale> filtered = new ArrayList<>(current.size()); for (int i = 0; i < current.size(); i++) { - Locale locale = copyExtensionToTargetLocale(restoredLocale, current.get(i)); - allLocales.remove(toFullLocale(locale)); - filtered.add(locale); + Locale locale = copyExtensionToTargetLocale(firstRestoredLocale, current.get(i)); + + if (locale != null && existingLanguageAndScript.add(getLanguageAndScript(locale))) { + allLocales.remove(toFullLocale(locale)); + filtered.add(locale); + } } - final HashSet<String> existingLanguageAndScript = new HashSet<>(); for (int i = 0; i < restore.size(); i++) { - final Locale restoredLocaleWithExtension = copyExtensionToTargetLocale(restoredLocale, - getFilteredLocale(restore.get(i), allLocales)); - - if (restoredLocaleWithExtension != null) { - String language = restoredLocaleWithExtension.getLanguage(); - String script = restoredLocaleWithExtension.getScript(); + final Locale restoredLocaleWithExtension = copyExtensionToTargetLocale( + firstRestoredLocale, getFilteredLocale(restore.get(i), allLocales)); - String restoredLanguageAndScript = - script == null ? language : language + "-" + script; - if (existingLanguageAndScript.add(restoredLanguageAndScript)) { - filtered.add(restoredLocaleWithExtension); - } + if (restoredLocaleWithExtension != null && existingLanguageAndScript.add( + getLanguageAndScript(restoredLocaleWithExtension))) { + filtered.add(restoredLocaleWithExtension); } } @@ -713,6 +710,16 @@ public class SettingsHelper { return new LocaleList(filtered.toArray(new Locale[filtered.size()])); } + private static String getLanguageAndScript(Locale locale) { + if (locale == null) { + return ""; + } + + String language = locale.getLanguage(); + String script = locale.getScript(); + return script == null ? language : String.join("-", language, script); + } + private static Locale copyExtensionToTargetLocale(Locale restoredLocale, Locale targetLocale) { if (!restoredLocale.hasExtensions()) { diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 65ede9d804d0..2dcaf088bf6c 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -4080,7 +4080,7 @@ public class SettingsProvider extends ContentProvider { @VisibleForTesting final class UpgradeController { - private static final int SETTINGS_VERSION = 228; + private static final int SETTINGS_VERSION = 229; private final int mUserId; @@ -6336,6 +6336,52 @@ public class SettingsProvider extends ContentProvider { currentVersion = 228; } + // Version 228: Migrate WearOS time settings + if (currentVersion == 228) { + if (getContext() + .getPackageManager() + .hasSystemFeature(PackageManager.FEATURE_WATCH)) { + + SettingsState global = getGlobalSettingsLocked(); + + Setting cwAutoTime = + global.getSettingLocked(Global.Wearable.CLOCKWORK_AUTO_TIME); + if (!cwAutoTime.isNull()) { + boolean phone = + String.valueOf(Global.Wearable.SYNC_TIME_FROM_PHONE) + .equals(cwAutoTime.getValue()); + boolean network = + String.valueOf(Global.Wearable.SYNC_TIME_FROM_NETWORK) + .equals(cwAutoTime.getValue()); + global.insertSettingLocked( + Global.AUTO_TIME, + phone || network ? "1" : "0", + null, + true, + SettingsState.SYSTEM_PACKAGE_NAME); + } + + Setting cwAutoTimeZone = + global.getSettingLocked(Global.Wearable.CLOCKWORK_AUTO_TIME_ZONE); + if (!cwAutoTimeZone.isNull()) { + boolean phone = + String.valueOf(Global.Wearable.SYNC_TIME_ZONE_FROM_PHONE) + .equals(cwAutoTimeZone.getValue()); + boolean network = + String.valueOf(Global.Wearable.SYNC_TIME_ZONE_FROM_NETWORK) + .equals(cwAutoTimeZone.getValue()); + global.insertSettingLocked( + Global.AUTO_TIME_ZONE, + phone || network ? "1" : "0", + null, + true, + SettingsState.SYSTEM_PACKAGE_NAME); + } + } + + currentVersion = 229; + } + // vXXX: Add new settings above this point. if (currentVersion != newVersion) { diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java index 48c778542d66..2160d3164b17 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java @@ -388,11 +388,18 @@ public class SettingsHelperTest { LocaleList.forLanguageTags("zh-Hant-TW"), // current new String[] { "fa-Arab-AF-u-nu-latn", "zh-Hant-TW" })); // supported - assertEquals(LocaleList.forLanguageTags("en-US,zh-Hans-TW"), + assertEquals(LocaleList.forLanguageTags("en-US,zh-Hans-TW,fr-FR"), SettingsHelper.resolveLocales( - LocaleList.forLanguageTags("en-UK,en-GB,zh-Hans-HK"), // restore - LocaleList.forLanguageTags("en-US,zh-Hans-TW"), // current - new String[] { "en-US,zh-Hans-TW,en-UK,en-GB,zh-Hans-HK" })); // supported + // restore + LocaleList.forLanguageTags("en-UK,en-GB,zh-Hans-HK,fr-FR"), + + // current + LocaleList.forLanguageTags("en-US,zh-Hans-TW"), + + // supported + new String[] { + "en-US" , "zh-Hans-TW" , "en-UK", "en-GB", "zh-Hans-HK", "fr-FR" + })); } @Test diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 758ad797f761..ea09d634b82b 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -349,7 +349,7 @@ <uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_COMPUTER" /> <uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION" /> <uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_NEARBY_DEVICE_STREAMING" /> - <uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_SENSOR_DEVICE_STREAMING" /> + <uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_VIRTUAL_DEVICE" /> <uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_WATCH" /> <uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_GLASSES" /> <uses-permission android:name="android.permission.REQUEST_COMPANION_SELF_MANAGED" /> @@ -961,6 +961,7 @@ android:featureFlag="android.security.aapm_api"/> <uses-permission android:name="android.permission.QUERY_ADVANCED_PROTECTION_MODE" android:featureFlag="android.security.aapm_api"/> + <uses-permission android:name="android.permission.MANAGE_DEVICE_POLICY_MTE" /> <!-- Permission required for CTS test - IntrusionDetectionManagerTest --> <uses-permission android:name="android.permission.READ_INTRUSION_DETECTION_STATE" diff --git a/packages/Shell/res/values-en-rCA-watch/strings.xml b/packages/Shell/res/values-en-rCA-watch/strings.xml new file mode 100644 index 000000000000..23f83c48def7 --- /dev/null +++ b/packages/Shell/res/values-en-rCA-watch/strings.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2021 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="bugreport_in_progress_title" msgid="5351751440620800623">"Bug report <xliff:g id="ID">#%1$d</xliff:g> is <xliff:g id="PERCENTAGE">%2$s</xliff:g> complete"</string> +</resources> diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS index ab3fa1b06255..728d206e2786 100644 --- a/packages/SystemUI/OWNERS +++ b/packages/SystemUI/OWNERS @@ -97,7 +97,6 @@ spdonghao@google.com steell@google.com stevenckng@google.com stwu@google.com -syeonlee@google.com sunnygoyal@google.com thiruram@google.com tracyzhou@google.com diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 9da07170430f..ed87a7d35f25 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -512,13 +512,6 @@ flag { } flag { - name: "status_bar_notification_chips" - namespace: "systemui" - description: "Show promoted ongoing notifications as chips in the status bar" - bug: "364653005" -} - -flag { name: "status_bar_popup_chips" namespace: "systemui" description: "Show rich ongoing processes as chips in the status bar" @@ -542,6 +535,16 @@ flag { } flag { + name: "status_bar_window_no_custom_touch" + namespace: "systemui" + description: "Don't have any custom touch handling in StatusBarWindowView" + bug: "391894499" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "icon_refresh_2025" namespace: "systemui" description: "Build time flag for 2025 icon refresh" @@ -559,6 +562,13 @@ flag { } flag { + name: "debug_live_updates_promote_all" + namespace: "systemui" + description: "Promote all notifications to Live Updates (RONs). (For testing/debugging only, do not promote/release!)" + bug: "404838239" +} + +flag { name: "compose_bouncer" namespace: "systemui" description: "Use the new compose bouncer in SystemUI" @@ -590,6 +600,16 @@ flag { } flag { + name: "avalanche_replace_hun_when_critical" + namespace: "systemui" + description: "Fix for replacing a sticky HUN when a critical HUN posted" + bug: "403301297" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "indication_text_a11y_fix" namespace: "systemui" description: "add double shadow to the indication text at the bottom of the lock screen" @@ -1373,13 +1393,6 @@ flag { } flag { - name: "media_controls_posts_optimization" - namespace: "systemui" - description: "Ignore duplicate media notifications posted" - bug: "358645640" -} - -flag { name: "media_controls_umo_inflation_in_background" namespace: "systemui" description: "Inflate UMO in background thread" @@ -1987,9 +2000,9 @@ flag { } flag { - name: "notification_magic_actions_treatment" + name: "notification_animated_actions_treatment" namespace: "systemui" - description: "Special UI treatment for magic actions" + description: "Special UI treatment for animated actions and replys" bug: "383567383" } @@ -2021,13 +2034,6 @@ flag { } flag { - name: "ui_rich_ongoing_force_expanded" - namespace: "systemui" - description: "Force promoted notifications to always be expanded" - bug: "380901479" -} - -flag { name: "permission_helper_ui_rich_ongoing" namespace: "systemui" description: "[RONs] Guards inline permission helper for demoting RONs [Guts/card version]" @@ -2042,13 +2048,6 @@ flag { } flag { - name: "aod_ui_rich_ongoing" - namespace: "systemui" - description: "Show a rich ongoing notification on the always-on display (depends on ui_rich_ongoing)" - bug: "369151941" -} - -flag { name: "stabilize_heads_up_group_v2" namespace: "systemui" description: "Stabilize heads up groups in VisualStabilityCoordinator" @@ -2130,6 +2129,14 @@ flag { } flag { + name: "lockscreen_font" + namespace: "systemui" + description: "Read-only flag for lockscreen font" + bug: "393610165" + is_fixed_read_only: true +} + +flag { name: "always_compose_qs_ui_fragment" namespace: "systemui" description: "Have QQS and QS scenes in the Compose fragment always composed, not just when it should be visible." @@ -2186,3 +2193,11 @@ flag { description: "Use AAD proximity sensor if flag is enabled and sensor is present" bug: "402534470" } + +flag { + name: "enable_underlay" + namespace: "ailabs" + description: "Enable the underlay additional layer" + bug: "403422950" +} + diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt index 440a81fc2152..0680faf5226a 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt @@ -1483,7 +1483,8 @@ constructor( // TODO(b/397646693): remove this exception. val isEligibleForReparenting = controller.isLaunching val viewRoot = controller.transitionContainer.viewRootImpl - val skipReparenting = skipReparentTransaction || viewRoot == null + val skipReparenting = + skipReparentTransaction || !window.leash.isValid || viewRoot == null if (moveTransitionAnimationLayer() && isEligibleForReparenting && !skipReparenting) { reparent = true } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt index c88c4ebb1a8d..6a620b3b9c34 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt @@ -35,6 +35,7 @@ import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALW import com.android.app.animation.Interpolators import com.android.internal.jank.Cuj.CujType import com.android.internal.jank.InteractionJankMonitor +import com.android.systemui.Flags import com.android.systemui.util.maybeForceFullscreen import com.android.systemui.util.registerAnimationOnBackInvoked import java.util.concurrent.Executor @@ -932,26 +933,29 @@ private class AnimatedDialog( } override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { - // onLaunchAnimationEnd is called by an Animator at the end of the animation, - // on a Choreographer animation tick. The following calls will move the animated - // content from the dialog overlay back to its original position, and this - // change must be reflected in the next frame given that we then sync the next - // frame of both the content and dialog ViewRoots. However, in case that content - // is rendered by Compose, whose compositions are also scheduled on a - // Choreographer frame, any state change made *right now* won't be reflected in - // the next frame given that a Choreographer frame can't schedule another and - // have it happen in the same frame. So we post the forwarded calls to - // [Controller.onLaunchAnimationEnd], leaving this Choreographer frame, ensuring - // that the move of the content back to its original window will be reflected in - // the next frame right after [onLaunchAnimationEnd] is called. - // - // TODO(b/330672236): Move this to TransitionAnimator. - dialog.context.mainExecutor.execute { + val onEnd = { startController.onTransitionAnimationEnd(isExpandingFullyAbove) endController.onTransitionAnimationEnd(isExpandingFullyAbove) - onLaunchAnimationEnd() } + if (Flags.sceneContainer()) { + onEnd() + } else { + // onLaunchAnimationEnd is called by an Animator at the end of the + // animation, on a Choreographer animation tick. The following calls will + // move the animated content from the dialog overlay back to its original + // position, and this change must be reflected in the next frame given that + // we then sync the next frame of both the content and dialog ViewRoots. + // However, in case that content is rendered by Compose, whose compositions + // are also scheduled on a Choreographer frame, any state change made *right + // now* won't be reflected in the next frame given that a Choreographer + // frame can't schedule another and have it happen in the same frame. So we + // post the forwarded calls to [Controller.onLaunchAnimationEnd], leaving + // this Choreographer frame, ensuring that the move of the content back to + // its original window will be reflected in the next frame right after + // [onLaunchAnimationEnd] is called. + dialog.context.mainExecutor.execute { onEnd() } + } } override fun onTransitionAnimationProgress( diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java index 060f0c94732d..041ccb567146 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java @@ -199,7 +199,11 @@ public abstract class RemoteAnimationRunnerCompat extends IRemoteAnimationRunner info.releaseAllSurfaces(); // Make sure that the transition leashes created are not leaked. for (SurfaceControl leash : leashMap.values()) { - finishTransaction.reparent(leash, null); + try { + finishTransaction.reparent(leash, null); + } catch (Exception e) { + Log.e(TAG, "Failed to reparent leash", e); + } } // Don't release here since launcher might still be using them. Instead // let launcher release them (eg. via RemoteAnimationTargets) diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt index a4a96d19e8bb..8886b9e5e275 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt @@ -39,6 +39,7 @@ import com.android.app.animation.Interpolators.LINEAR import com.android.internal.annotations.VisibleForTesting import com.android.internal.dynamicanimation.animation.SpringAnimation import com.android.internal.dynamicanimation.animation.SpringForce +import com.android.systemui.Flags import com.android.systemui.Flags.moveTransitionAnimationLayer import com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary import com.android.systemui.shared.Flags.returnAnimationFrameworkLongLived @@ -1014,11 +1015,7 @@ class TransitionAnimator( openingWindowSyncViewOverlay?.remove(windowBackgroundLayer) } } - // TODO(b/330672236): Post this to the main thread for launches as well, so that they do not - // flicker with Flexiglass enabled. - if (controller.isLaunching) { - onEnd() - } else { + if (Flags.sceneContainer() || !controller.isLaunching) { // onAnimationEnd is called at the end of the animation, on a Choreographer animation // tick. During dialog launches, the following calls will move the animated content from // the dialog overlay back to its original position, and this change must be reflected @@ -1032,6 +1029,8 @@ class TransitionAnimator( // Choreographer frame, ensuring that any state change applied by // onTransitionAnimationEnd() will be reflected in the same frame. mainExecutor.execute { onEnd() } + } else { + onEnd() } } diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/ContentOverscrollEffect.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/ContentOverscrollEffect.kt index cb713ece12a5..5ed72c7d94a2 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/ContentOverscrollEffect.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/ContentOverscrollEffect.kt @@ -55,7 +55,12 @@ open class BaseContentOverscrollEffect( get() = animatable.value override val isInProgress: Boolean - get() = overscrollDistance != 0f + /** + * We need both checks, because [overscrollDistance] can be + * - zero while it is already being animated, if the animation starts from 0 + * - greater than zero without an animation, if the content is still being dragged + */ + get() = overscrollDistance != 0f || animatable.isRunning override fun applyToScroll( delta: Offset, diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt index 07a571b94ce4..c411d272cb22 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.unit.dp import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope +/** Returns a [remember]ed [OffsetOverscrollEffect]. */ @Composable @OptIn(ExperimentalMaterial3ExpressiveApi::class) fun rememberOffsetOverscrollEffect( @@ -63,7 +64,10 @@ data class OffsetOverscrollEffectFactory( private val animationSpec: AnimationSpec<Float>, ) : OverscrollFactory { override fun createOverscrollEffect(): OverscrollEffect { - return OffsetOverscrollEffect(animationScope, animationSpec) + return OffsetOverscrollEffect( + animationScope = animationScope, + animationSpec = animationSpec, + ) } } @@ -80,11 +84,11 @@ class OffsetOverscrollEffect(animationScope: CoroutineScope, animationSpec: Anim return layout(placeable.width, placeable.height) { val offsetPx = computeOffset(density = this@measure, overscrollDistance) if (offsetPx != 0) { - placeable.placeRelativeWithLayer( + placeable.placeWithLayer( with(requireConverter()) { offsetPx.toIntOffset() } ) } else { - placeable.placeRelative(0, 0) + placeable.place(0, 0) } } } diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/effect/OffsetOverscrollEffectTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/effect/OffsetOverscrollEffectTest.kt index e7c47fb56130..8a1fa3724d15 100644 --- a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/effect/OffsetOverscrollEffectTest.kt +++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/effect/OffsetOverscrollEffectTest.kt @@ -16,12 +16,17 @@ package com.android.compose.gesture.effect +import androidx.compose.foundation.OverscrollEffect import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.ScrollableState import androidx.compose.foundation.gestures.rememberScrollableState import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.foundation.overscroll +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.platform.LocalDensity @@ -32,11 +37,14 @@ import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeWithVelocity import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat import kotlin.properties.Delegates +import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -47,7 +55,13 @@ class OffsetOverscrollEffectTest { private val BOX_TAG = "box" - private data class LayoutInfo(val layoutSize: Dp, val touchSlop: Float, val density: Density) { + private data class LayoutInfo( + val layoutSize: Dp, + val touchSlop: Float, + val density: Density, + val scrollableState: ScrollableState, + val overscrollEffect: OverscrollEffect, + ) { fun expectedOffset(currentOffset: Dp): Dp { return with(density) { OffsetOverscrollEffect.computeOffset(this, currentOffset.toPx()).toDp() @@ -55,22 +69,29 @@ class OffsetOverscrollEffectTest { } } - private fun setupOverscrollableBox(scrollableOrientation: Orientation): LayoutInfo { + private fun setupOverscrollableBox( + scrollableOrientation: Orientation, + canScroll: () -> Boolean, + ): LayoutInfo { val layoutSize: Dp = 200.dp var touchSlop: Float by Delegates.notNull() // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is // detected as a drag event. lateinit var density: Density + lateinit var scrollableState: ScrollableState + lateinit var overscrollEffect: OverscrollEffect + rule.setContent { density = LocalDensity.current touchSlop = LocalViewConfiguration.current.touchSlop - val overscrollEffect = rememberOffsetOverscrollEffect() + scrollableState = rememberScrollableState { if (canScroll()) it else 0f } + overscrollEffect = rememberOffsetOverscrollEffect() Box( Modifier.overscroll(overscrollEffect) // A scrollable that does not consume the scroll gesture. .scrollable( - state = rememberScrollableState { 0f }, + state = scrollableState, orientation = scrollableOrientation, overscrollEffect = overscrollEffect, ) @@ -78,12 +99,16 @@ class OffsetOverscrollEffectTest { .testTag(BOX_TAG) ) } - return LayoutInfo(layoutSize, touchSlop, density) + return LayoutInfo(layoutSize, touchSlop, density, scrollableState, overscrollEffect) } @Test fun applyVerticalOffset_duringVerticalOverscroll() { - val info = setupOverscrollableBox(scrollableOrientation = Orientation.Vertical) + val info = + setupOverscrollableBox( + scrollableOrientation = Orientation.Vertical, + canScroll = { false }, + ) rule.onNodeWithTag(BOX_TAG).assertTopPositionInRootIsEqualTo(0.dp) @@ -99,7 +124,11 @@ class OffsetOverscrollEffectTest { @Test fun applyNoOffset_duringHorizontalOverscroll() { - val info = setupOverscrollableBox(scrollableOrientation = Orientation.Vertical) + val info = + setupOverscrollableBox( + scrollableOrientation = Orientation.Vertical, + canScroll = { false }, + ) rule.onNodeWithTag(BOX_TAG).assertTopPositionInRootIsEqualTo(0.dp) @@ -113,7 +142,11 @@ class OffsetOverscrollEffectTest { @Test fun backToZero_afterOverscroll() { - val info = setupOverscrollableBox(scrollableOrientation = Orientation.Vertical) + val info = + setupOverscrollableBox( + scrollableOrientation = Orientation.Vertical, + canScroll = { false }, + ) rule.onRoot().performTouchInput { down(center) @@ -131,7 +164,11 @@ class OffsetOverscrollEffectTest { @Test fun offsetOverscroll_followTheTouchPointer() { - val info = setupOverscrollableBox(scrollableOrientation = Orientation.Vertical) + val info = + setupOverscrollableBox( + scrollableOrientation = Orientation.Vertical, + canScroll = { false }, + ) // First gesture, drag down. rule.onRoot().performTouchInput { @@ -165,4 +202,130 @@ class OffsetOverscrollEffectTest { .onNodeWithTag(BOX_TAG) .assertTopPositionInRootIsEqualTo(info.expectedOffset(-info.layoutSize)) } + + @Test + fun isScrollInProgress_overscroll() = runTest { + val info = + setupOverscrollableBox( + scrollableOrientation = Orientation.Vertical, + canScroll = { false }, + ) + + // Start a swipe gesture, and swipe down to start an overscroll. + rule.onRoot().performTouchInput { + down(center) + moveBy(Offset(0f, info.touchSlop + info.layoutSize.toPx() / 2)) + } + + assertThat(info.scrollableState.isScrollInProgress).isTrue() + assertThat(info.overscrollEffect.isInProgress).isTrue() + + // Finish the swipe gesture. + rule.onRoot().performTouchInput { up() } + + assertThat(info.scrollableState.isScrollInProgress).isFalse() + assertThat(info.overscrollEffect.isInProgress).isTrue() + + // Wait until the overscroll returns to idle. + rule.awaitIdle() + + assertThat(info.scrollableState.isScrollInProgress).isFalse() + assertThat(info.overscrollEffect.isInProgress).isFalse() + } + + @Test + fun isScrollInProgress_scroll() = runTest { + val info = + setupOverscrollableBox( + scrollableOrientation = Orientation.Vertical, + canScroll = { true }, + ) + + rule.onNodeWithTag(BOX_TAG).assertTopPositionInRootIsEqualTo(0.dp) + + // Start a swipe gesture, and swipe down to scroll. + rule.onRoot().performTouchInput { + down(center) + moveBy(Offset(0f, info.touchSlop + info.layoutSize.toPx() / 2)) + } + + assertThat(info.scrollableState.isScrollInProgress).isTrue() + assertThat(info.overscrollEffect.isInProgress).isFalse() + + // Finish the swipe gesture. + rule.onRoot().performTouchInput { up() } + + assertThat(info.scrollableState.isScrollInProgress).isFalse() + assertThat(info.overscrollEffect.isInProgress).isTrue() + + // Wait until the overscroll returns to idle. + rule.awaitIdle() + + assertThat(info.scrollableState.isScrollInProgress).isFalse() + assertThat(info.overscrollEffect.isInProgress).isFalse() + } + + @Test + fun isScrollInProgress_flingToScroll() = runTest { + val info = + setupOverscrollableBox( + scrollableOrientation = Orientation.Vertical, + canScroll = { true }, + ) + + rule.onNodeWithTag(BOX_TAG).assertTopPositionInRootIsEqualTo(0.dp) + + // Swipe down and leave some velocity to start a fling. + rule.onRoot().performTouchInput { + swipeWithVelocity( + Offset.Zero, + Offset(0f, info.touchSlop + info.layoutSize.toPx() / 2), + endVelocity = 100f, + ) + } + + assertThat(info.scrollableState.isScrollInProgress).isTrue() + assertThat(info.overscrollEffect.isInProgress).isFalse() + + // Wait until the fling is finished. + rule.awaitIdle() + + assertThat(info.scrollableState.isScrollInProgress).isFalse() + assertThat(info.overscrollEffect.isInProgress).isFalse() + } + + @Test + fun isScrollInProgress_flingToOverscroll() = runTest { + // Start with a scrollable state. + var canScroll by mutableStateOf(true) + val info = + setupOverscrollableBox(scrollableOrientation = Orientation.Vertical) { canScroll } + + rule.onNodeWithTag(BOX_TAG).assertTopPositionInRootIsEqualTo(0.dp) + + // Swipe down and leave some velocity to start a fling. + rule.onRoot().performTouchInput { + swipeWithVelocity( + Offset.Zero, + Offset(0f, info.touchSlop + info.layoutSize.toPx() / 2), + endVelocity = 100f, + ) + } + + assertThat(info.scrollableState.isScrollInProgress).isTrue() + assertThat(info.overscrollEffect.isInProgress).isFalse() + + // The fling reaches the end of the scrollable region, and an overscroll starts. + canScroll = false + rule.mainClock.advanceTimeUntil { !info.scrollableState.isScrollInProgress } + + assertThat(info.scrollableState.isScrollInProgress).isFalse() + assertThat(info.overscrollEffect.isInProgress).isTrue() + + // Wait until the overscroll returns to idle. + rule.awaitIdle() + + assertThat(info.scrollableState.isScrollInProgress).isFalse() + assertThat(info.overscrollEffect.isInProgress).isFalse() + } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index 2b8fe39c4870..16a8f987ba83 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -1,5 +1,6 @@ package com.android.systemui.communal.ui.compose +import android.content.res.Configuration import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat @@ -15,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -26,6 +28,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.disabled @@ -55,6 +58,7 @@ import com.android.systemui.communal.ui.compose.Dimensions.Companion.SlideOffset import com.android.systemui.communal.ui.compose.extensions.allowGestures import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.util.CommunalColors +import com.android.systemui.keyguard.domain.interactor.FromGlanceableHubTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor.Companion.TO_GONE_DURATION import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.scene.ui.composable.SceneTransitionLayoutDataSource @@ -98,6 +102,17 @@ val sceneTransitionsV2 = transitions { spec = tween(durationMillis = TO_GONE_DURATION.toInt(DurationUnit.MILLISECONDS)) fade(AllElements) } + to(CommunalScenes.Blank, key = CommunalTransitionKeys.SwipeInLandscape) { + spec = tween(durationMillis = TO_LOCKSCREEN_DURATION.toInt(DurationUnit.MILLISECONDS)) + translate(Communal.Elements.Grid, Edge.End) + timestampRange(endMillis = 167) { + fade(Communal.Elements.Grid) + fade(Communal.Elements.IndicationArea) + fade(Communal.Elements.LockIcon) + fade(Communal.Elements.StatusBar) + } + timestampRange(startMillis = 167, endMillis = 500) { fade(Communal.Elements.Scrim) } + } to(CommunalScenes.Blank, key = CommunalTransitionKeys.Swipe) { spec = tween(durationMillis = TransitionDuration.TO_GLANCEABLE_HUB_DURATION_MS) translate(Communal.Elements.Grid, Edge.End) @@ -214,6 +229,9 @@ fun CommunalContainer( val blurRadius = with(LocalDensity.current) { viewModel.blurRadiusPx.toDp() } + val swipeFromHubInLandscape by + viewModel.swipeFromHubInLandscape.collectAsStateWithLifecycle(false) + SceneTransitionLayout( state = state, modifier = modifier.fillMaxSize().thenIf(isUiBlurred) { Modifier.blur(blurRadius) }, @@ -241,7 +259,14 @@ fun CommunalContainer( userActions = mapOf( Swipe.End to - UserActionResult(CommunalScenes.Blank, CommunalTransitionKeys.Swipe) + UserActionResult( + CommunalScenes.Blank, + if (swipeFromHubInLandscape) { + CommunalTransitionKeys.SwipeInLandscape + } else { + CommunalTransitionKeys.Swipe + }, + ) ), ) { CommunalScene( @@ -258,6 +283,20 @@ fun CommunalContainer( Box(modifier = Modifier.fillMaxSize().allowGestures(touchesAllowed)) } +/** Listens to orientation changes on communal scene and reset when scene is disposed. */ +@Composable +fun ObserveOrientationChange(viewModel: CommunalViewModel) { + val configuration = LocalConfiguration.current + + LaunchedEffect(configuration.orientation) { + viewModel.onOrientationChange(configuration.orientation) + } + + DisposableEffect(Unit) { + onDispose { viewModel.onOrientationChange(Configuration.ORIENTATION_UNDEFINED) } + } +} + /** Scene containing the glanceable hub UI. */ @Composable fun ContentScope.CommunalScene( @@ -269,6 +308,8 @@ fun ContentScope.CommunalScene( ) { val isFocusable by viewModel.isFocusable.collectAsStateWithLifecycle(initialValue = false) + // Observe screen rotation while Communal Scene is active. + ObserveOrientationChange(viewModel) Box( modifier = Modifier.element(Communal.Elements.Scrim) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt index 0181928317e1..1a0fb0afd385 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal.ui.compose +import android.content.res.Configuration import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -23,6 +24,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntRect @@ -66,6 +68,7 @@ constructor( @Composable fun ContentScope.Content(modifier: Modifier = Modifier) { CommunalTouchableSurface(viewModel = viewModel, modifier = modifier) { + val orientation = LocalConfiguration.current.orientation Layout( modifier = Modifier.fillMaxSize(), content = { @@ -150,13 +153,29 @@ constructor( val bottomAreaPlaceable = bottomAreaMeasurable.measure(noMinConstraints) + val communalGridMaxHeight: Int + val communalGridPositionY: Int + if (Flags.communalResponsiveGrid()) { + val communalGridVerticalMargin = constraints.maxHeight - lockIconBounds.top + // Bias the widgets up by a small offset for visual balance in landscape + // orientation + val verticalOffset = + (if (orientation == Configuration.ORIENTATION_LANDSCAPE) (-3).dp else 0.dp) + .roundToPx() + // Use even top and bottom margin for grid to be centered in maxHeight (window) + communalGridMaxHeight = constraints.maxHeight - communalGridVerticalMargin * 2 + communalGridPositionY = communalGridVerticalMargin + verticalOffset + } else { + communalGridMaxHeight = lockIconBounds.top + communalGridPositionY = 0 + } val communalGridPlaceable = communalGridMeasurable.measure( - noMinConstraints.copy(maxHeight = lockIconBounds.top) + noMinConstraints.copy(maxHeight = communalGridMaxHeight) ) layout(constraints.maxWidth, constraints.maxHeight) { - communalGridPlaceable.place(x = 0, y = 0) + communalGridPlaceable.place(x = 0, y = communalGridPositionY) lockIconPlaceable.place(x = lockIconBounds.left, y = lockIconBounds.top) val bottomAreaTop = constraints.maxHeight - bottomAreaPlaceable.height 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 835dd7aa9f24..ad2a32e030bb 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 @@ -54,7 +54,10 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -76,8 +79,8 @@ import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.TextAutoSize import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.text.TextAutoSize import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add @@ -99,6 +102,8 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -174,6 +179,7 @@ import com.android.compose.animation.Easings.Emphasized import com.android.compose.animation.scene.ContentScope import com.android.compose.modifiers.thenIf import com.android.compose.ui.graphics.painter.rememberDrawablePainter +import com.android.compose.windowsizeclass.LocalWindowSizeClass import com.android.internal.R.dimen.system_app_widget_background_radius import com.android.systemui.Flags import com.android.systemui.Flags.communalResponsiveGrid @@ -254,6 +260,7 @@ fun CommunalHub( val windowMetrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(context) val screenWidth = windowMetrics.bounds.width() val layoutDirection = LocalLayoutDirection.current + if (viewModel.isEditMode) { ObserveNewWidgetAddedEffect(communalContent, gridState, viewModel) } else { @@ -757,11 +764,33 @@ fun calculateWidgetSize( } @Composable +private fun horizontalPaddingWithInsets(padding: Dp): Dp { + val orientation = LocalConfiguration.current.orientation + val displayCutoutPaddings = WindowInsets.displayCutout.asPaddingValues() + val horizontalDisplayCutoutPadding = + remember(orientation, displayCutoutPaddings) { + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + maxOf( + // Top in portrait becomes startPadding (or endPadding) in landscape + displayCutoutPaddings.calculateTopPadding(), + // Bottom in portrait becomes endPadding (or startPadding) in landscape + displayCutoutPaddings.calculateBottomPadding(), + ) + } else { + 0.dp + } + } + return padding + horizontalDisplayCutoutPadding +} + +@Composable private fun HorizontalGridWrapper( minContentPadding: PaddingValues, gridState: LazyGridState, dragDropState: GridDragDropState?, setContentOffset: (offset: Offset) -> Unit, + minHorizontalArrangement: Dp, + minVerticalArrangement: Dp, modifier: Modifier = Modifier, content: LazyGridScope.(sizeInfo: SizeInfo?) -> Unit, ) { @@ -775,8 +804,8 @@ private fun HorizontalGridWrapper( state = gridState, flingBehavior = flingBehavior, minContentPadding = minContentPadding, - minHorizontalArrangement = Dimensions.ItemSpacing, - minVerticalArrangement = Dimensions.ItemSpacing, + minHorizontalArrangement = minHorizontalArrangement, + minVerticalArrangement = minVerticalArrangement, setContentOffset = setContentOffset, // Temporarily disable user gesture scrolling while dragging a widget to prevent // conflicts between the drag and scroll gestures. Programmatic scrolling remains @@ -833,6 +862,7 @@ private fun BoxScope.CommunalHubLazyGrid( Modifier.align(Alignment.TopStart).onGloballyPositioned { setGridCoordinates(it) } var list = communalContent var dragDropState: GridDragDropState? = null + var arrangementSpacing = Dimensions.ItemSpacing if (viewModel.isEditMode && viewModel is CommunalEditModeViewModel) { list = contentListState.list // for drag & drop operations within the communal hub grid @@ -866,6 +896,9 @@ private fun BoxScope.CommunalHubLazyGrid( Box(Modifier.fillMaxSize().dragAndDropTarget(dragAndDropTargetState)) {} } else if (communalResponsiveGrid()) { gridModifier = gridModifier.fillMaxSize() + if (isCompactWindow()) { + arrangementSpacing = Dimensions.ItemSpacingCompact + } } else { gridModifier = gridModifier.height(hubDimensions.GridHeight) } @@ -875,6 +908,8 @@ private fun BoxScope.CommunalHubLazyGrid( gridState = gridState, dragDropState = dragDropState, minContentPadding = minContentPadding, + minHorizontalArrangement = arrangementSpacing, + minVerticalArrangement = arrangementSpacing, setContentOffset = setContentOffset, ) { sizeInfo -> /** Override spans based on the responsive grid size */ @@ -1839,11 +1874,21 @@ private fun nonScalableTextSize(sizeInDp: Dp) = with(LocalDensity.current) { siz @Composable private fun gridContentPadding(isEditMode: Boolean, toolbarSize: IntSize?): PaddingValues { if (!isEditMode || toolbarSize == null) { - return PaddingValues( - start = Dimensions.ItemSpacing, - end = Dimensions.ItemSpacing, - top = hubDimensions.GridTopSpacing, - ) + return if (communalResponsiveGrid()) { + val horizontalPaddings: Dp = + if (isCompactWindow()) { + horizontalPaddingWithInsets(Dimensions.ItemSpacingCompact) + } else { + Dimensions.ItemSpacing + } + PaddingValues(start = horizontalPaddings, end = horizontalPaddings) + } else { + PaddingValues( + start = Dimensions.ItemSpacing, + end = Dimensions.ItemSpacing, + top = hubDimensions.GridTopSpacing, + ) + } } val context = LocalContext.current val density = LocalDensity.current @@ -1870,6 +1915,16 @@ private fun gridContentPadding(isEditMode: Boolean, toolbarSize: IntSize?): Padd } } +/** Compact size in landscape or portrait */ +@Composable +fun isCompactWindow(): Boolean { + val windowSizeClass = LocalWindowSizeClass.current + return remember(windowSizeClass) { + windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact || + windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact + } +} + private fun CommunalContentSize.FixedSize.dp(): Dp { return when (this) { CommunalContentSize.FixedSize.FULL -> Dimensions.CardHeightFull @@ -1911,6 +1966,9 @@ class Dimensions(val context: Context, val config: Configuration) { val CardHeightFull get() = 530.adjustedDp + val ItemSpacingCompact + get() = 12.adjustedDp + val ItemSpacing get() = if (communalResponsiveGrid()) 32.adjustedDp else 50.adjustedDp diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt index 5fac6863e931..3e1252babee4 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt @@ -48,6 +48,7 @@ import com.android.systemui.log.dagger.LongPressTouchLog import com.android.systemui.plugins.FalsingManager import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper +import com.google.android.msdl.domain.MSDLPlayer import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -66,6 +67,7 @@ constructor( private val deviceEntryBackgroundViewModel: Lazy<DeviceEntryBackgroundViewModel>, private val falsingManager: Lazy<FalsingManager>, private val vibratorHelper: Lazy<VibratorHelper>, + private val msdlPlayer: Lazy<MSDLPlayer>, @LongPressTouchLog private val logBuffer: LogBuffer, ) { @Composable @@ -90,6 +92,7 @@ constructor( deviceEntryBackgroundViewModel.get(), falsingManager.get(), vibratorHelper.get(), + msdlPlayer.get(), overrideColor, ) } 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 d903c3d16fdb..748c3b89649a 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 @@ -53,7 +53,7 @@ import com.android.systemui.statusbar.notification.icon.ui.viewbinder.Notificati import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel import com.android.systemui.statusbar.notification.promoted.AODPromotedNotification -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi import com.android.systemui.statusbar.notification.promoted.ui.viewmodel.AODPromotedNotificationViewModel import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView @@ -111,7 +111,7 @@ constructor( @Composable fun AodPromotedNotificationArea(modifier: Modifier = Modifier) { - if (!PromotedNotificationUiAod.isEnabled) { + if (!PromotedNotificationUi.isEnabled) { return } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt deleted file mode 100644 index e1ee59ba0626..000000000000 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.notifications.ui.composable - -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.tween -import androidx.compose.foundation.gestures.FlingBehavior -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.ScrollableDefaults -import androidx.compose.foundation.layout.offset -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.Velocity -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastCoerceAtLeast -import com.android.compose.nestedscroll.OnStopScope -import com.android.compose.nestedscroll.PriorityNestedScrollConnection -import com.android.compose.nestedscroll.ScrollController -import kotlin.math.max -import kotlin.math.roundToInt -import kotlin.math.tanh -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -@Composable -fun Modifier.stackVerticalOverscroll( - coroutineScope: CoroutineScope, - canScrollForward: () -> Boolean, -): Modifier { - val screenHeight = - with(LocalDensity.current) { LocalConfiguration.current.screenHeightDp.dp.toPx() } - val overscrollOffset = remember { Animatable(0f) } - val flingBehavior = ScrollableDefaults.flingBehavior() - val stackNestedScrollConnection = - remember(flingBehavior) { - NotificationStackNestedScrollConnection( - stackOffset = { overscrollOffset.value }, - canScrollForward = canScrollForward, - onScroll = { offsetAvailable -> - coroutineScope.launch { - val maxProgress = screenHeight * 0.2f - val tilt = 3f - var offset = - overscrollOffset.value + - maxProgress * tanh(x = offsetAvailable / (maxProgress * tilt)) - offset = max(offset, -1f * maxProgress) - overscrollOffset.snapTo(offset) - } - }, - onStop = { velocityAvailable -> - coroutineScope.launch { - overscrollOffset.animateTo( - targetValue = 0f, - initialVelocity = velocityAvailable, - animationSpec = tween(), - ) - } - }, - flingBehavior = flingBehavior, - ) - } - - return this.then( - Modifier.nestedScroll( - remember { - object : NestedScrollConnection { - override suspend fun onPostFling( - consumed: Velocity, - available: Velocity, - ): Velocity { - return if (available.y < 0f && !canScrollForward()) { - overscrollOffset.animateTo( - targetValue = 0f, - initialVelocity = available.y, - animationSpec = tween(), - ) - available - } else { - Velocity.Zero - } - } - } - } - ) - .nestedScroll(stackNestedScrollConnection) - .offset { IntOffset(x = 0, y = overscrollOffset.value.roundToInt()) } - ) -} - -fun NotificationStackNestedScrollConnection( - stackOffset: () -> Float, - canScrollForward: () -> Boolean, - onStart: (Float) -> Unit = {}, - onScroll: (Float) -> Unit, - onStop: (Float) -> Unit = {}, - flingBehavior: FlingBehavior, -): PriorityNestedScrollConnection { - return PriorityNestedScrollConnection( - orientation = Orientation.Vertical, - canStartPreScroll = { _, _, _ -> false }, - canStartPostScroll = { offsetAvailable, offsetBeforeStart, _ -> - offsetAvailable < 0f && offsetBeforeStart < 0f && !canScrollForward() - }, - onStart = { firstScroll -> - onStart(firstScroll) - object : ScrollController { - override fun onScroll(deltaScroll: Float, source: NestedScrollSource): Float { - val minOffset = 0f - val consumed = deltaScroll.fastCoerceAtLeast(minOffset - stackOffset()) - if (consumed != 0f) { - onScroll(consumed) - } - return consumed - } - - override suspend fun OnStopScope.onStop(initialVelocity: Float): Float { - val consumedByScroll = flingToScroll(initialVelocity, flingBehavior) - onStop(initialVelocity - consumedByScroll) - return initialVelocity - } - - override fun onCancel() { - onStop(0f) - } - - override fun canStopOnPreFling() = false - } - }, - ) -} 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 79b346439d5d..2f9cfb6aa211 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 @@ -78,6 +78,7 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset @@ -92,7 +93,11 @@ import com.android.compose.animation.scene.LowestZIndexContentPicker import com.android.compose.animation.scene.SceneTransitionLayoutState import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.gesture.NestedScrollableBound +import com.android.compose.gesture.effect.OffsetOverscrollEffect +import com.android.compose.gesture.effect.rememberOffsetOverscrollEffect import com.android.compose.modifiers.thenIf +import com.android.internal.jank.InteractionJankMonitor +import com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius import com.android.systemui.res.R import com.android.systemui.scene.session.ui.composable.SaveableSession @@ -288,17 +293,19 @@ fun ContentScope.NotificationScrollingStack( shadeSession: SaveableSession, stackScrollView: NotificationScrollView, viewModel: NotificationsPlaceholderViewModel, + jankMonitor: InteractionJankMonitor, maxScrimTop: () -> Float, shouldPunchHoleBehindScrim: Boolean, stackTopPadding: Dp, stackBottomPadding: Dp, + modifier: Modifier = Modifier, shouldFillMaxSize: Boolean = true, shouldIncludeHeadsUpSpace: Boolean = true, shouldShowScrim: Boolean = true, supportNestedScrolling: Boolean, onEmptySpaceClick: (() -> Unit)? = null, - modifier: Modifier = Modifier, ) { + val composeViewRoot = LocalView.current val coroutineScope = shadeSession.sessionCoroutineScope() val density = LocalDensity.current val screenCornerRadius = LocalScreenCornerRadius.current @@ -477,6 +484,21 @@ fun ContentScope.NotificationScrollingStack( ) } + val overScrollEffect: OffsetOverscrollEffect = rememberOffsetOverscrollEffect() + // whether the stack is moving due to a swipe or fling + val isScrollInProgress = + scrollState.isScrollInProgress || overScrollEffect.isInProgress || scrimOffset.isRunning + + LaunchedEffect(isScrollInProgress) { + if (isScrollInProgress) { + jankMonitor.begin(composeViewRoot, CUJ_NOTIFICATION_SHADE_SCROLL_FLING) + debugLog(viewModel) { "STACK scroll begins" } + } else { + debugLog(viewModel) { "STACK scroll ends" } + jankMonitor.end(CUJ_NOTIFICATION_SHADE_SCROLL_FLING) + } + } + Box( modifier = modifier @@ -577,8 +599,7 @@ fun ContentScope.NotificationScrollingStack( .thenIf(supportNestedScrolling) { Modifier.nestedScroll(scrimNestedScrollConnection) } - .stackVerticalOverscroll(coroutineScope) { scrollState.canScrollForward } - .verticalScroll(scrollState) + .verticalScroll(scrollState, overscrollEffect = overScrollEffect) .padding(top = stackTopPadding, bottom = stackBottomPadding) .fillMaxWidth() .onGloballyPositioned { coordinates -> diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt index 7cd6c6b47f2a..6d37e0affd6a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt @@ -29,6 +29,7 @@ import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult +import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn import com.android.systemui.keyguard.ui.composable.section.DefaultClockSection @@ -49,6 +50,7 @@ import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.ui.composable.Overlay import com.android.systemui.shade.ui.composable.OverlayShade import com.android.systemui.shade.ui.composable.OverlayShadeHeader +import com.android.systemui.shade.ui.composable.isFullWidthShade import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView import com.android.systemui.util.Utils import dagger.Lazy @@ -68,6 +70,7 @@ constructor( private val keyguardClockViewModel: KeyguardClockViewModel, private val mediaCarouselController: MediaCarouselController, @Named(QUICK_QS_PANEL) private val mediaHost: Lazy<MediaHost>, + private val jankMonitor: InteractionJankMonitor, ) : Overlay { override val key = Overlays.NotificationsShade @@ -117,7 +120,7 @@ constructor( ) { Box { Column { - if (viewModel.showClock) { + if (isFullWidthShade()) { val burnIn = rememberBurnIn(keyguardClockViewModel) with(clockSection) { @@ -145,6 +148,7 @@ constructor( shadeSession = shadeSession, stackScrollView = stackScrollView.get(), viewModel = placeholderViewModel, + jankMonitor = jankMonitor, maxScrimTop = { 0f }, shouldPunchHoleBehindScrim = false, stackTopPadding = notificationStackPadding, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index 0a711487ccb1..d667f68e4fdd 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -75,6 +75,7 @@ import com.android.compose.animation.scene.animateSceneFloatAsState import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.modifiers.thenIf import com.android.compose.windowsizeclass.LocalWindowSizeClass +import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout import com.android.systemui.compose.modifiers.sysuiResTag @@ -126,6 +127,7 @@ constructor( private val contentViewModelFactory: QuickSettingsSceneContentViewModel.Factory, private val mediaCarouselController: MediaCarouselController, @Named(MediaModule.QS_PANEL) private val mediaHost: MediaHost, + private val jankMonitor: InteractionJankMonitor, ) : ExclusiveActivatable(), Scene { override val key = Scenes.QuickSettings @@ -165,6 +167,7 @@ constructor( mediaHost = mediaHost, modifier = modifier, shadeSession = shadeSession, + jankMonitor = jankMonitor, ) } @@ -186,6 +189,7 @@ private fun ContentScope.QuickSettingsScene( mediaHost: MediaHost, modifier: Modifier = Modifier, shadeSession: SaveableSession, + jankMonitor: InteractionJankMonitor, ) { val cutoutLocation = LocalDisplayCutout.current.location val brightnessMirrorShowing by brightnessMirrorViewModel.isShowing.collectAsStateWithLifecycle() @@ -432,6 +436,7 @@ private fun ContentScope.QuickSettingsScene( shadeSession = shadeSession, stackScrollView = notificationStackScrollView, viewModel = notificationsPlaceholderViewModel, + jankMonitor = jankMonitor, maxScrimTop = { minNotificationStackTop.toFloat() }, shouldPunchHoleBehindScrim = shouldPunchHoleBehindScrim, stackTopPadding = notificationStackPadding, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt index 8f0fb20cef36..cb03119d1e19 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onPlaced @@ -266,7 +267,10 @@ fun ContentScope.QuickSettingsLayout( BrightnessSliderContainer( viewModel = viewModel.brightnessSliderViewModel, containerColors = - ContainerColors.singleColor(OverlayShade.Colors.PanelBackground), + ContainerColors( + idleColor = Color.Transparent, + mirrorColor = OverlayShade.Colors.PanelBackground, + ), modifier = Modifier.fillMaxWidth(), ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/shared/SessionStorage.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/shared/SessionStorage.kt index dc5891915bfc..d1cbeca2070c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/shared/SessionStorage.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/shared/SessionStorage.kt @@ -16,6 +16,7 @@ package com.android.systemui.scene.session.shared +import androidx.compose.runtime.RememberObserver import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -39,6 +40,9 @@ class SessionStorage { /** Clears the data store; any downstream usage within `@Composable`s will be recomposed. */ fun clear() { + for (storageEntry in _storage.values) { + (storageEntry.stored as? RememberObserver)?.onForgotten() + } _storage = hashMapOf() } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt index b30e12f073ad..89c54bcc1ced 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt @@ -37,7 +37,6 @@ import androidx.compose.foundation.layout.systemBarsIgnoringVisibility import androidx.compose.foundation.layout.waterfall import androidx.compose.foundation.layout.width import androidx.compose.foundation.overscroll -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable @@ -46,6 +45,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -57,6 +57,8 @@ import com.android.mechanics.behavior.VerticalExpandContainerSpec import com.android.mechanics.behavior.verticalExpandContainerBackground import com.android.systemui.Flags import com.android.systemui.res.R +import com.android.systemui.shade.ui.ShadeColors.notificationScrim +import com.android.systemui.shade.ui.ShadeColors.shadePanel import com.android.systemui.shade.ui.composable.OverlayShade.rememberShadeExpansionMotion /** Renders a lightweight shade UI container, as an overlay. */ @@ -190,17 +192,15 @@ object OverlayShade { } object Colors { - val ScrimBackground = Color(0f, 0f, 0f, alpha = 0.3f) + val ScrimBackground: Color + @Composable + @ReadOnlyComposable + get() = Color(LocalResources.current.notificationScrim(Flags.notificationShadeBlur())) + val PanelBackground: Color @Composable @ReadOnlyComposable - get() { - return if (Flags.notificationShadeBlur()) { - MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f) - } else { - MaterialTheme.colorScheme.surfaceContainer - } - } + get() = Color(LocalResources.current.shadePanel(Flags.notificationShadeBlur())) } object Dimensions { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index 885d34fb95c9..60e32d7ce824 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -73,6 +73,7 @@ import com.android.compose.animation.scene.animateSceneFloatAsState import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.modifiers.padding import com.android.compose.modifiers.thenIf +import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout @@ -145,6 +146,7 @@ constructor( private val mediaCarouselController: MediaCarouselController, @Named(QUICK_QS_PANEL) private val qqsMediaHost: MediaHost, @Named(QS_PANEL) private val qsMediaHost: MediaHost, + private val jankMonitor: InteractionJankMonitor, ) : ExclusiveActivatable(), Scene { override val key = Scenes.Shade @@ -182,6 +184,7 @@ constructor( mediaCarouselController = mediaCarouselController, qqsMediaHost = qqsMediaHost, qsMediaHost = qsMediaHost, + jankMonitor = jankMonitor, modifier = modifier, shadeSession = shadeSession, usingCollapsedLandscapeMedia = @@ -212,6 +215,7 @@ private fun ContentScope.ShadeScene( mediaCarouselController: MediaCarouselController, qqsMediaHost: MediaHost, qsMediaHost: MediaHost, + jankMonitor: InteractionJankMonitor, modifier: Modifier = Modifier, shadeSession: SaveableSession, usingCollapsedLandscapeMedia: Boolean, @@ -229,6 +233,7 @@ private fun ContentScope.ShadeScene( modifier = modifier, shadeSession = shadeSession, usingCollapsedLandscapeMedia = usingCollapsedLandscapeMedia, + jankMonitor = jankMonitor, ) is ShadeMode.Split -> SplitShade( @@ -240,6 +245,7 @@ private fun ContentScope.ShadeScene( mediaHost = qsMediaHost, modifier = modifier, shadeSession = shadeSession, + jankMonitor = jankMonitor, ) is ShadeMode.Dual -> error("Dual shade is implemented separately as an overlay.") } @@ -253,6 +259,7 @@ private fun ContentScope.SingleShade( notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, mediaCarouselController: MediaCarouselController, mediaHost: MediaHost, + jankMonitor: InteractionJankMonitor, modifier: Modifier = Modifier, shadeSession: SaveableSession, usingCollapsedLandscapeMedia: Boolean, @@ -379,6 +386,7 @@ private fun ContentScope.SingleShade( shadeSession = shadeSession, stackScrollView = notificationStackScrollView, viewModel = notificationsPlaceholderViewModel, + jankMonitor = jankMonitor, maxScrimTop = { maxNotifScrimTop.toFloat() }, shouldPunchHoleBehindScrim = shouldPunchHoleBehindScrim, stackTopPadding = notificationStackPadding, @@ -419,6 +427,7 @@ private fun ContentScope.SplitShade( mediaHost: MediaHost, modifier: Modifier = Modifier, shadeSession: SaveableSession, + jankMonitor: InteractionJankMonitor, ) { val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsStateWithLifecycle() val isQsEnabled by viewModel.isQsEnabled.collectAsStateWithLifecycle() @@ -596,6 +605,7 @@ private fun ContentScope.SplitShade( shadeSession = shadeSession, stackScrollView = notificationStackScrollView, viewModel = notificationsPlaceholderViewModel, + jankMonitor = jankMonitor, maxScrimTop = { 0f }, stackTopPadding = notificationStackPadding, stackBottomPadding = notificationStackPadding, 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 05958a212f47..9ba74749639a 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 @@ -52,6 +52,7 @@ import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEachIndexed import androidx.compose.ui.util.fastForEachReversed import androidx.compose.ui.util.lerp +import com.android.compose.animation.scene.Element.Companion.SizeUnspecified import com.android.compose.animation.scene.content.Content import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.animation.scene.transformation.CustomPropertyTransformation @@ -105,6 +106,13 @@ internal class Element(val key: ElementKey) { var targetSize by mutableStateOf(SizeUnspecified) var targetOffset by mutableStateOf(Offset.Unspecified) + /** + * The *approach* state of this element in this content, i.e. the intermediate layout state + * during transitions, used for smooth animation. Note: These values are computed before + * measuring the children. + */ + var approachSize by mutableStateOf(SizeUnspecified) + /** The last state this element had in this content. */ var lastOffset = Offset.Unspecified var lastSize = SizeUnspecified @@ -340,7 +348,11 @@ internal class ElementNode( override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean { // TODO(b/324191441): Investigate whether making this check more complex (checking if this // element is shared or transformed) would lead to better performance. - return isAnyStateTransitioning() + val isTransitioning = isAnyStateTransitioning() + if (!isTransitioning) { + stateInContent.approachSize = SizeUnspecified + } + return isTransitioning } override fun Placeable.PlacementScope.isPlacementApproachInProgress( @@ -392,6 +404,7 @@ internal class ElementNode( // sharedElement isn't part of either but the element is still rendered as part of // the underlying scene that is currently not being transitioned. val currentState = currentTransitionStates.last().last() + stateInContent.approachSize = Element.SizeUnspecified val shouldPlaceInThisContent = elementContentWhenIdle( layoutImpl, @@ -409,7 +422,14 @@ internal class ElementNode( val transition = elementState as? TransitionState.Transition val placeable = - measure(layoutImpl, element, transition, stateInContent, measurable, constraints) + approachMeasure( + layoutImpl = layoutImpl, + element = element, + transition = transition, + stateInContent = stateInContent, + measurable = measurable, + constraints = constraints, + ) stateInContent.lastSize = placeable.size() return layout(placeable.width, placeable.height) { place(elementState, placeable) } } @@ -1183,7 +1203,7 @@ private fun interruptedAlpha( ) } -private fun measure( +private fun approachMeasure( layoutImpl: SceneTransitionLayoutImpl, element: Element, transition: TransitionState.Transition?, @@ -1214,6 +1234,7 @@ private fun measure( maybePlaceable?.let { placeable -> stateInContent.sizeBeforeInterruption = Element.SizeUnspecified stateInContent.sizeInterruptionDelta = IntSize.Zero + stateInContent.approachSize = Element.SizeUnspecified return placeable } @@ -1236,6 +1257,10 @@ private fun measure( ) }, ) + + // Important: Set approachSize before child measurement. Could be used for their calculations. + stateInContent.approachSize = interruptedSize + return measurable.measure( Constraints.fixed( interruptedSize.width.coerceAtLeast(0), 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 22688d310b44..cfd59fd316d3 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 @@ -160,6 +160,13 @@ interface ElementStateScope { fun ElementKey.targetSize(content: ContentKey): IntSize? /** + * Return the *approaching* size of [this] element in the given [content], i.e. thethe size the + * element when is transitioning, or `null` if the element is not composed and measured in that + * content (yet). + */ + fun ElementKey.approachSize(content: ContentKey): IntSize? + + /** * Return the *target* offset of [this] element in the given [content], i.e. the size of the * element when idle, or `null` if the element is not composed and placed in that content (yet). */ diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt index 5d4232d8a8b7..0d5ae81c501d 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt @@ -31,6 +31,12 @@ internal class ElementStateScopeImpl(private val layoutImpl: SceneTransitionLayo } } + override fun ElementKey.approachSize(content: ContentKey): IntSize? { + return layoutImpl.elements[this]?.stateByContent?.get(content)?.approachSize.takeIf { + it != Element.SizeUnspecified + } + } + override fun ElementKey.targetOffset(content: ContentKey): Offset? { return layoutImpl.elements[this]?.stateByContent?.get(content)?.targetOffset.takeIf { it != Offset.Unspecified 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 86cbfe4f1a8b..aff5aa097a8e 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 @@ -2312,4 +2312,76 @@ class ElementTest { assertThat(compositions).isEqualTo(1) } + + @Test + fun measureElementApproachSizeBeforeChildren() { + val state = + rule.runOnUiThread { + MutableSceneTransitionLayoutStateForTests(SceneA, SceneTransitions.Empty) + } + + lateinit var fooHeight: () -> Dp? + val fooHeightPreChildMeasure = mutableListOf<Dp?>() + + val scope = + rule.setContentAndCreateMainScope { + val density = LocalDensity.current + SceneTransitionLayoutForTesting(state) { + scene(SceneA) { + fooHeight = { + with(density) { TestElements.Foo.approachSize(SceneA)?.height?.toDp() } + } + Box(Modifier.element(TestElements.Foo).size(200.dp)) { + Box( + Modifier.approachLayout( + isMeasurementApproachInProgress = { false }, + approachMeasure = { measurable, constraints -> + fooHeightPreChildMeasure += fooHeight() + measurable.measure(constraints).run { + layout(width, height) {} + } + }, + ) + ) + } + } + scene(SceneB) { Box(Modifier.element(TestElements.Foo).size(100.dp)) } + } + } + + var progress by mutableFloatStateOf(0f) + val transition = transition(from = SceneA, to = SceneB, progress = { progress }) + var countApproachPass = fooHeightPreChildMeasure.size + + // Idle state: Scene A. + assertThat(state.isTransitioning()).isFalse() + assertThat(fooHeight()).isNull() + + // Start transition: Scene A -> Scene B (progress 0%). + scope.launch { state.startTransition(transition) } + rule.waitForIdle() + assertThat(state.isTransitioning()).isTrue() + assertThat(fooHeightPreChildMeasure[countApproachPass]?.value).isWithin(.5f).of(200f) + assertThat(fooHeight()).isNotNull() + countApproachPass = fooHeightPreChildMeasure.size + + // progress 50%: height is going from 200dp to 100dp, so 150dp is expected now. + progress = 0.5f + rule.waitForIdle() + assertThat(fooHeightPreChildMeasure[countApproachPass]?.value).isWithin(.5f).of(150f) + assertThat(fooHeight()).isNotNull() + countApproachPass = fooHeightPreChildMeasure.size + + progress = 1f + rule.waitForIdle() + assertThat(fooHeightPreChildMeasure[countApproachPass]?.value).isWithin(.5f).of(100f) + assertThat(fooHeight()).isNotNull() + countApproachPass = fooHeightPreChildMeasure.size + + transition.finish() + rule.waitForIdle() + assertThat(state.isTransitioning()).isFalse() + assertThat(fooHeight()).isNull() + assertThat(fooHeightPreChildMeasure.size).isEqualTo(countApproachPass) + } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt index 5f71b19fbc3f..884666854ab4 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt @@ -111,10 +111,6 @@ class ComposedDigitalLayerController(private val clockCtx: ClockContext) : override fun onZenDataChanged(data: ZenData) {} - override fun onFontAxesChanged(axes: ClockAxisStyle) { - view.updateAxes(axes) - } - override var isReactiveTouchInteractionEnabled get() = view.isReactiveTouchInteractionEnabled set(value) { @@ -152,6 +148,13 @@ class ComposedDigitalLayerController(private val clockCtx: ClockContext) : override fun onFidgetTap(x: Float, y: Float) { view.animateFidget(x, y) } + + private var hasFontAxes = false + + override fun onFontAxesChanged(style: ClockAxisStyle) { + view.updateAxes(style, isAnimated = hasFontAxes) + hasFontAxes = true + } } override val faceEvents = diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt index 3cfa78d17fe7..450cece8709a 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt @@ -231,8 +231,6 @@ class DefaultClockController( override fun onAlarmDataChanged(data: AlarmData) {} override fun onZenDataChanged(data: ZenData) {} - - override fun onFontAxesChanged(axes: ClockAxisStyle) {} } open inner class DefaultClockAnimations( @@ -285,6 +283,8 @@ class DefaultClockController( override fun onPositionUpdated(distance: Float, fraction: Float) {} override fun onFidgetTap(x: Float, y: Float) {} + + override fun onFontAxesChanged(style: ClockAxisStyle) {} } inner class LargeClockAnimations( diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt index 8744357a74c9..84f45fcc0532 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt @@ -99,12 +99,6 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController smallClock.events.onZenDataChanged(data) largeClock.events.onZenDataChanged(data) } - - override fun onFontAxesChanged(axes: ClockAxisStyle) { - val fontAxes = ClockAxisStyle(getDefaultAxes(clockCtx.settings).merge(axes)) - smallClock.events.onFontAxesChanged(fontAxes) - largeClock.events.onFontAxesChanged(fontAxes) - } } override fun initialize( @@ -113,10 +107,10 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController foldFraction: Float, clockListener: ClockEventListener?, ) { - events.onFontAxesChanged(clockCtx.settings.axes) smallClock.run { layerController.onViewBoundsChanged = { clockListener?.onBoundsChanged(it) } events.onThemeChanged(theme.copy(isDarkTheme = isDarkTheme)) + animations.onFontAxesChanged(clockCtx.settings.axes) animations.doze(dozeFraction) animations.fold(foldFraction) events.onTimeTick() @@ -125,6 +119,7 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController largeClock.run { layerController.onViewBoundsChanged = { clockListener?.onBoundsChanged(it) } events.onThemeChanged(theme.copy(isDarkTheme = isDarkTheme)) + animations.onFontAxesChanged(clockCtx.settings.axes) animations.doze(dozeFraction) animations.fold(foldFraction) events.onTimeTick() diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt index 171a68f72e20..ec7803d0c6d6 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt @@ -31,10 +31,12 @@ import com.android.systemui.plugins.clocks.ClockFaceConfig import com.android.systemui.plugins.clocks.ClockFaceController import com.android.systemui.plugins.clocks.ClockFaceEvents import com.android.systemui.plugins.clocks.ClockFaceLayout +import com.android.systemui.plugins.clocks.ClockFontAxis.Companion.merge import com.android.systemui.plugins.clocks.DefaultClockFaceLayout import com.android.systemui.plugins.clocks.ThemeConfig import com.android.systemui.plugins.clocks.WeatherData import com.android.systemui.plugins.clocks.ZenData +import com.android.systemui.shared.clocks.FlexClockController.Companion.getDefaultAxes import com.android.systemui.shared.clocks.FontUtils.get import com.android.systemui.shared.clocks.FontUtils.set import com.android.systemui.shared.clocks.ViewUtils.computeLayoutDiff @@ -131,15 +133,6 @@ class FlexClockFaceController(clockCtx: ClockContext, private val isLargeClock: layerController.faceEvents.onThemeChanged(theme) } - override fun onFontAxesChanged(settings: ClockAxisStyle) { - var axes = ClockAxisStyle(settings) - if (!isLargeClock && axes[GSFAxes.WIDTH] > SMALL_CLOCK_MAX_WDTH) { - axes[GSFAxes.WIDTH] = SMALL_CLOCK_MAX_WDTH - } - - layerController.events.onFontAxesChanged(axes) - } - /** * targetRegion passed to all customized clock applies counter translationY of Keyguard and * keyguard_large_clock_top_margin from default clock @@ -232,6 +225,15 @@ class FlexClockFaceController(clockCtx: ClockContext, private val isLargeClock: override fun onFidgetTap(x: Float, y: Float) { layerController.animations.onFidgetTap(x, y) } + + override fun onFontAxesChanged(style: ClockAxisStyle) { + var axes = ClockAxisStyle(getDefaultAxes(clockCtx.settings).merge(style)) + if (!isLargeClock && axes[GSFAxes.WIDTH] > SMALL_CLOCK_MAX_WDTH) { + axes[GSFAxes.WIDTH] = SMALL_CLOCK_MAX_WDTH + } + + layerController.animations.onFontAxesChanged(axes) + } } companion object { diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt index 7be9a936cbd3..eef910c3bd27 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt @@ -171,10 +171,6 @@ open class SimpleDigitalHandLayerController( override fun onAlarmDataChanged(data: AlarmData) {} override fun onZenDataChanged(data: ZenData) {} - - override fun onFontAxesChanged(axes: ClockAxisStyle) { - view.updateAxes(axes) - } } override val animations = @@ -195,6 +191,13 @@ open class SimpleDigitalHandLayerController( view.dozeFraction = fraction } + private var hasFontAxes = false + + override fun onFontAxesChanged(style: ClockAxisStyle) { + view.updateAxes(style, isAnimated = hasFontAxes) + hasFontAxes = true + } + override fun fold(fraction: Float) { applyLayout() refreshTime() diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt index 4531aed0e83d..4a5532b6e462 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt @@ -272,8 +272,8 @@ class FlexClockView(clockCtx: ClockContext) : ViewGroup(clockCtx.context) { invalidate() } - fun updateAxes(axes: ClockAxisStyle) { - childViews.forEach { view -> view.updateAxes(axes) } + fun updateAxes(axes: ClockAxisStyle, isAnimated: Boolean) { + childViews.forEach { view -> view.updateAxes(axes, isAnimated) } requestLayout() } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt index 377a24c2899b..655b79c5b635 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt @@ -200,19 +200,26 @@ open class SimpleDigitalClockTextView( invalidate() } - fun updateAxes(lsAxes: ClockAxisStyle) { + fun updateAxes(lsAxes: ClockAxisStyle, isAnimated: Boolean) { lsFontVariation = lsAxes.toFVar() aodFontVariation = lsAxes.copyWith(fixedAodAxes).toFVar() fidgetFontVariation = buildFidgetVariation(lsAxes).toFVar() - logger.updateAxes(lsFontVariation, aodFontVariation) + logger.updateAxes(lsFontVariation, aodFontVariation, isAnimated) lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation) typeface = lockScreenPaint.typeface - textBounds = lockScreenPaint.getTextBounds(text) - targetTextBounds = textBounds + updateTextBounds() + + textAnimator.setTextStyle( + TextAnimator.Style(fVar = lsFontVariation), + TextAnimator.Animation( + animate = isAnimated && isAnimationEnabled, + duration = AXIS_CHANGE_ANIMATION_DURATION, + interpolator = aodDozingInterpolator, + ), + ) - textAnimator.setTextStyle(TextAnimator.Style(fVar = lsFontVariation)) measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) recomputeMaxSingleDigitSizes() requestLayout() @@ -245,9 +252,9 @@ open class SimpleDigitalClockTextView( object : TextAnimatorListener { override fun onInvalidate() = invalidate() - override fun onRebased() = updateTextBounds() + override fun onRebased() = updateAnimationTextBounds() - override fun onPaintModified() = updateTextBounds() + override fun onPaintModified() = updateAnimationTextBounds() }, ) setInterpolatorPaint() @@ -406,10 +413,7 @@ open class SimpleDigitalClockTextView( } fun refreshText() { - textBounds = lockScreenPaint.getTextBounds(text) - targetTextBounds = - if (!this::textAnimator.isInitialized) textBounds - else textAnimator.textInterpolator.targetPaint.getTextBounds(text) + updateTextBounds() if (layout == null) { requestLayout() @@ -571,8 +575,7 @@ open class SimpleDigitalClockTextView( if (fontSizePx > 0) { setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx) lockScreenPaint.textSize = textSize - textBounds = lockScreenPaint.getTextBounds(text) - targetTextBounds = textBounds + updateTextBounds() } if (!constrainedByHeight) { val lastUnconstrainedHeight = textBounds.height + lockScreenPaint.strokeWidth * 2 @@ -616,15 +619,26 @@ open class SimpleDigitalClockTextView( } } + /** Updates both the lockscreen text bounds and animation text bounds */ + private fun updateTextBounds() { + textBounds = lockScreenPaint.getTextBounds(text) + updateAnimationTextBounds() + } + /** * Called after textAnimator.setTextStyle textAnimator.setTextStyle will update targetPaint, and * rebase if previous animator is canceled so basePaint will store the state we transition from * and targetPaint will store the state we transition to */ - private fun updateTextBounds() { + private fun updateAnimationTextBounds() { drawnProgress = null - prevTextBounds = textAnimator.textInterpolator.basePaint.getTextBounds(text) - targetTextBounds = textAnimator.textInterpolator.targetPaint.getTextBounds(text) + if (this::textAnimator.isInitialized) { + prevTextBounds = textAnimator.textInterpolator.basePaint.getTextBounds(text) + targetTextBounds = textAnimator.textInterpolator.targetPaint.getTextBounds(text) + } else { + prevTextBounds = textBounds + targetTextBounds = textBounds + } } /** @@ -651,6 +665,7 @@ open class SimpleDigitalClockTextView( .compose() val CHARGE_ANIMATION_DURATION = 500L + val AXIS_CHANGE_ANIMATION_DURATION = 400L val FIDGET_ANIMATION_DURATION = 250L val FIDGET_INTERPOLATOR = PathInterpolator(0.26873f, 0f, 0.45042f, 1f) val FIDGET_DISTS = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java index 02ec5aac120c..81bc94943b71 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java @@ -32,6 +32,7 @@ import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.SysuiTestCase; import com.android.systemui.accessibility.WindowMagnificationSettings.MagnificationSize; import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import org.junit.After; import org.junit.Before; @@ -58,14 +59,14 @@ public class MagnificationSettingsControllerTest extends SysuiTestCase { @Mock private SecureSettings mSecureSettings; @Mock - private WindowManager mWindowManager; + private WindowManagerProvider mWindowManagerProvider; @Before public void setUp() { MockitoAnnotations.initMocks(this); mMagnificationSettingsController = new MagnificationSettingsController( mContext, mSfVsyncFrameProvider, - mMagnificationSettingControllerCallback, mSecureSettings, mWindowManager, + mMagnificationSettingControllerCallback, mSecureSettings, mWindowManagerProvider, mWindowMagnificationSettings); } 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 675c9deaff23..56c018218269 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -16,6 +16,8 @@ package com.android.systemui.biometrics; +import static com.android.systemui.SysuiTestCaseExtKt.testKosmos; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; @@ -69,7 +71,9 @@ import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInt import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.ScreenLifecycle; +import com.android.systemui.keyguard.UserActivityNotifierKosmosKt; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.kosmos.Kosmos; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -115,7 +119,7 @@ import java.util.List; @RunWith(AndroidJUnit4.class) @RunWithLooper(setAsMainLooper = true) public class UdfpsControllerTest extends SysuiTestCase { - + private final Kosmos mKosmos = testKosmos(this); private static final long TEST_REQUEST_ID = 70; @Rule @@ -325,7 +329,8 @@ public class UdfpsControllerTest extends SysuiTestCase { mDefaultUdfpsTouchOverlayViewModel, mUdfpsOverlayInteractor, mPowerInteractor, - mock(CoroutineScope.class) + mock(CoroutineScope.class), + UserActivityNotifierKosmosKt.getUserActivityNotifier(mKosmos) ); verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture()); mOverlayController = mOverlayCaptor.getValue(); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt index e8c30bafbba0..c963157318ed 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt @@ -191,7 +191,6 @@ class SideFpsOverlayViewBinderTest : SysuiTestCase() { val clickListenerCaptor = ArgumentCaptor.forClass(View.OnClickListener::class.java) verify(sideFpsView).setOnClickListener(clickListenerCaptor.capture()) clickListenerCaptor.value.onClick(sideFpsView) - verify(lottieAnimationView).toggleAnimation() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ActionIntentCreatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ActionIntentCreatorTest.kt index 652a2ff21e9b..87eea82ef30d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ActionIntentCreatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ActionIntentCreatorTest.kt @@ -19,12 +19,17 @@ package com.android.systemui.clipboardoverlay import android.content.ClipData import android.content.ComponentName import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager import android.net.Uri import android.text.SpannableString import androidx.test.filters.SmallTest import androidx.test.runner.AndroidJUnit4 import com.android.systemui.SysuiTestCase import com.android.systemui.res.R +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock import kotlinx.coroutines.test.TestCoroutineScheduler import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -33,6 +38,8 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @@ -40,8 +47,10 @@ class ActionIntentCreatorTest : SysuiTestCase() { private val scheduler = TestCoroutineScheduler() private val mainDispatcher = UnconfinedTestDispatcher(scheduler) private val testScope = TestScope(mainDispatcher) + val packageManager = mock<PackageManager>() - val creator = ActionIntentCreator(testScope.backgroundScope) + val creator = + ActionIntentCreator(context, packageManager, testScope.backgroundScope, mainDispatcher) @Test fun test_getTextEditorIntent() { @@ -73,7 +82,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { } @Test - fun test_getImageEditIntent() = runTest { + fun test_getImageEditIntent_noDefault() = runTest { context.getOrCreateTestableResources().addOverride(R.string.config_screenshotEditor, "") val fakeUri = Uri.parse("content://foo") var intent = creator.getImageEditIntent(fakeUri, context) @@ -83,18 +92,82 @@ class ActionIntentCreatorTest : SysuiTestCase() { assertEquals(null, intent.component) assertEquals("clipboard", intent.getStringExtra("edit_source")) assertFlags(intent, EXTERNAL_INTENT_FLAGS) + } + + @Test + fun test_getImageEditIntent_defaultProvided() = runTest { + val fakeUri = Uri.parse("content://foo") - // try again with an editor component val fakeComponent = ComponentName("com.android.remotecopy", "com.android.remotecopy.RemoteCopyActivity") context .getOrCreateTestableResources() .addOverride(R.string.config_screenshotEditor, fakeComponent.flattenToString()) - intent = creator.getImageEditIntent(fakeUri, context) + val intent = creator.getImageEditIntent(fakeUri, context) assertEquals(fakeComponent, intent.component) } @Test + fun test_getImageEditIntent_preferredProvidedButDisabled() = runTest { + val fakeUri = Uri.parse("content://foo") + + val defaultComponent = ComponentName("com.android.foo", "com.android.foo.Something") + val preferredComponent = ComponentName("com.android.bar", "com.android.bar.Something") + + val packageInfo = + PackageInfo().apply { + activities = arrayOf() // no activities + } + whenever(packageManager.getPackageInfo(eq(preferredComponent.packageName), anyInt())) + .thenReturn(packageInfo) + + context + .getOrCreateTestableResources() + .addOverride(R.string.config_screenshotEditor, defaultComponent.flattenToString()) + context + .getOrCreateTestableResources() + .addOverride( + R.string.config_preferredScreenshotEditor, + preferredComponent.flattenToString(), + ) + val intent = creator.getImageEditIntent(fakeUri, context) + assertEquals(defaultComponent, intent.component) + } + + @Test + fun test_getImageEditIntent_preferredProvided() = runTest { + val fakeUri = Uri.parse("content://foo") + + val defaultComponent = ComponentName("com.android.foo", "com.android.foo.Something") + val preferredComponent = ComponentName("com.android.bar", "com.android.bar.Something") + + val packageInfo = + PackageInfo().apply { + activities = + arrayOf( + ActivityInfo().apply { + packageName = preferredComponent.packageName + name = preferredComponent.className + } + ) + } + whenever(packageManager.getPackageInfo(eq(preferredComponent.packageName), anyInt())) + .thenReturn(packageInfo) + + context + .getOrCreateTestableResources() + .addOverride(R.string.config_screenshotEditor, defaultComponent.flattenToString()) + context + .getOrCreateTestableResources() + .addOverride( + R.string.config_preferredScreenshotEditor, + preferredComponent.flattenToString(), + ) + val intent = creator.getImageEditIntent(fakeUri, context) + assertEquals(preferredComponent, intent.component) + } + + @Test fun test_getShareIntent_plaintext() { val clipData = ClipData.newPlainText("Test", "Test Item") val intent = creator.getShareIntent(clipData, context) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSceneRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSceneRepositoryImplTest.kt index 293d32471713..51ad6a146d0e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSceneRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSceneRepositoryImplTest.kt @@ -21,23 +21,18 @@ import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.SysuiTestCase import com.android.systemui.communal.shared.model.CommunalScenes -import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.backgroundScope import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher -import com.android.systemui.scene.shared.model.SceneDataSource import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.flowOf import org.junit.Test import org.junit.runner.RunWith -import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.mock -import org.mockito.kotlin.verify @SmallTest @RunWith(AndroidJUnit4::class) @@ -49,10 +44,8 @@ class CommunalSceneRepositoryImplTest : SysuiTestCase() { private val Kosmos.underTest by Kosmos.Fixture { CommunalSceneRepositoryImpl( - applicationScope = applicationCoroutineScope, backgroundScope = backgroundScope, sceneDataSource = delegator, - delegator = delegator, ) } @@ -90,18 +83,4 @@ class CommunalSceneRepositoryImplTest : SysuiTestCase() { assertThat(transitionState) .isEqualTo(ObservableTransitionState.Idle(CommunalScenes.Default)) } - - @Test - fun showHubFromPowerButton() = - kosmos.runTest { - fakeKeyguardRepository.setKeyguardShowing(false) - - underTest.showHubFromPowerButton() - - argumentCaptor<SceneDataSource>().apply { - verify(delegator).setDelegate(capture()) - - assertThat(firstValue.currentScene.value).isEqualTo(CommunalScenes.Communal) - } - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt index 856a62e3f5a7..a6be3ce43b6a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt @@ -16,9 +16,11 @@ package com.android.systemui.communal.domain.interactor +import android.platform.test.annotations.EnableFlags import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.common.data.repository.batteryRepository import com.android.systemui.common.data.repository.fake @@ -47,6 +49,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@EnableFlags(FLAG_GLANCEABLE_HUB_V2) class CommunalAutoOpenInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() @@ -54,6 +57,7 @@ class CommunalAutoOpenInteractorTest : SysuiTestCase() { @Before fun setUp() { + kosmos.setCommunalV2ConfigEnabled(true) runBlocking { kosmos.fakeUserRepository.asMainUser() } with(kosmos.fakeSettings) { putIntForUser( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index b65ecf46dcca..215d36fcb2a5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -36,6 +36,7 @@ import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.Flags.FLAG_COMMUNAL_RESPONSIVE_GRID import com.android.systemui.Flags.FLAG_COMMUNAL_WIDGET_RESIZING import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 +import com.android.systemui.Flags.glanceableHubV2 import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.communal.data.model.CommunalSmartspaceTimer @@ -90,6 +91,7 @@ import platform.test.runner.parameterized.Parameters @SmallTest @RunWith(ParameterizedAndroidJunit4::class) +@EnableFlags(FLAG_COMMUNAL_HUB) class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { private val mainUser = UserInfo(/* id= */ 0, /* name= */ "primary user", /* flags= */ UserInfo.FLAG_MAIN) @@ -110,7 +112,9 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { kosmos.fakeUserRepository.setUserInfos(listOf(mainUser, secondaryUser)) kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) - mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB) + if (glanceableHubV2()) { + kosmos.setCommunalV2ConfigEnabled(true) + } } @Test @@ -120,7 +124,9 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(underTest.isCommunalEnabled.value).isTrue() } + /** Test not applicable when [FLAG_GLANCEABLE_HUB_V2] enabled */ @Test + @DisableFlags(FLAG_GLANCEABLE_HUB_V2) fun isCommunalAvailable_whenKeyguardShowing_true() = kosmos.runTest { communalSettingsInteractor.setSuppressionReasons(emptyList()) @@ -1212,7 +1218,10 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @JvmStatic @Parameters(name = "{0}") fun getParams(): List<FlagsParameterization> { - return FlagsParameterization.allCombinationsOf(FLAG_COMMUNAL_RESPONSIVE_GRID) + return FlagsParameterization.allCombinationsOf( + FLAG_COMMUNAL_RESPONSIVE_GRID, + FLAG_GLANCEABLE_HUB_V2, + ) } private val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorTest.kt index dc21f0692c9e..7bdac476641b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorTest.kt @@ -16,6 +16,8 @@ package com.android.systemui.communal.domain.interactor +import android.content.res.Configuration.ORIENTATION_LANDSCAPE +import android.content.res.Configuration.ORIENTATION_PORTRAIT import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization @@ -33,11 +35,15 @@ import com.android.systemui.flags.andSceneContainer import com.android.systemui.kosmos.testScope import com.android.systemui.scene.initialSceneKey import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.policy.keyguardStateController import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -46,9 +52,11 @@ import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(ParameterizedAndroidJunit4::class) class CommunalSceneInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @@ -70,6 +78,7 @@ class CommunalSceneInteractorTest(flags: FlagsParameterization) : SysuiTestCase( private val repository = kosmos.communalSceneRepository private val underTest by lazy { kosmos.communalSceneInteractor } + private val keyguardStateController: KeyguardStateController = kosmos.keyguardStateController @DisableFlags(FLAG_SCENE_CONTAINER) @Test @@ -551,4 +560,57 @@ class CommunalSceneInteractorTest(flags: FlagsParameterization) : SysuiTestCase( transitionState.value = ObservableTransitionState.Idle(Scenes.Lockscreen) assertThat(isCommunalVisible).isEqualTo(false) } + + @Test + fun willRotateToPortrait_whenKeyguardRotationNotAllowed() = + testScope.runTest { + whenever(keyguardStateController.isKeyguardScreenRotationAllowed()).thenReturn(false) + val willRotateToPortrait by collectLastValue(underTest.willRotateToPortrait) + + repository.setCommunalContainerOrientation(ORIENTATION_LANDSCAPE) + runCurrent() + + assertThat(willRotateToPortrait).isEqualTo(true) + + repository.setCommunalContainerOrientation(ORIENTATION_PORTRAIT) + runCurrent() + + assertThat(willRotateToPortrait).isEqualTo(false) + } + + @Test + fun willRotateToPortrait_isFalse_whenKeyguardRotationIsAllowed() = + testScope.runTest { + whenever(keyguardStateController.isKeyguardScreenRotationAllowed()).thenReturn(true) + val willRotateToPortrait by collectLastValue(underTest.willRotateToPortrait) + + repository.setCommunalContainerOrientation(ORIENTATION_LANDSCAPE) + runCurrent() + + assertThat(willRotateToPortrait).isEqualTo(false) + + repository.setCommunalContainerOrientation(ORIENTATION_PORTRAIT) + runCurrent() + + assertThat(willRotateToPortrait).isEqualTo(false) + } + + @Test + fun rotatedToPortrait() = + testScope.runTest { + val rotatedToPortrait by collectLastValue(underTest.rotatedToPortrait) + assertThat(rotatedToPortrait).isEqualTo(false) + + repository.setCommunalContainerOrientation(ORIENTATION_PORTRAIT) + runCurrent() + assertThat(rotatedToPortrait).isEqualTo(false) + + repository.setCommunalContainerOrientation(ORIENTATION_LANDSCAPE) + runCurrent() + assertThat(rotatedToPortrait).isEqualTo(false) + + repository.setCommunalContainerOrientation(ORIENTATION_PORTRAIT) + runCurrent() + assertThat(rotatedToPortrait).isEqualTo(true) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorTest.kt index d6f7145bd770..c671aed1f4a1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorTest.kt @@ -21,9 +21,11 @@ import android.app.admin.devicePolicyManager import android.content.Intent import android.content.pm.UserInfo import android.os.UserManager +import android.platform.test.annotations.EnableFlags import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.communal.shared.model.WhenToStartHub @@ -86,8 +88,10 @@ class CommunalSettingsInteractorTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) fun whenToStartHub_matchesRepository() = kosmos.runTest { + setCommunalV2ConfigEnabled(true) fakeSettings.putIntForUser( Settings.Secure.WHEN_TO_START_GLANCEABLE_HUB, Settings.Secure.GLANCEABLE_HUB_START_CHARGING, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt index b4708d97c4c3..80f0005cb73f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt @@ -53,7 +53,7 @@ class PosturingInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() - private val underTest by lazy { kosmos.posturingInteractor } + private val Kosmos.underTest by Kosmos.Fixture { kosmos.posturingInteractor } @Test fun testNoDebugOverride() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/util/UserTouchActivityNotifierTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/util/UserTouchActivityNotifierTest.kt new file mode 100644 index 000000000000..581f3cb172fe --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/util/UserTouchActivityNotifierTest.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.util + +import android.testing.AndroidTestingRunner +import android.view.MotionEvent +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.power.data.repository.fakePowerRepository +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import org.junit.runner.RunWith + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class UserTouchActivityNotifierTest : SysuiTestCase() { + private val kosmos: Kosmos = testKosmos().useUnconfinedTestDispatcher() + + @Test + fun firstEventTriggersNotify() = + kosmos.runTest { sendEventAndVerify(0, MotionEvent.ACTION_MOVE, true) } + + @Test + fun secondEventTriggersRateLimited() = + kosmos.runTest { + var eventTime = 0L + + sendEventAndVerify(eventTime, MotionEvent.ACTION_MOVE, true) + eventTime += 50 + sendEventAndVerify(eventTime, MotionEvent.ACTION_MOVE, false) + eventTime += USER_TOUCH_ACTIVITY_RATE_LIMIT + sendEventAndVerify(eventTime, MotionEvent.ACTION_MOVE, true) + } + + @Test + fun overridingActionNotifies() = + kosmos.runTest { + var eventTime = 0L + sendEventAndVerify(eventTime, MotionEvent.ACTION_MOVE, true) + sendEventAndVerify(eventTime, MotionEvent.ACTION_DOWN, true) + sendEventAndVerify(eventTime, MotionEvent.ACTION_UP, true) + sendEventAndVerify(eventTime, MotionEvent.ACTION_CANCEL, true) + } + + private fun sendEventAndVerify(eventTime: Long, action: Int, shouldBeHandled: Boolean) { + kosmos.fakePowerRepository.userTouchRegistered = false + val motionEvent = MotionEvent.obtain(0, eventTime, action, 0f, 0f, 0) + kosmos.userTouchActivityNotifier.notifyActivity(motionEvent) + + if (shouldBeHandled) { + assertThat(kosmos.fakePowerRepository.userTouchRegistered).isTrue() + } else { + assertThat(kosmos.fakePowerRepository.userTouchRegistered).isFalse() + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt index b08e6761d92f..6b2207e0d754 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt @@ -18,15 +18,18 @@ package com.android.systemui.communal.widgets import android.appwidget.AppWidgetProviderInfo import android.content.pm.UserInfo -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.systemui.Flags.FLAG_COMMUNAL_HUB +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.communal.domain.interactor.setCommunalEnabled +import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled import com.android.systemui.communal.shared.model.FakeGlanceableHubMultiUserHelper import com.android.systemui.communal.shared.model.fakeGlanceableHubMultiUserHelper import com.android.systemui.coroutines.collectLastValue @@ -54,10 +57,13 @@ import org.mockito.Mockito.never import org.mockito.Mockito.spy import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest -@RunWith(AndroidJUnit4::class) -class CommunalAppWidgetHostStartableTest : SysuiTestCase() { +@RunWith(ParameterizedAndroidJunit4::class) +@EnableFlags(FLAG_COMMUNAL_HUB) +class CommunalAppWidgetHostStartableTest(flags: FlagsParameterization) : SysuiTestCase() { private val kosmos = testKosmos() @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost @@ -71,12 +77,27 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { private lateinit var communalInteractorSpy: CommunalInteractor private lateinit var underTest: CommunalAppWidgetHostStartable + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + + companion object { + private val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN) + private val USER_INFO_WORK = UserInfo(10, "work", UserInfo.FLAG_PROFILE) + + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf(FLAG_GLANCEABLE_HUB_V2) + } + } + @Before fun setUp() { MockitoAnnotations.initMocks(this) kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO, USER_INFO_WORK)) kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) - mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB) + kosmos.setCommunalV2ConfigEnabled(true) widgetManager = kosmos.mockGlanceableHubWidgetManager helper = kosmos.fakeGlanceableHubMultiUserHelper @@ -327,9 +348,4 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { fakeKeyguardRepository.setKeyguardShowing(true) } } - - private companion object { - val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN) - val USER_INFO_WORK = UserInfo(10, "work", UserInfo.FLAG_PROFILE) - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/cursorposition/data/repository/MultiDisplayCursorPositionRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/cursorposition/data/repository/MultiDisplayCursorPositionRepositoryTest.kt new file mode 100644 index 000000000000..5609e8b7604c --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/cursorposition/data/repository/MultiDisplayCursorPositionRepositoryTest.kt @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.cursorposition.data.repository + +import android.os.Handler +import android.testing.TestableLooper +import android.testing.TestableLooper.RunWithLooper +import android.view.Display.DEFAULT_DISPLAY +import android.view.Display.TYPE_EXTERNAL +import android.view.Display.TYPE_INTERNAL +import android.view.InputDevice.SOURCE_MOUSE +import android.view.InputDevice.SOURCE_TOUCHPAD +import android.view.MotionEvent +import androidx.test.filters.SmallTest +import com.android.app.displaylib.PerDisplayInstanceRepositoryImpl +import com.android.systemui.SysuiTestCase +import com.android.systemui.cursorposition.data.model.CursorPosition +import com.android.systemui.cursorposition.data.repository.SingleDisplayCursorPositionRepositoryImpl.Companion.defaultInputEventListenerBuilder +import com.android.systemui.cursorposition.domain.data.repository.TestCursorPositionRepositoryInstanceProvider +import com.android.systemui.display.data.repository.display +import com.android.systemui.display.data.repository.displayRepository +import com.android.systemui.display.data.repository.fakeDisplayInstanceLifecycleManager +import com.android.systemui.display.data.repository.perDisplayDumpHelper +import com.android.systemui.kosmos.backgroundScope +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.shared.system.InputChannelCompat +import com.android.systemui.shared.system.InputMonitorCompat +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlinx.coroutines.launch +import org.junit.Before +import org.junit.Rule +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.kotlin.any +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters + +@SmallTest +@RunWith(ParameterizedAndroidJunit4::class) +@RunWithLooper +@kotlinx.coroutines.ExperimentalCoroutinesApi +class MultiDisplayCursorPositionRepositoryTest(private val cursorEventSource: Int) : + SysuiTestCase() { + + @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() + private lateinit var underTest: MultiDisplayCursorPositionRepository + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val displayRepository = kosmos.displayRepository + private val displayLifecycleManager = kosmos.fakeDisplayInstanceLifecycleManager + + private lateinit var listener: InputChannelCompat.InputEventListener + private var emittedCursorPosition: CursorPosition? = null + + @Mock private lateinit var inputMonitor: InputMonitorCompat + @Mock private lateinit var inputReceiver: InputChannelCompat.InputEventReceiver + + private val x = 100f + private val y = 200f + + private lateinit var testableLooper: TestableLooper + + @Before + fun setup() { + testableLooper = TestableLooper.get(this) + val testHandler = Handler(testableLooper.looper) + whenever(inputMonitor.getInputReceiver(any(), any(), any())).thenReturn(inputReceiver) + displayLifecycleManager.displayIds.value = setOf(DEFAULT_DISPLAY, DISPLAY_2) + + val cursorPerDisplayRepository = + PerDisplayInstanceRepositoryImpl( + debugName = "testCursorPositionPerDisplayInstanceRepository", + instanceProvider = + TestCursorPositionRepositoryInstanceProvider( + testHandler, + { channel -> + listener = defaultInputEventListenerBuilder.build(channel) + listener + }, + ) { _: String, _: Int -> + inputMonitor + }, + displayLifecycleManager, + kosmos.backgroundScope, + displayRepository, + kosmos.perDisplayDumpHelper, + ) + + underTest = + MultiDisplayCursorPositionRepositoryImpl( + displayRepository, + backgroundScope = kosmos.backgroundScope, + cursorPerDisplayRepository, + ) + } + + @Test + fun getCursorPositionFromDefaultDisplay() = setUpAndRunTest { + val event = getMotionEvent(x, y, 0) + listener.onInputEvent(event) + + assertThat(emittedCursorPosition).isEqualTo(CursorPosition(x, y, 0)) + } + + @Test + fun getCursorPositionFromAdditionDisplay() = setUpAndRunTest { + addDisplay(id = DISPLAY_2, type = TYPE_EXTERNAL) + + val event = getMotionEvent(x, y, DISPLAY_2) + listener.onInputEvent(event) + + assertThat(emittedCursorPosition).isEqualTo(CursorPosition(x, y, DISPLAY_2)) + } + + @Test + fun noCursorPositionFromRemovedDisplay() = setUpAndRunTest { + addDisplay(id = DISPLAY_2, type = TYPE_EXTERNAL) + removeDisplay(DISPLAY_2) + + val event = getMotionEvent(x, y, DISPLAY_2) + listener.onInputEvent(event) + + assertThat(emittedCursorPosition).isEqualTo(null) + } + + @Test + fun disposeInputMonitorAndInputReceiver() = setUpAndRunTest { + addDisplay(DISPLAY_2, TYPE_EXTERNAL) + removeDisplay(DISPLAY_2) + + verify(inputMonitor).dispose() + verify(inputReceiver).dispose() + } + + private fun setUpAndRunTest(block: suspend () -> Unit) = + kosmos.runTest { + // Add default display before creating cursor repository + displayRepository.addDisplays(display(id = DEFAULT_DISPLAY, type = TYPE_INTERNAL)) + + backgroundScope.launch { + underTest.cursorPositions.collect { emittedCursorPosition = it } + } + // Run all tasks received by TestHandler to create input monitors + testableLooper.processAllMessages() + + block() + } + + private suspend fun addDisplay(id: Int, type: Int) { + displayRepository.addDisplays(display(id = id, type = type)) + testableLooper.processAllMessages() + } + + private suspend fun removeDisplay(id: Int) { + displayRepository.removeDisplay(id) + testableLooper.processAllMessages() + } + + private fun getMotionEvent(x: Float, y: Float, displayId: Int): MotionEvent { + val event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, x, y, 0) + event.source = cursorEventSource + event.displayId = displayId + return event + } + + private companion object { + const val DISPLAY_2 = DEFAULT_DISPLAY + 1 + + @JvmStatic + @Parameters(name = "source = {0}") + fun data(): List<Int> { + return listOf(SOURCE_MOUSE, SOURCE_TOUCHPAD) + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt index d2d8ab9d5cb7..e6153e8ab337 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt @@ -20,6 +20,8 @@ import android.content.pm.UserInfo import android.hardware.biometrics.BiometricFaceConstants import android.hardware.biometrics.BiometricSourceType import android.os.PowerManager +import android.platform.test.annotations.EnableFlags +import android.service.dreams.Flags.FLAG_DREAMS_V2 import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState @@ -157,6 +159,33 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_DREAMS_V2) + fun faceAuthIsRequestedWhenTransitioningFromDreamToLockscreen() = + testScope.runTest { + underTest.start() + runCurrent() + + powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_LID) + faceWakeUpTriggersConfig.setTriggerFaceAuthOnWakeUpFrom( + setOf(WakeSleepReason.LID.powerManagerWakeReason) + ) + + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + KeyguardState.DREAMING, + KeyguardState.LOCKSCREEN, + transitionState = TransitionState.STARTED, + ) + ) + + runCurrent() + assertThat(faceAuthRepository.runningAuthRequest.value) + .isEqualTo( + Pair(FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED, true) + ) + } + + @Test fun whenFaceIsLockedOutAnyAttemptsToTriggerFaceAuthMustProvideLockoutError() = testScope.runTest { underTest.start() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt index 454c15667f22..89d3060d020a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt @@ -311,6 +311,20 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { assertThat(playSuccessHaptic).isNull() } + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun playSuccessHaptic_onDeviceEntry_fromDeviceEntryIcon() = + testScope.runTest { + underTest = kosmos.deviceEntryHapticsInteractor + val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry) + + kosmos.fakeKeyguardRepository.setKeyguardDismissible(true) + runCurrent() + kosmos.deviceEntrySourceInteractor.attemptEnterDeviceFromDeviceEntryIcon() + + assertThat(playSuccessHaptic).isNotNull() + } + // Mock dependencies for DeviceEntrySourceInteractor#deviceEntryFromBiometricSource private fun configureDeviceEntryFromBiometricSource( isFpUnlock: Boolean = false, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt index 90500839c8ad..a7810a69265a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt @@ -16,13 +16,17 @@ package com.android.systemui.deviceentry.domain.ui.viewmodel +import android.graphics.Point import android.platform.test.flag.junit.FlagsParameterization +import android.view.MotionEvent +import android.view.View import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.accessibility.data.repository.fakeAccessibilityRepository import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository +import com.android.systemui.biometrics.udfpsUtils import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.deviceentry.data.ui.viewmodel.alternateBouncerUdfpsAccessibilityOverlayViewModel import com.android.systemui.deviceentry.data.ui.viewmodel.deviceEntryUdfpsAccessibilityOverlayViewModel import com.android.systemui.deviceentry.ui.viewmodel.DeviceEntryUdfpsAccessibilityOverlayViewModel import com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER @@ -34,6 +38,7 @@ import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepos import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.keyguard.ui.viewmodel.accessibilityActionsViewModelKosmos import com.android.systemui.keyguard.ui.viewmodel.fakeDeviceEntryIconViewModelTransition import com.android.systemui.kosmos.testScope import com.android.systemui.res.R @@ -41,14 +46,22 @@ import com.android.systemui.shade.shadeTestUtil import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Before import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(ParameterizedAndroidJunit4::class) class UdfpsAccessibilityOverlayViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { @@ -63,7 +76,6 @@ class UdfpsAccessibilityOverlayViewModelTest(flags: FlagsParameterization) : Sys private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository private val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository private val deviceEntryFingerprintAuthRepository = kosmos.deviceEntryFingerprintAuthRepository - private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository private val shadeTestUtil by lazy { kosmos.shadeTestUtil } @@ -83,6 +95,22 @@ class UdfpsAccessibilityOverlayViewModelTest(flags: FlagsParameterization) : Sys @Before fun setup() { + whenever(kosmos.udfpsUtils.isWithinSensorArea(any(), any(), any())).thenReturn(false) + whenever( + kosmos.udfpsUtils.getTouchInNativeCoordinates(anyInt(), any(), any(), anyBoolean()) + ) + .thenReturn(Point(0, 0)) + whenever( + kosmos.udfpsUtils.onTouchOutsideOfSensorArea( + anyBoolean(), + eq(null), + anyInt(), + anyInt(), + any(), + anyBoolean(), + ) + ) + .thenReturn("Move left") underTest = kosmos.deviceEntryUdfpsAccessibilityOverlayViewModel overrideResource(R.integer.udfps_padding_debounce_duration, 0) } @@ -101,6 +129,55 @@ class UdfpsAccessibilityOverlayViewModelTest(flags: FlagsParameterization) : Sys } @Test + fun contentDescription_setOnUdfpsTouchOutsideSensorArea() = + testScope.runTest { + val contentDescription by collectLastValue(underTest.contentDescription) + setupVisibleStateOnLockscreen() + underTest.onHoverEvent(mock<View>(), mock<MotionEvent>()) + runCurrent() + assertThat(contentDescription).isEqualTo("Move left") + } + + @Test + fun clearAccessibilityOverlayMessageReason_updatesWhenFocusChangesFromUdfpsOverlayToLockscreen() = + testScope.runTest { + val clearAccessibilityOverlayMessageReason by + collectLastValue(underTest.clearAccessibilityOverlayMessageReason) + val contentDescription by collectLastValue(underTest.contentDescription) + setupVisibleStateOnLockscreen() + kosmos.accessibilityActionsViewModelKosmos.clearUdfpsAccessibilityOverlayMessage("test") + runCurrent() + assertThat(clearAccessibilityOverlayMessageReason).isEqualTo("test") + + // UdfpsAccessibilityOverlayViewBinder collects clearAccessibilityOverlayMessageReason + // and calls + // viewModel.setContentDescription(null) - mock this here + underTest.setContentDescription(null) + runCurrent() + assertThat(contentDescription).isNull() + } + + @Test + fun clearAccessibilityOverlayMessageReason_updatesAfterUdfpsOverlayFocusOnAlternateBouncer() = + testScope.runTest { + val clearAccessibilityOverlayMessageReason by + collectLastValue(underTest.clearAccessibilityOverlayMessageReason) + val contentDescription by collectLastValue(underTest.contentDescription) + setupVisibleStateOnLockscreen() + kosmos.alternateBouncerUdfpsAccessibilityOverlayViewModel + .clearUdfpsAccessibilityOverlayMessage("test") + runCurrent() + assertThat(clearAccessibilityOverlayMessageReason).isEqualTo("test") + + // UdfpsAccessibilityOverlayViewBinder collects clearAccessibilityOverlayMessageReason + // and calls + // viewModel.setContentDescription(null) - mock this here + underTest.setContentDescription(null) + runCurrent() + assertThat(contentDescription).isNull() + } + + @Test fun touchExplorationNotEnabled_overlayNotVisible() = testScope.runTest { val visible by collectLastValue(underTest.visible) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java index 3895595aaea6..33c4c44111aa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java @@ -16,6 +16,8 @@ package com.android.systemui.dreams; +import static com.android.systemui.Flags.FLAG_BOUNCER_UI_REVAMP; + import static kotlinx.coroutines.flow.FlowKt.emptyFlow; import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow; @@ -32,6 +34,7 @@ import android.app.DreamManager; import android.content.res.Resources; import android.graphics.Region; import android.os.Handler; +import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.testing.TestableLooper.RunWithLooper; import android.view.AttachedSurfaceControl; @@ -231,6 +234,7 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { } @Test + @DisableFlags(FLAG_BOUNCER_UI_REVAMP) public void testBouncerAnimation_updateBlur() { final ArgumentCaptor<PrimaryBouncerExpansionCallback> bouncerExpansionCaptor = ArgumentCaptor.forClass(PrimaryBouncerExpansionCallback.class); @@ -253,6 +257,26 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { } @Test + @EnableFlags(FLAG_BOUNCER_UI_REVAMP) + public void testBouncerAnimation_doesNotBlur_whenBouncerRevampEnabled() { + final ArgumentCaptor<PrimaryBouncerExpansionCallback> bouncerExpansionCaptor = + ArgumentCaptor.forClass(PrimaryBouncerExpansionCallback.class); + mController.onViewAttached(); + verify(mPrimaryBouncerCallbackInteractor).addBouncerExpansionCallback( + bouncerExpansionCaptor.capture()); + + final float blurRadius = 1337f; + when(mBlurUtils.blurRadiusOfRatio(anyFloat())).thenReturn(blurRadius); + + bouncerExpansionCaptor.getValue().onStartingToShow(); + final float bouncerHideAmount = 0.05f; + + bouncerExpansionCaptor.getValue().onExpansionChanged(bouncerHideAmount); + verify(mBlurUtils, never()).blurRadiusOfRatio(anyFloat()); + verify(mBlurUtils, never()).applyBlur(eq(mViewRoot), anyInt(), anyBoolean()); + } + + @Test public void testStartDreamEntryAnimationsOnAttachedNonLowLight() { when(mStateController.isLowLightActive()).thenReturn(false); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt index fd99313a17b7..b74d53987503 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt @@ -74,6 +74,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.navigationbar.gestural.domain.GestureInteractor import com.android.systemui.navigationbar.gestural.domain.TaskInfo import com.android.systemui.navigationbar.gestural.domain.TaskMatcher +import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.data.repository.sceneContainerRepository import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Overlays @@ -265,6 +266,8 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() { mDreamOverlayCallbackController, kosmos.keyguardInteractor, gestureInteractor, + kosmos.wakeGestureMonitor, + kosmos.powerInteractor, WINDOW_NAME, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/WakeGestureMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/WakeGestureMonitorTest.kt new file mode 100644 index 000000000000..b5f8f7884d7f --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/WakeGestureMonitorTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams + +import android.hardware.Sensor +import android.hardware.TriggerEventListener +import android.hardware.display.ambientDisplayConfiguration +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.collectValues +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.testKosmos +import com.android.systemui.util.sensors.asyncSensorManager +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub + +@SmallTest +@RunWith(AndroidJUnit4::class) +class WakeGestureMonitorTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val Kosmos.underTest by Kosmos.Fixture { wakeGestureMonitor } + + @Test + fun testPickupGestureNotEnabled_doesNotSubscribeToSensor() = + kosmos.runTest { + ambientDisplayConfiguration.fakePickupGestureEnabled = false + val triggerSensor = stubSensorManager() + + val wakeUpDetected by collectValues(underTest.wakeUpDetected) + triggerSensor() + assertThat(wakeUpDetected).isEmpty() + } + + @Test + fun testPickupGestureEnabled_subscribesToSensor() = + kosmos.runTest { + ambientDisplayConfiguration.fakePickupGestureEnabled = true + val triggerSensor = stubSensorManager() + + val wakeUpDetected by collectValues(underTest.wakeUpDetected) + triggerSensor() + assertThat(wakeUpDetected).hasSize(1) + triggerSensor() + assertThat(wakeUpDetected).hasSize(2) + } + + private fun Kosmos.stubSensorManager(): () -> Unit { + val callbacks = mutableListOf<TriggerEventListener>() + val pickupSensor = mock<Sensor>() + + asyncSensorManager.stub { + on { getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE) } doReturn pickupSensor + on { requestTriggerSensor(any(), eq(pickupSensor)) } doAnswer + { + val callback = it.arguments[0] as TriggerEventListener + callbacks.add(callback) + true + } + on { cancelTriggerSensor(any(), any()) } doAnswer + { + val callback = it.arguments[0] as TriggerEventListener + callbacks.remove(callback) + true + } + } + + return { + val list = callbacks.toList() + // Simulate a trigger sensor which unregisters callbacks after triggering. + while (callbacks.isNotEmpty()) { + callbacks.removeLast() + } + list.forEach { it.onTrigger(mock()) } + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCustomizationModeRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCustomizationModeRepositoryTest.kt new file mode 100644 index 000000000000..e955ca85c8e2 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCustomizationModeRepositoryTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.shortcut.data.repository + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyboard.shortcut.shortcutHelperCustomizationModeRepository +import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ShortcutCustomizationModeRepositoryTest : SysuiTestCase() { + + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val repo = kosmos.shortcutHelperCustomizationModeRepository + private val testScope = kosmos.testScope + private val helper = kosmos.shortcutHelperTestHelper + + @Test + fun customizationMode_disabledByDefault() { + testScope.runTest { + val customizationMode by collectLastValue(repo.isCustomizationModeEnabled) + + assertThat(customizationMode).isFalse() + } + } + + @Test + fun customizationMode_enabledOnRequest_whenShortcutHelperIsOpen() { + testScope.runTest { + val customizationMode by collectLastValue(repo.isCustomizationModeEnabled) + helper.showFromActivity() + repo.toggleCustomizationMode(isCustomizing = true) + assertThat(customizationMode).isTrue() + } + } + + @Test + fun customizationMode_disabledOnRequest_whenShortcutHelperIsOpen() { + testScope.runTest { + val customizationMode by collectLastValue(repo.isCustomizationModeEnabled) + helper.showFromActivity() + repo.toggleCustomizationMode(isCustomizing = true) + repo.toggleCustomizationMode(isCustomizing = false) + assertThat(customizationMode).isFalse() + } + } + + @Test + fun customizationMode_disabledWhenShortcutHelperIsDismissed() { + testScope.runTest { + val customizationMode by collectLastValue(repo.isCustomizationModeEnabled) + helper.showFromActivity() + repo.toggleCustomizationMode(isCustomizing = true) + helper.hideFromActivity() + assertThat(customizationMode).isFalse() + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/UserVisibleAppsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/UserVisibleAppsRepositoryTest.kt new file mode 100644 index 000000000000..a2e42976f413 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/UserVisibleAppsRepositoryTest.kt @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.shortcut.data.repository + +import android.content.pm.UserInfo +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.coroutines.collectLastValue +import com.android.systemui.keyboard.shortcut.fakeLauncherApps +import com.android.systemui.keyboard.shortcut.userVisibleAppsRepository +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.settings.fakeUserTracker +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class UserVisibleAppsRepositoryTest : SysuiTestCase() { + + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val fakeLauncherApps = kosmos.fakeLauncherApps + private val repo = kosmos.userVisibleAppsRepository + private val userTracker = kosmos.fakeUserTracker + private val testScope = kosmos.testScope + private val userVisibleAppsContainsApplication: + (pkgName: String, clsName: String) -> Flow<Boolean> = + { pkgName, clsName -> + repo.userVisibleApps.map { userVisibleApps -> + userVisibleApps.any { + it.componentName.packageName == pkgName && it.componentName.className == clsName + } + } + } + + @Before + fun setup() { + switchUser(index = PRIMARY_USER_INDEX) + } + + @Test + fun userVisibleApps_emitsUpdatedAppsList_onNewAppInstalled() { + testScope.runTest { + val containsPackageOne by + collectLastValue(userVisibleAppsContainsApplication(TEST_PACKAGE_1, TEST_CLASS_1)) + + installPackageOneForUserOne() + + assertThat(containsPackageOne).isTrue() + } + } + + @Test + fun userVisibleApps_emitsUpdatedAppsList_onAppUserChanged() { + testScope.runTest { + val containsPackageOne by + collectLastValue(userVisibleAppsContainsApplication(TEST_PACKAGE_1, TEST_CLASS_1)) + val containsPackageTwo by + collectLastValue(userVisibleAppsContainsApplication(TEST_PACKAGE_2, TEST_CLASS_2)) + + installPackageOneForUserOne() + installPackageTwoForUserTwo() + + assertThat(containsPackageOne).isTrue() + assertThat(containsPackageTwo).isFalse() + + switchUser(index = SECONDARY_USER_INDEX) + + assertThat(containsPackageOne).isFalse() + assertThat(containsPackageTwo).isTrue() + } + } + + @Test + fun userVisibleApps_emitsUpdatedAppsList_onAppUninstalled() { + testScope.runTest { + val containsPackageOne by + collectLastValue(userVisibleAppsContainsApplication(TEST_PACKAGE_1, TEST_CLASS_1)) + + installPackageOneForUserOne() + uninstallPackageOneForUserOne() + + assertThat(containsPackageOne).isFalse() + } + } + + private fun switchUser(index: Int) { + userTracker.set( + userInfos = + listOf( + UserInfo(/* id= */ PRIMARY_USER_ID, /* name= */ "Primary User", /* flags= */ 0), + UserInfo( + /* id= */ SECONDARY_USER_ID, + /* name= */ "Secondary User", + /* flags= */ 0, + ), + ), + selectedUserIndex = index, + ) + } + + private fun installPackageOneForUserOne() { + fakeLauncherApps.installPackageForUser( + TEST_PACKAGE_1, + TEST_CLASS_1, + UserHandle(/* userId= */ PRIMARY_USER_ID), + ) + } + + private fun uninstallPackageOneForUserOne() { + fakeLauncherApps.uninstallPackageForUser( + TEST_PACKAGE_1, + TEST_CLASS_1, + UserHandle(/* userId= */ PRIMARY_USER_ID), + ) + } + + private fun installPackageTwoForUserTwo() { + fakeLauncherApps.installPackageForUser( + TEST_PACKAGE_2, + TEST_CLASS_2, + UserHandle(/* userId= */ SECONDARY_USER_ID), + ) + } + + companion object { + const val TEST_PACKAGE_1 = "test.package.one" + const val TEST_PACKAGE_2 = "test.package.two" + const val TEST_CLASS_1 = "TestClassOne" + const val TEST_CLASS_2 = "TestClassTwo" + const val PRIMARY_USER_ID = 10 + const val PRIMARY_USER_INDEX = 0 + const val SECONDARY_USER_ID = 11 + const val SECONDARY_USER_INDEX = 1 + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt index af6c65ec6d6d..1f74ad496bbb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt @@ -533,7 +533,7 @@ object TestShortcuts { val expectedShortcutCategoriesWithSimpleShortcutCombination = listOf( - simpleShortcutCategory(System, "System apps", "Open assistant"), + simpleShortcutCategory(System, "System apps", "Open digital assistant"), simpleShortcutCategory(System, "System controls", "Go to home screen"), simpleShortcutCategory(System, "System apps", "Open settings"), simpleShortcutCategory(System, "System controls", "Lock screen"), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt index 766744885077..d4292bf5b66c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt @@ -56,7 +56,6 @@ import com.android.systemui.keyboard.shortcut.shortcutHelperViewModel import com.android.systemui.keyboard.shortcut.ui.model.IconSource import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCategoryUi import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState -import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.model.sysUiState @@ -66,7 +65,8 @@ import com.android.systemui.settings.userTracker import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SHORTCUT_HELPER_SHOWING import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlin.test.assertFalse +import kotlin.test.assertTrue import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -89,7 +89,6 @@ class ShortcutHelperViewModelTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher().also { - it.testDispatcher = UnconfinedTestDispatcher() it.shortcutHelperSystemShortcutsSource = fakeSystemSource it.shortcutHelperMultiTaskingShortcutsSource = fakeMultiTaskingSource it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource() @@ -445,6 +444,51 @@ class ShortcutHelperViewModelTest : SysuiTestCase() { assertThat((uiState as? ShortcutsUiState.Active)?.searchQuery).isEqualTo("") } + @Test + fun shortcutsUiState_customizationModeDisabledByDefault() { + testScope.runTest { + testHelper.showFromActivity() + val uiState by collectLastValue(viewModel.shortcutsUiState) + + assertFalse((uiState as ShortcutsUiState.Active).isCustomizationModeEnabled) + } + } + + @Test + fun shortcutsUiState_customizationModeEnabledOnRequest() { + testScope.runTest { + testHelper.showFromActivity() + val uiState by collectLastValue(viewModel.shortcutsUiState) + viewModel.toggleCustomizationMode(true) + + assertTrue((uiState as ShortcutsUiState.Active).isCustomizationModeEnabled) + } + } + + @Test + fun shortcutsUiState_customizationModeDisabledOnRequest() { + testScope.runTest { + testHelper.showFromActivity() + val uiState by collectLastValue(viewModel.shortcutsUiState) + viewModel.toggleCustomizationMode(true) + viewModel.toggleCustomizationMode(false) + + assertFalse((uiState as ShortcutsUiState.Active).isCustomizationModeEnabled) + } + } + + @Test + fun shortcutsUiState_customizationModeDisabledWhenShortcutHelperIsReopened() { + testScope.runTest { + testHelper.showFromActivity() + val uiState by collectLastValue(viewModel.shortcutsUiState) + viewModel.toggleCustomizationMode(true) + closeAndReopenShortcutHelper() + + assertFalse((uiState as ShortcutsUiState.Active).isCustomizationModeEnabled) + } + } + private fun openHelperAndSearchForFooString() { testHelper.showFromActivity() viewModel.onSearchQueryChanged("foo") diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt index 57b12990fb97..f88b8529866b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt @@ -21,8 +21,12 @@ import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils -import com.android.systemui.Flags +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 +import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.communalSceneRepository +import com.android.systemui.communal.domain.interactor.setCommunalV2Enabled +import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepositorySpy @@ -32,10 +36,12 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.testKosmos -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest +import com.google.common.truth.Truth import org.junit.Before import org.junit.Ignore import org.junit.Test @@ -46,12 +52,10 @@ import org.mockito.Mockito.reset @RunWith(AndroidJUnit4::class) class FromGoneTransitionInteractorTest : SysuiTestCase() { private val kosmos = - testKosmos().apply { + testKosmos().useUnconfinedTestDispatcher().apply { this.keyguardTransitionRepository = fakeKeyguardTransitionRepositorySpy } - private val testScope = kosmos.testScope private val underTest = kosmos.fromGoneTransitionInteractor - private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepositorySpy @Before fun setUp() { @@ -61,8 +65,8 @@ class FromGoneTransitionInteractorTest : SysuiTestCase() { @Test @Ignore("Fails due to fix for b/324432820 - will re-enable once permanent fix is submitted.") fun testDoesNotTransitionToLockscreen_ifStartedButNotFinishedInGone() = - testScope.runTest { - keyguardTransitionRepository.sendTransitionSteps( + kosmos.runTest { + fakeKeyguardTransitionRepositorySpy.sendTransitionSteps( listOf( TransitionStep( from = KeyguardState.LOCKSCREEN, @@ -77,54 +81,74 @@ class FromGoneTransitionInteractorTest : SysuiTestCase() { ), testScope, ) - reset(keyguardTransitionRepository) - kosmos.fakeKeyguardRepository.setKeyguardShowing(true) - runCurrent() + reset(fakeKeyguardTransitionRepositorySpy) + fakeKeyguardRepository.setKeyguardShowing(true) // We're in the middle of a LOCKSCREEN -> GONE transition. - assertThat(keyguardTransitionRepository).noTransitionsStarted() + assertThat(fakeKeyguardTransitionRepositorySpy).noTransitionsStarted() } @Test - @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + @DisableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR) fun testTransitionsToLockscreen_ifFinishedInGone() = - testScope.runTest { - keyguardTransitionRepository.sendTransitionSteps( + kosmos.runTest { + fakeKeyguardTransitionRepositorySpy.sendTransitionSteps( from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE, testScope, ) - reset(keyguardTransitionRepository) - kosmos.fakeKeyguardRepository.setKeyguardShowing(true) - runCurrent() + reset(fakeKeyguardTransitionRepositorySpy) + fakeKeyguardRepository.setKeyguardShowing(true) // We're in the middle of a GONE -> LOCKSCREEN transition. - assertThat(keyguardTransitionRepository) + assertThat(fakeKeyguardTransitionRepositorySpy) .startedTransition(to = KeyguardState.LOCKSCREEN) } @Test - @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR) fun testTransitionsToLockscreen_ifFinishedInGone_wmRefactor() = - testScope.runTest { - keyguardTransitionRepository.sendTransitionSteps( + kosmos.runTest { + fakeKeyguardTransitionRepositorySpy.sendTransitionSteps( from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE, testScope, ) - reset(keyguardTransitionRepository) + reset(fakeKeyguardTransitionRepositorySpy) // Trigger lockdown. - kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags( + fakeBiometricSettingsRepository.setAuthenticationFlags( AuthenticationFlags( 0, LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN, ) ) - runCurrent() // We're in the middle of a GONE -> LOCKSCREEN transition. - assertThat(keyguardTransitionRepository) + assertThat(fakeKeyguardTransitionRepositorySpy) .startedTransition(to = KeyguardState.LOCKSCREEN) } + + @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) + @DisableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionToGlanceableHub() = + kosmos.runTest { + val currentScene by collectLastValue(communalSceneRepository.currentScene) + + fakeKeyguardTransitionRepositorySpy.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + testScope, + ) + reset(fakeKeyguardTransitionRepositorySpy) + // Communal is enabled + setCommunalV2Enabled(true) + Truth.assertThat(currentScene).isEqualTo(CommunalScenes.Blank) + + fakeKeyguardRepository.setKeyguardShowing(true) + + Truth.assertThat(currentScene).isEqualTo(CommunalScenes.Communal) + assertThat(fakeKeyguardTransitionRepositorySpy).noTransitionsStarted() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt index d0762a3797c0..807cab7cf1b6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt @@ -43,12 +43,15 @@ import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito.anyInt +import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.MockitoAnnotations import org.mockito.kotlin.any +import org.mockito.kotlin.clearInvocations import org.mockito.kotlin.doAnswer import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.whenever @SmallTest @@ -59,6 +62,7 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { private lateinit var underTest: WindowManagerLockscreenVisibilityManager private lateinit var executor: FakeExecutor + private lateinit var uiBgExecutor: FakeExecutor @Mock private lateinit var activityTaskManagerService: IActivityTaskManager @Mock private lateinit var keyguardStateController: KeyguardStateController @@ -74,10 +78,12 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) executor = FakeExecutor(FakeSystemClock()) + uiBgExecutor = FakeExecutor(FakeSystemClock()) underTest = WindowManagerLockscreenVisibilityManager( executor = executor, + uiBgExecutor = uiBgExecutor, activityTaskManagerService = activityTaskManagerService, keyguardStateController = keyguardStateController, keyguardSurfaceBehindAnimator = keyguardSurfaceBehindAnimator, @@ -93,8 +99,10 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { @RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) fun testLockscreenVisible_andAodVisible_without_keyguard_shell_transitions() { underTest.setLockscreenShown(true) + uiBgExecutor.runAllReady() verify(activityTaskManagerService).setLockScreenShown(true, false) underTest.setAodVisible(true) + uiBgExecutor.runAllReady() verify(activityTaskManagerService).setLockScreenShown(true, true) verifyNoMoreInteractions(activityTaskManagerService) @@ -104,8 +112,10 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { @RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) fun testLockscreenVisible_andAodVisible_with_keyguard_shell_transitions() { underTest.setLockscreenShown(true) + uiBgExecutor.runAllReady() verify(keyguardTransitions).startKeyguardTransition(true, false) underTest.setAodVisible(true) + uiBgExecutor.runAllReady() verify(keyguardTransitions).startKeyguardTransition(true, true) verifyNoMoreInteractions(keyguardTransitions) @@ -115,13 +125,16 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { @RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) fun testGoingAway_whenLockscreenVisible_thenSurfaceMadeVisible_without_keyguard_shell_transitions() { underTest.setLockscreenShown(true) + uiBgExecutor.runAllReady() verify(activityTaskManagerService).setLockScreenShown(true, false) underTest.setAodVisible(true) + uiBgExecutor.runAllReady() verify(activityTaskManagerService).setLockScreenShown(true, true) verifyNoMoreInteractions(activityTaskManagerService) underTest.setSurfaceBehindVisibility(true) + uiBgExecutor.runAllReady() verify(activityTaskManagerService).keyguardGoingAway(anyInt()) verifyNoMoreInteractions(activityTaskManagerService) @@ -131,13 +144,16 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { @RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) fun testGoingAway_whenLockscreenVisible_thenSurfaceMadeVisible_with_keyguard_shell_transitions() { underTest.setLockscreenShown(true) + uiBgExecutor.runAllReady() verify(keyguardTransitions).startKeyguardTransition(true, false) underTest.setAodVisible(true) + uiBgExecutor.runAllReady() verify(keyguardTransitions).startKeyguardTransition(true, true) verifyNoMoreInteractions(keyguardTransitions) underTest.setSurfaceBehindVisibility(true) + uiBgExecutor.runAllReady() verify(keyguardTransitions).startKeyguardTransition(false, false) verifyNoMoreInteractions(keyguardTransitions) @@ -148,11 +164,13 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { fun testSurfaceVisible_whenLockscreenNotShowing_doesNotTriggerGoingAway_without_keyguard_shell_transitions() { underTest.setLockscreenShown(false) underTest.setAodVisible(false) + uiBgExecutor.runAllReady() verify(activityTaskManagerService).setLockScreenShown(false, false) verifyNoMoreInteractions(activityTaskManagerService) underTest.setSurfaceBehindVisibility(true) + uiBgExecutor.runAllReady() verifyNoMoreInteractions(activityTaskManagerService) } @@ -162,11 +180,13 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { fun testSurfaceVisible_whenLockscreenNotShowing_doesNotTriggerGoingAway_with_keyguard_shell_transitions() { underTest.setLockscreenShown(false) underTest.setAodVisible(false) + uiBgExecutor.runAllReady() verify(keyguardTransitions).startKeyguardTransition(false, false) verifyNoMoreInteractions(keyguardTransitions) underTest.setSurfaceBehindVisibility(true) + uiBgExecutor.runAllReady() verifyNoMoreInteractions(keyguardTransitions) } @@ -175,9 +195,11 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { @RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) fun testAodVisible_noLockscreenShownCallYet_doesNotShowLockscreenUntilLater_without_keyguard_shell_transitions() { underTest.setAodVisible(false) + uiBgExecutor.runAllReady() verifyNoMoreInteractions(activityTaskManagerService) underTest.setLockscreenShown(true) + uiBgExecutor.runAllReady() verify(activityTaskManagerService).setLockScreenShown(true, false) verifyNoMoreInteractions(activityTaskManagerService) } @@ -186,9 +208,11 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { @RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) fun testAodVisible_noLockscreenShownCallYet_doesNotShowLockscreenUntilLater_with_keyguard_shell_transitions() { underTest.setAodVisible(false) + uiBgExecutor.runAllReady() verifyNoMoreInteractions(keyguardTransitions) underTest.setLockscreenShown(true) + uiBgExecutor.runAllReady() verify(keyguardTransitions).startKeyguardTransition(true, false) verifyNoMoreInteractions(activityTaskManagerService) } @@ -197,10 +221,13 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { @RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) fun setSurfaceBehindVisibility_goesAwayFirst_andIgnoresSecondCall_without_keyguard_shell_transitions() { underTest.setLockscreenShown(true) + uiBgExecutor.runAllReady() underTest.setSurfaceBehindVisibility(true) + uiBgExecutor.runAllReady() verify(activityTaskManagerService).keyguardGoingAway(0) underTest.setSurfaceBehindVisibility(true) + uiBgExecutor.runAllReady() verifyNoMoreInteractions(keyguardTransitions) } @@ -208,22 +235,31 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { @RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) fun setSurfaceBehindVisibility_goesAwayFirst_andIgnoresSecondCall_with_keyguard_shell_transitions() { underTest.setLockscreenShown(true) + uiBgExecutor.runAllReady() verify(keyguardTransitions).startKeyguardTransition(true, false) underTest.setSurfaceBehindVisibility(true) + uiBgExecutor.runAllReady() verify(keyguardTransitions).startKeyguardTransition(false, false) underTest.setSurfaceBehindVisibility(true) + uiBgExecutor.runAllReady() verifyNoMoreInteractions(keyguardTransitions) } @Test @RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) fun setSurfaceBehindVisibility_falseSetsLockscreenVisibility_without_keyguard_shell_transitions() { - // Show the surface behind, then hide it. underTest.setLockscreenShown(true) + uiBgExecutor.runAllReady() + verify(activityTaskManagerService).setLockScreenShown(eq(true), any()) + + // Show the surface behind, then hide it. underTest.setSurfaceBehindVisibility(true) + uiBgExecutor.runAllReady() underTest.setSurfaceBehindVisibility(false) - verify(activityTaskManagerService).setLockScreenShown(eq(true), any()) + uiBgExecutor.runAllReady() + + verify(activityTaskManagerService, times(2)).setLockScreenShown(eq(true), any()) } @Test @@ -233,6 +269,7 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { underTest.setLockscreenShown(true) underTest.setSurfaceBehindVisibility(true) underTest.setSurfaceBehindVisibility(false) + uiBgExecutor.runAllReady() verify(keyguardTransitions).startKeyguardTransition(eq(true), any()) } @@ -258,4 +295,33 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { verify(mockedCallback).onAnimationFinished() verifyNoMoreInteractions(mockedCallback) } + + @Test + fun lockscreenEventuallyShown_ifReshown_afterGoingAwayExecutionDelayed() { + underTest.setLockscreenShown(true) + uiBgExecutor.runAllReady() + clearInvocations(activityTaskManagerService) + + // Trigger keyguardGoingAway, then immediately setLockScreenShown before going away runs on + // the uiBgExecutor. + underTest.setSurfaceBehindVisibility(true) + underTest.setLockscreenShown(true) + + // Next ready should be the keyguardGoingAway call. + uiBgExecutor.runNextReady() + verify(activityTaskManagerService).keyguardGoingAway(anyInt()) + verify(activityTaskManagerService, never()).setLockScreenShown(any(), any()) + clearInvocations(activityTaskManagerService) + + // Then, the setLockScreenShown call, which should have been enqueued when we called + // setLockScreenShown(true) even though keyguardGoingAway() hadn't yet been called. + uiBgExecutor.runNextReady() + verify(activityTaskManagerService).setLockScreenShown(eq(true), any()) + verify(activityTaskManagerService, never()).keyguardGoingAway(anyInt()) + clearInvocations(activityTaskManagerService) + + // Shouldn't be anything left in the queue. + uiBgExecutor.runAllReady() + verifyNoMoreInteractions(activityTaskManagerService) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerTransitionViewModelTest.kt new file mode 100644 index 000000000000..c515fc394bda --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerTransitionViewModelTest.kt @@ -0,0 +1,63 @@ +/* + * 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.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectValues +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.keyguard.ui.transitions.blurConfig +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DreamingToPrimaryBouncerTransitionViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val underTest by lazy { kosmos.dreamingToPrimaryBouncerViewModel } + + @Test + fun dreamingToPrimaryBouncerChangesBlurToMax() = + testScope.runTest { + val values by collectValues(underTest.windowBlurRadius) + + kosmos.keyguardWindowBlurTestUtil.assertTransitionToBlurRadius( + transitionProgress = listOf(0.0f, 0.0f, 0.3f, 0.4f, 0.5f, 1.0f), + startValue = kosmos.blurConfig.maxBlurRadiusPx, + endValue = kosmos.blurConfig.maxBlurRadiusPx, + transitionFactory = ::step, + actualValuesProvider = { values }, + checkInterpolatedValues = false, + ) + } + + private fun step(value: Float, transitionState: TransitionState = RUNNING) = + TransitionStep( + from = KeyguardState.DREAMING, + to = KeyguardState.PRIMARY_BOUNCER, + value = value, + transitionState = transitionState, + ownerName = "dreamingToPrimaryBouncerTransitionViewModelTest", + ) +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt index 3ab920a46084..cdd093a410df 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt @@ -17,11 +17,20 @@ package com.android.systemui.keyguard.ui.viewmodel import android.content.res.Configuration +import android.content.res.mainResources +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization import android.util.LayoutDirection -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.ObservableTransitionState +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository +import com.android.systemui.communal.data.repository.communalSceneRepository +import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled +import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState @@ -29,30 +38,53 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.keyguard.ui.transitions.blurConfig import com.android.systemui.kosmos.collectValues +import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.policy.keyguardStateController import com.android.systemui.testKosmos import com.google.common.collect.Range import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock import org.mockito.kotlin.whenever +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest -@RunWith(AndroidJUnit4::class) -class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() { - val kosmos = testKosmos() +@RunWith(ParameterizedAndroidJunit4::class) +class GlanceableHubToLockscreenTransitionViewModelTest(flags: FlagsParameterization) : + SysuiTestCase() { + val kosmos = testKosmos().apply { mainResources = mContext.orCreateTestableResources.resources } val testScope = kosmos.testScope val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository val configurationRepository = kosmos.fakeConfigurationRepository + val keyguardStateController: KeyguardStateController = kosmos.keyguardStateController val underTest by lazy { kosmos.glanceableHubToLockscreenTransitionViewModel } + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf(FLAG_GLANCEABLE_HUB_V2) + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + @Test fun lockscreenFadeIn() = kosmos.runTest { + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + val values by collectValues(underTest.keyguardAlpha) assertThat(values).isEmpty() @@ -79,6 +111,116 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) + fun lockscreenFadeIn_fromHubInLandscape() = + kosmos.runTest { + kosmos.setCommunalV2ConfigEnabled(true) + whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false) + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + communalSceneRepository.setCommunalContainerOrientation( + Configuration.ORIENTATION_LANDSCAPE + ) + + val values by collectValues(underTest.keyguardAlpha) + assertThat(values).isEmpty() + + // Exit hub to lockscreen + val progress = MutableStateFlow(0f) + val transitionState = + MutableStateFlow( + ObservableTransitionState.Transition( + fromScene = CommunalScenes.Communal, + toScene = CommunalScenes.Blank, + currentScene = flowOf(CommunalScenes.Blank), + progress = progress, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + ) + communalSceneInteractor.setTransitionState(transitionState) + progress.value = .2f + + // Still in landscape + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + step(0.1f), + // start here.. + step(0.5f), + ), + testScope, + ) + + // Communal container is rotated to portrait + communalSceneRepository.setCommunalContainerOrientation( + Configuration.ORIENTATION_PORTRAIT + ) + runCurrent() + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0.6f), + step(0.7f), + // should stop here.. + step(0.8f), + step(1f), + ), + testScope, + ) + // Scene transition finished. + progress.value = 1f + keyguardTransitionRepository.sendTransitionSteps( + listOf(step(1f, TransitionState.FINISHED)), + testScope, + ) + + assertThat(values).hasSize(4) + // onStart + assertThat(values[0]).isEqualTo(0f) + assertThat(values[1]).isEqualTo(0f) + assertThat(values[2]).isEqualTo(1f) + // onFinish + assertThat(values[3]).isEqualTo(1f) + } + + @Test + @DisableFlags(FLAG_GLANCEABLE_HUB_V2) + fun lockscreenFadeIn_v2FlagDisabledAndFromHubInLandscape() = + kosmos.runTest { + whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false) + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + // Rotation is not enabled so communal container is in portrait. + communalSceneRepository.setCommunalContainerOrientation( + Configuration.ORIENTATION_PORTRAIT + ) + + val values by collectValues(underTest.keyguardAlpha) + assertThat(values).isEmpty() + + // Exit hub to lockscreen + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + // Should start running here... + step(0.1f), + step(0.2f), + step(0.3f), + step(0.4f), + // ...up to here + step(0.5f), + step(0.6f), + step(0.7f), + step(0.8f), + step(1f), + ), + testScope, + ) + + assertThat(values).hasSize(4) + values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } + } + + @Test fun lockscreenTranslationX() = kosmos.runTest { val config: Configuration = mock() @@ -89,6 +231,8 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() { R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x, 100, ) + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + val values by collectValues(underTest.keyguardTranslationX) assertThat(values).isEmpty() @@ -108,6 +252,44 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) + fun lockscreenTranslationX_fromHubInLandscape() = + kosmos.runTest { + kosmos.setCommunalV2ConfigEnabled(true) + val config: Configuration = mock() + whenever(config.layoutDirection).thenReturn(LayoutDirection.LTR) + configurationRepository.onConfigurationChange(config) + + configurationRepository.setDimensionPixelSize( + R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x, + 100, + ) + whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false) + + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + communalSceneRepository.setCommunalContainerOrientation( + Configuration.ORIENTATION_LANDSCAPE + ) + + val values by collectValues(underTest.keyguardTranslationX) + assertThat(values).isEmpty() + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + step(0.3f), + step(0.5f), + step(0.7f), + step(1f), + step(1f, TransitionState.FINISHED), + ), + testScope, + ) + // no translation-x animation + values.forEach { assertThat(it.value).isEqualTo(0f) } + } + + @Test fun lockscreenTranslationX_resetsAfterCancellation() = kosmos.runTest { val config: Configuration = mock() @@ -118,6 +300,9 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() { R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x, 100, ) + + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + val values by collectValues(underTest.keyguardTranslationX) assertThat(values).isEmpty() @@ -137,6 +322,42 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) + fun lockscreenTranslationX_resetsAfterCancellation_fromHubInLandscape() = + kosmos.runTest { + kosmos.setCommunalV2ConfigEnabled(true) + val config: Configuration = mock() + whenever(config.layoutDirection).thenReturn(LayoutDirection.LTR) + configurationRepository.onConfigurationChange(config) + + configurationRepository.setDimensionPixelSize( + R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x, + 100, + ) + whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false) + + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + communalSceneRepository.setCommunalContainerOrientation( + Configuration.ORIENTATION_LANDSCAPE + ) + + val values by collectValues(underTest.keyguardTranslationX) + assertThat(values).isEmpty() + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + step(0.3f), + step(0.6f), + step(0.9f, TransitionState.CANCELED), + ), + testScope, + ) + // no translation-x animation + values.forEach { assertThat(it.value).isEqualTo(0f) } + } + + @Test @DisableSceneContainer fun blurBecomesMinValueImmediately() = kosmos.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt index fe213a6ebbf0..71e09d982494 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt @@ -17,12 +17,19 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.content.res.Configuration +import android.content.res.mainResources +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import android.view.View import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.communalSceneRepository +import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository @@ -35,6 +42,9 @@ import com.android.systemui.keyguard.domain.interactor.pulseExpansionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.scene.data.repository.Idle import com.android.systemui.scene.data.repository.sceneContainerRepository @@ -48,6 +58,7 @@ import com.android.systemui.statusbar.notification.data.repository.activeNotific import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationsKeyguardInteractor import com.android.systemui.statusbar.phone.dozeParameters import com.android.systemui.statusbar.phone.screenOffAnimationController +import com.android.systemui.statusbar.policy.keyguardStateController import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -69,7 +80,8 @@ import platform.test.runner.parameterized.Parameters @SmallTest @RunWith(ParameterizedAndroidJunit4::class) class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { - private val kosmos = testKosmos() + private val kosmos = + testKosmos().apply { mainResources = mContext.orCreateTestableResources.resources } private val testScope = kosmos.testScope private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository } private val keyguardRepository by lazy { kosmos.fakeKeyguardRepository } @@ -419,6 +431,7 @@ class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() } @Test + @DisableFlags(FLAG_GLANCEABLE_HUB_V2) fun alpha_transitionFromHubToLockscreen_isOne() = testScope.runTest { val alpha by collectLastValue(underTest.alpha(viewState)) @@ -439,6 +452,84 @@ class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() } @Test + @DisableSceneContainer + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) + fun alpha_transitionFromHubToLockscreenInLandscape_isOne() = + kosmos.runTest { + setCommunalV2ConfigEnabled(true) + whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false) + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + communalSceneRepository.setCommunalContainerOrientation( + Configuration.ORIENTATION_LANDSCAPE + ) + + val alpha by collectLastValue(underTest.alpha(viewState)) + + // Transition to the glanceable hub and back. + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GLANCEABLE_HUB, + testScope, + ) + + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + runCurrent() + + // Exit hub to lockscreen + val progress = MutableStateFlow(0f) + val transitionState = + MutableStateFlow( + ObservableTransitionState.Transition( + fromScene = CommunalScenes.Communal, + toScene = CommunalScenes.Blank, + currentScene = flowOf(CommunalScenes.Blank), + progress = progress, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + ) + communalSceneInteractor.setTransitionState(transitionState) + progress.value = .4f + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + TransitionStep( + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.LOCKSCREEN, + transitionState = TransitionState.STARTED, + value = 0f, + ), + TransitionStep( + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.LOCKSCREEN, + transitionState = TransitionState.RUNNING, + value = 0.4f, + ), + ), + testScope, + ) + + communalSceneRepository.setCommunalContainerOrientation( + Configuration.ORIENTATION_PORTRAIT + ) + runCurrent() + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + TransitionStep( + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.LOCKSCREEN, + transitionState = TransitionState.FINISHED, + value = 1f, + ) + ), + testScope, + ) + + assertThat(alpha).isEqualTo(1.0f) + } + + @Test fun alpha_emitsOnShadeExpansion() = testScope.runTest { val alpha by collectLastValue(underTest.alpha(viewState)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelTest.kt index adce9d65cbe0..e89c05f3a84d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelTest.kt @@ -16,6 +16,9 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.content.res.Configuration +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -44,7 +47,7 @@ class KeyguardSmartspaceViewModelTest : SysuiTestCase() { val kosmos = testKosmos() val testScope = kosmos.testScope val underTest = kosmos.keyguardSmartspaceViewModel - val res = context.resources + @Mock private lateinit var mockConfiguration: Configuration @Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var clockController: ClockController @@ -119,4 +122,63 @@ class KeyguardSmartspaceViewModelTest : SysuiTestCase() { assertThat(isShadeLayoutWide).isFalse() } } + + @Test + @DisableFlags(com.android.systemui.shared.Flags.FLAG_CLOCK_REACTIVE_SMARTSPACE_LAYOUT) + fun dateWeatherBelowSmallClock_smartspacelayoutflag_off_true() { + val result = KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(mockConfiguration) + + assertThat(result).isTrue() + } + + @Test + @EnableFlags(com.android.systemui.shared.Flags.FLAG_CLOCK_REACTIVE_SMARTSPACE_LAYOUT) + fun dateWeatherBelowSmallClock_defaultFontAndDisplaySize_false() { + val fontScale = 1.0f + val screenWidthDp = 347 + mockConfiguration.fontScale = fontScale + mockConfiguration.screenWidthDp = screenWidthDp + + val result = KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(mockConfiguration) + + assertThat(result).isFalse() + } + + @Test + @EnableFlags(com.android.systemui.shared.Flags.FLAG_CLOCK_REACTIVE_SMARTSPACE_LAYOUT) + fun dateWeatherBelowSmallClock_variousFontAndDisplaySize_false() { + mockConfiguration.fontScale = 1.0f + mockConfiguration.screenWidthDp = 347 + val result1 = KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(mockConfiguration) + assertThat(result1).isFalse() + + mockConfiguration.fontScale = 1.2f + mockConfiguration.screenWidthDp = 347 + val result2 = KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(mockConfiguration) + assertThat(result2).isFalse() + + mockConfiguration.fontScale = 1.7f + mockConfiguration.screenWidthDp = 412 + val result3 = KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(mockConfiguration) + assertThat(result3).isFalse() + } + + @Test + @EnableFlags(com.android.systemui.shared.Flags.FLAG_CLOCK_REACTIVE_SMARTSPACE_LAYOUT) + fun dateWeatherBelowSmallClock_variousFontAndDisplaySize_true() { + mockConfiguration.fontScale = 1.0f + mockConfiguration.screenWidthDp = 310 + val result1 = KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(mockConfiguration) + assertThat(result1).isTrue() + + mockConfiguration.fontScale = 1.5f + mockConfiguration.screenWidthDp = 347 + val result2 = KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(mockConfiguration) + assertThat(result2).isTrue() + + mockConfiguration.fontScale = 2.0f + mockConfiguration.screenWidthDp = 411 + val result3 = KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(mockConfiguration) + assertThat(result3).isTrue() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModelTest.kt new file mode 100644 index 000000000000..9c2c3c3f1498 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModelTest.kt @@ -0,0 +1,74 @@ +/* + * 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.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectValues +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.keyguard.ui.transitions.blurConfig +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class PrimaryBouncerToDreamingTransitionViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository + private lateinit var underTest: PrimaryBouncerToDreamingTransitionViewModel + + @Before + fun setUp() { + keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + underTest = kosmos.primaryBouncerToDreamingTransitionViewModel + } + + @Test + fun blurRadiusGoesToMinImmediately() = + testScope.runTest { + val values by collectValues(underTest.windowBlurRadius) + + kosmos.keyguardWindowBlurTestUtil.assertTransitionToBlurRadius( + transitionProgress = listOf(0.0f, 0.2f, 0.3f, 0.65f, 0.7f, 1.0f), + startValue = kosmos.blurConfig.maxBlurRadiusPx, + endValue = kosmos.blurConfig.minBlurRadiusPx, + actualValuesProvider = { values }, + transitionFactory = ::step, + ) + } + + private fun step(value: Float, state: TransitionState = RUNNING): TransitionStep { + return TransitionStep( + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.DREAMING, + value = value, + transitionState = state, + ownerName = "PrimaryBouncerToDreamingTransitionViewModelTest", + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java deleted file mode 100644 index b177e07d09b6..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright (C) 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.systemui.lowlightclock; - -import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT; -import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR; -import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_ON; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.any; -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.content.ComponentName; -import android.content.pm.PackageManager; -import android.testing.TestableLooper; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.dream.lowlight.LowLightDreamManager; -import com.android.systemui.SysuiTestCase; -import com.android.systemui.keyguard.ScreenLifecycle; -import com.android.systemui.shared.condition.Condition; -import com.android.systemui.shared.condition.Monitor; -import com.android.systemui.util.concurrency.FakeExecutor; -import com.android.systemui.util.time.FakeSystemClock; - -import dagger.Lazy; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.Set; - -@SmallTest -@RunWith(AndroidJUnit4.class) -@TestableLooper.RunWithLooper() -public class LowLightMonitorTest extends SysuiTestCase { - - @Mock - private Lazy<LowLightDreamManager> mLowLightDreamManagerLazy; - @Mock - private LowLightDreamManager mLowLightDreamManager; - @Mock - private Monitor mMonitor; - @Mock - private ScreenLifecycle mScreenLifecycle; - @Mock - private LowLightLogger mLogger; - - private LowLightMonitor mLowLightMonitor; - - @Mock - Lazy<Set<Condition>> mLazyConditions; - - @Mock - private PackageManager mPackageManager; - - @Mock - private ComponentName mDreamComponent; - - FakeExecutor mBackgroundExecutor = new FakeExecutor(new FakeSystemClock()); - - Condition mCondition = mock(Condition.class); - Set<Condition> mConditionSet = Set.of(mCondition); - - @Captor - ArgumentCaptor<Monitor.Subscription> mPreconditionsSubscriptionCaptor; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - when(mLowLightDreamManagerLazy.get()).thenReturn(mLowLightDreamManager); - when(mLazyConditions.get()).thenReturn(mConditionSet); - mLowLightMonitor = new LowLightMonitor(mLowLightDreamManagerLazy, - mMonitor, mLazyConditions, mScreenLifecycle, mLogger, mDreamComponent, - mPackageManager, mBackgroundExecutor); - } - - @Test - public void testSetAmbientLowLightWhenInLowLight() { - mLowLightMonitor.onConditionsChanged(true); - mBackgroundExecutor.runAllReady(); - // Verify setting low light when condition is true - verify(mLowLightDreamManager).setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); - } - - @Test - public void testExitAmbientLowLightWhenNotInLowLight() { - mLowLightMonitor.onConditionsChanged(true); - mLowLightMonitor.onConditionsChanged(false); - mBackgroundExecutor.runAllReady(); - // Verify ambient light toggles back to light mode regular - verify(mLowLightDreamManager).setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR); - } - - @Test - public void testStartMonitorLowLightConditionsWhenScreenTurnsOn() { - mLowLightMonitor.onScreenTurnedOn(); - mBackgroundExecutor.runAllReady(); - - // Verify subscribing to low light conditions monitor when screen turns on. - verify(mMonitor).addSubscription(any()); - } - - @Test - public void testStopMonitorLowLightConditionsWhenScreenTurnsOff() { - final Monitor.Subscription.Token token = mock(Monitor.Subscription.Token.class); - when(mMonitor.addSubscription(any())).thenReturn(token); - mLowLightMonitor.onScreenTurnedOn(); - - // Verify removing subscription when screen turns off. - mLowLightMonitor.onScreenTurnedOff(); - mBackgroundExecutor.runAllReady(); - verify(mMonitor).removeSubscription(token); - } - - @Test - public void testSubscribeToLowLightConditionsOnlyOnceWhenScreenTurnsOn() { - final Monitor.Subscription.Token token = mock(Monitor.Subscription.Token.class); - when(mMonitor.addSubscription(any())).thenReturn(token); - - mLowLightMonitor.onScreenTurnedOn(); - mLowLightMonitor.onScreenTurnedOn(); - mBackgroundExecutor.runAllReady(); - // Verify subscription is only added once. - verify(mMonitor, times(1)).addSubscription(any()); - } - - @Test - public void testSubscribedToExpectedConditions() { - final Monitor.Subscription.Token token = mock(Monitor.Subscription.Token.class); - when(mMonitor.addSubscription(any())).thenReturn(token); - - mLowLightMonitor.onScreenTurnedOn(); - mLowLightMonitor.onScreenTurnedOn(); - mBackgroundExecutor.runAllReady(); - Set<Condition> conditions = captureConditions(); - // Verify Monitor is subscribed to the expected conditions - assertThat(conditions).isEqualTo(mConditionSet); - } - - @Test - public void testNotUnsubscribeIfNotSubscribedWhenScreenTurnsOff() { - mLowLightMonitor.onScreenTurnedOff(); - mBackgroundExecutor.runAllReady(); - // Verify doesn't remove subscription since there is none. - verify(mMonitor, never()).removeSubscription(any()); - } - - @Test - public void testSubscribeIfScreenIsOnWhenStarting() { - when(mScreenLifecycle.getScreenState()).thenReturn(SCREEN_ON); - mLowLightMonitor.start(); - mBackgroundExecutor.runAllReady(); - // Verify to add subscription on start if the screen state is on - verify(mMonitor, times(1)).addSubscription(any()); - } - - @Test - public void testNoSubscribeIfDreamNotPresent() { - LowLightMonitor lowLightMonitor = new LowLightMonitor(mLowLightDreamManagerLazy, - mMonitor, mLazyConditions, mScreenLifecycle, mLogger, null, mPackageManager, - mBackgroundExecutor); - when(mScreenLifecycle.getScreenState()).thenReturn(SCREEN_ON); - lowLightMonitor.start(); - mBackgroundExecutor.runAllReady(); - verify(mScreenLifecycle, never()).addObserver(any()); - } - - private Set<Condition> captureConditions() { - verify(mMonitor).addSubscription(mPreconditionsSubscriptionCaptor.capture()); - return mPreconditionsSubscriptionCaptor.getValue().getConditions(); - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.kt new file mode 100644 index 000000000000..11f0f4394a85 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.kt @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.lowlightclock + +import android.content.ComponentName +import android.content.pm.PackageManager +import android.testing.TestableLooper.RunWithLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.dream.lowlight.LowLightDreamManager +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.domain.interactor.displayStateInteractor +import com.android.systemui.display.data.repository.displayRepository +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.shared.condition.Condition +import com.android.systemui.shared.condition.Monitor +import com.android.systemui.testKosmos +import com.google.common.truth.Truth +import dagger.Lazy +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +@RunWithLooper +class LowLightMonitorTest : SysuiTestCase() { + val kosmos = testKosmos().useUnconfinedTestDispatcher() + + @Mock private lateinit var lowLightDreamManagerLazy: Lazy<LowLightDreamManager> + + @Mock private lateinit var lowLightDreamManager: LowLightDreamManager + + private val monitor: Monitor = prepareMonitor() + + @Mock private lateinit var logger: LowLightLogger + + private lateinit var lowLightMonitor: LowLightMonitor + + @Mock private lateinit var lazyConditions: Lazy<Set<Condition>> + + @Mock private lateinit var packageManager: PackageManager + + @Mock private lateinit var dreamComponent: ComponentName + + private val condition = mock<Condition>() + + private val conditionSet = setOf(condition) + + @Captor + private lateinit var preconditionsSubscriptionCaptor: ArgumentCaptor<Monitor.Subscription> + + private fun prepareMonitor(): Monitor { + val monitor = mock<Monitor>() + whenever(monitor.addSubscription(ArgumentMatchers.any())).thenReturn(mock()) + + return monitor + } + + private fun setDisplayOn(screenOn: Boolean) { + kosmos.displayRepository.setDefaultDisplayOff(!screenOn) + } + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(lowLightDreamManagerLazy.get()).thenReturn(lowLightDreamManager) + whenever(lazyConditions.get()).thenReturn(conditionSet) + lowLightMonitor = + LowLightMonitor( + lowLightDreamManagerLazy, + monitor, + lazyConditions, + kosmos.displayStateInteractor, + logger, + dreamComponent, + packageManager, + kosmos.testScope.backgroundScope, + ) + whenever(monitor.addSubscription(ArgumentMatchers.any())).thenReturn(mock()) + val subscriptionCaptor = argumentCaptor<Monitor.Subscription>() + + setDisplayOn(false) + + lowLightMonitor.start() + verify(monitor).addSubscription(subscriptionCaptor.capture()) + clearInvocations(monitor) + + subscriptionCaptor.firstValue.callback.onConditionsChanged(true) + } + + private fun getConditionCallback(monitor: Monitor): Monitor.Callback { + val subscriptionCaptor = argumentCaptor<Monitor.Subscription>() + verify(monitor).addSubscription(subscriptionCaptor.capture()) + return subscriptionCaptor.firstValue.callback + } + + @Test + fun testSetAmbientLowLightWhenInLowLight() = + kosmos.runTest { + // Turn on screen + setDisplayOn(true) + + // Set conditions to true + val callback = getConditionCallback(monitor) + callback.onConditionsChanged(true) + + // Verify setting low light when condition is true + Mockito.verify(lowLightDreamManager) + .setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) + } + + @Test + fun testExitAmbientLowLightWhenNotInLowLight() = + kosmos.runTest { + // Turn on screen + setDisplayOn(true) + + // Set conditions to true then false + val callback = getConditionCallback(monitor) + callback.onConditionsChanged(true) + clearInvocations(lowLightDreamManager) + callback.onConditionsChanged(false) + + // Verify ambient light toggles back to light mode regular + Mockito.verify(lowLightDreamManager) + .setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR) + } + + @Test + fun testStopMonitorLowLightConditionsWhenScreenTurnsOff() = + kosmos.runTest { + val token = mock<Monitor.Subscription.Token>() + whenever(monitor.addSubscription(ArgumentMatchers.any())).thenReturn(token) + + setDisplayOn(true) + + // Verify removing subscription when screen turns off. + setDisplayOn(false) + Mockito.verify(monitor).removeSubscription(token) + } + + @Test + fun testSubscribeToLowLightConditionsOnlyOnceWhenScreenTurnsOn() = + kosmos.runTest { + val token = mock<Monitor.Subscription.Token>() + whenever(monitor.addSubscription(ArgumentMatchers.any())).thenReturn(token) + + setDisplayOn(true) + setDisplayOn(true) + // Verify subscription is only added once. + Mockito.verify(monitor, Mockito.times(1)).addSubscription(ArgumentMatchers.any()) + } + + @Test + fun testSubscribedToExpectedConditions() = + kosmos.runTest { + val token = mock<Monitor.Subscription.Token>() + whenever(monitor.addSubscription(ArgumentMatchers.any())).thenReturn(token) + + setDisplayOn(true) + + val conditions = captureConditions() + // Verify Monitor is subscribed to the expected conditions + Truth.assertThat(conditions).isEqualTo(conditionSet) + } + + @Test + fun testNotUnsubscribeIfNotSubscribedWhenScreenTurnsOff() = + kosmos.runTest { + setDisplayOn(true) + clearInvocations(monitor) + setDisplayOn(false) + runCurrent() + // Verify doesn't remove subscription since there is none. + Mockito.verify(monitor).removeSubscription(ArgumentMatchers.any()) + } + + @Test + fun testSubscribeIfScreenIsOnWhenStarting() = + kosmos.runTest { + val monitor = prepareMonitor() + + setDisplayOn(true) + + val targetMonitor = + LowLightMonitor( + lowLightDreamManagerLazy, + monitor, + lazyConditions, + displayStateInteractor, + logger, + dreamComponent, + packageManager, + testScope.backgroundScope, + ) + + // start + targetMonitor.start() + + val callback = getConditionCallback(monitor) + clearInvocations(monitor) + callback.onConditionsChanged(true) + + // Verify to add subscription on start and when the screen state is on + Mockito.verify(monitor).addSubscription(ArgumentMatchers.any()) + } + + @Test + fun testNoSubscribeIfDreamNotPresent() = + kosmos.runTest { + val monitor = prepareMonitor() + + setDisplayOn(true) + + val lowLightMonitor = + LowLightMonitor( + lowLightDreamManagerLazy, + monitor, + lazyConditions, + displayStateInteractor, + logger, + null, + packageManager, + testScope, + ) + + // start + lowLightMonitor.start() + + val callback = getConditionCallback(monitor) + clearInvocations(monitor) + callback.onConditionsChanged(true) + + // Verify to add subscription on start and when the screen state is on + Mockito.verify(monitor, never()).addSubscription(ArgumentMatchers.any()) + } + + private fun captureConditions(): Set<Condition?> { + Mockito.verify(monitor).addSubscription(preconditionsSubscriptionCaptor.capture()) + return preconditionsSubscriptionCaptor.value.conditions + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt index 1f1a74b6c389..63c0f4371c62 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt @@ -34,7 +34,6 @@ import androidx.media.utils.MediaConstants import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.Flags.MEDIA_RESUME_PROGRESS import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.graphics.ImageLoader import com.android.systemui.graphics.imageLoader @@ -167,8 +166,6 @@ class MediaDataLoaderTest : SysuiTestCase() { @Test fun loadMediaDataForResumption_returnsMediaData() = testScope.runTest { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - val song = "THIS_IS_A_SONG" val artist = "THIS_IS_AN_ARTIST" val albumArt = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java index 9c4d93c17d00..f7298dd0bf36 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java @@ -67,6 +67,7 @@ import java.util.concurrent.Executor; import java.util.stream.Collectors; @SmallTest +@DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_REDESIGN) @RunWith(AndroidJUnit4.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) public class MediaOutputAdapterLegacyTest extends SysuiTestCase { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.kt new file mode 100644 index 000000000000..70adfd324e94 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.kt @@ -0,0 +1,763 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.media.dialog + +import android.content.Context +import android.graphics.drawable.Icon +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.testing.TestableLooper.RunWithLooper +import android.view.View.GONE +import android.view.View.VISIBLE +import android.widget.LinearLayout +import androidx.appcompat.view.ContextThemeWrapper +import androidx.core.graphics.drawable.IconCompat +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.widget.RecyclerView +import com.android.media.flags.Flags +import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTED +import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTING +import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTING_FAILED +import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED +import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_GROUPING +import com.android.settingslib.media.MediaDevice +import com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_GO_TO_APP +import com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_NONE +import com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER +import com.android.systemui.SysuiTestCase +import com.android.systemui.media.dialog.MediaItem.MediaItemType +import com.android.systemui.media.dialog.MediaItem.createDeviceMediaItem +import com.android.systemui.media.dialog.MediaOutputAdapter.MediaDeviceViewHolder +import com.android.systemui.media.dialog.MediaOutputAdapter.MediaGroupDividerViewHolder +import com.android.systemui.res.R +import com.google.android.material.slider.Slider +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.kotlin.any +import org.mockito.kotlin.doNothing +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.spy +import org.mockito.kotlin.stub +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_REDESIGN) +@RunWith(AndroidJUnit4::class) +@RunWithLooper(setAsMainLooper = true) +class MediaOutputAdapterTest : SysuiTestCase() { + private val mMediaSwitchingController = mock<MediaSwitchingController>() + private val mMediaDevice1: MediaDevice = mock<MediaDevice>() + private val mMediaDevice2: MediaDevice = mock<MediaDevice>() + private val mIcon: Icon = mock<Icon>() + private val mIconCompat: IconCompat = mock<IconCompat>() + private lateinit var mMediaOutputAdapter: MediaOutputAdapter + private val mMediaItems: MutableList<MediaItem> = ArrayList() + + @Before + fun setUp() { + mMediaSwitchingController.stub { + on { getMediaItemList(false) } doReturn mMediaItems + on { hasAdjustVolumeUserRestriction() } doReturn false + on { isAnyDeviceTransferring } doReturn false + on { currentConnectedMediaDevice } doReturn mMediaDevice1 + on { connectedSpeakersExpandableGroupDivider } + .doReturn( + MediaItem.createExpandableGroupDividerMediaItem( + mContext.getString(R.string.media_output_group_title_connected_speakers) + ) + ) + on { sessionVolumeMax } doReturn TEST_MAX_VOLUME + on { sessionVolume } doReturn TEST_CURRENT_VOLUME + on { sessionName } doReturn TEST_SESSION_NAME + on { colorSchemeLegacy } doReturn mock<MediaOutputColorSchemeLegacy>() + on { colorScheme } doReturn mock<MediaOutputColorScheme>() + } + + mIconCompat.stub { on { toIcon(mContext) } doReturn mIcon } + + mMediaDevice1 + .stub { + on { id } doReturn TEST_DEVICE_ID_1 + on { name } doReturn TEST_DEVICE_NAME_1 + } + .also { + whenever(mMediaSwitchingController.getDeviceIconCompat(it)) doReturn mIconCompat + } + + mMediaDevice2 + .stub { + on { id } doReturn TEST_DEVICE_ID_2 + on { name } doReturn TEST_DEVICE_NAME_2 + } + .also { + whenever(mMediaSwitchingController.getDeviceIconCompat(it)) doReturn mIconCompat + } + + mMediaOutputAdapter = MediaOutputAdapter(mMediaSwitchingController) + } + + @Test + fun getItemCount_returnsMediaItemSize() { + updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2)) + + assertThat(mMediaOutputAdapter.itemCount).isEqualTo(mMediaItems.size) + } + + @Test + fun getItemId_forDifferentItemsTypes_returnCorrespondingHashCode() { + updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2)) + + assertThat(mMediaOutputAdapter.getItemId(0)) + .isEqualTo(mMediaItems[0].mediaDevice.get().id.hashCode()) + } + + @Test + fun getItemId_invalidPosition_returnPosition() { + updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2)) + val invalidPosition = mMediaItems.size + 1 + + assertThat(mMediaOutputAdapter.getItemId(invalidPosition)).isEqualTo(RecyclerView.NO_ID) + } + + @Test + fun onBindViewHolder_bindDisconnectedDevice_verifyView() { + mMediaDevice2.stub { on { state } doReturn STATE_DISCONNECTED } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleIcon.visibility).isEqualTo(VISIBLE) + assertThat(mTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2) + assertThat(mSlider.visibility).isEqualTo(GONE) + } + } + + @Test + fun onBindViewHolder_bindConnectedDevice_verifyView() { + mMediaDevice1.stub { on { state } doReturn STATE_CONNECTED } + updateAdapterWithDevices(listOf(mMediaDevice1)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleIcon.visibility).isEqualTo(GONE) + assertThat(mTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1) + assertThat(mSlider.visibility).isEqualTo(VISIBLE) + } + } + + @Test + fun onBindViewHolder_isMutingExpectedDevice_verifyView() { + mMediaDevice1.stub { + on { isMutingExpectedDevice } doReturn true + on { state } doReturn STATE_DISCONNECTED + } + mMediaSwitchingController.stub { on { isCurrentConnectedDeviceRemote } doReturn false } + updateAdapterWithDevices(listOf(mMediaDevice1)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1) + assertThat(mLoadingIndicator.visibility).isEqualTo(GONE) + assertThat(mSlider.visibility).isEqualTo(GONE) + assertThat(mGroupButton.visibility).isEqualTo(GONE) + assertThat(mTitleText.visibility).isEqualTo(VISIBLE) + } + } + + @Test + fun onBindViewHolder_bindConnectedDeviceWithMutingExpectedDeviceExist_verifyView() { + mMediaDevice1.stub { + on { isMutingExpectedDevice } doReturn true + on { state } doReturn STATE_CONNECTED + } + mMediaSwitchingController.stub { + on { hasMutingExpectedDevice() } doReturn true + on { isCurrentConnectedDeviceRemote } doReturn false + } + updateAdapterWithDevices(listOf(mMediaDevice1)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mLoadingIndicator.visibility).isEqualTo(GONE) + assertThat(mSlider.visibility).isEqualTo(GONE) + assertThat(mGroupButton.visibility).isEqualTo(GONE) + assertThat(mTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1) + } + } + + @Test + fun onBindViewHolder_initSeekbar_setsVolume() { + mMediaDevice1.stub { + on { state } doReturn STATE_CONNECTED + on { maxVolume } doReturn TEST_MAX_VOLUME + on { currentVolume } doReturn TEST_CURRENT_VOLUME + } + updateAdapterWithDevices(listOf(mMediaDevice1)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mSlider.visibility).isEqualTo(VISIBLE) + assertThat(mSlider.value).isEqualTo(TEST_CURRENT_VOLUME) + assertThat(mSlider.valueFrom).isEqualTo(0) + assertThat(mSlider.valueTo).isEqualTo(TEST_MAX_VOLUME) + } + } + + @Test + fun onBindViewHolder_dragSeekbar_adjustsVolume() { + mMediaDevice1.stub { + on { maxVolume } doReturn TEST_MAX_VOLUME + on { currentVolume } doReturn TEST_CURRENT_VOLUME + } + updateAdapterWithDevices(listOf(mMediaDevice1)) + + val viewHolder = + mMediaOutputAdapter.onCreateViewHolder( + LinearLayout(mContext), + MediaItemType.TYPE_DEVICE, + ) as MediaDeviceViewHolder + + var sliderChangeListener: Slider.OnChangeListener? = null + viewHolder.mSlider = + object : Slider(contextWithTheme(mContext)) { + override fun addOnChangeListener(listener: OnChangeListener) { + sliderChangeListener = listener + } + } + mMediaOutputAdapter.onBindViewHolder(viewHolder, 0) + sliderChangeListener?.onValueChange(viewHolder.mSlider, 5f, true) + + verify(mMediaSwitchingController).adjustVolume(mMediaDevice1, 5) + } + + @Test + fun onBindViewHolder_dragSeekbar_logsInteraction() { + mMediaDevice1 + .stub { + on { maxVolume } doReturn TEST_MAX_VOLUME + on { currentVolume } doReturn TEST_CURRENT_VOLUME + } + .also { mMediaItems.add(createDeviceMediaItem(it)) } + updateAdapterWithDevices(listOf(mMediaDevice1)) + + val viewHolder = + mMediaOutputAdapter.onCreateViewHolder( + LinearLayout(mContext), + MediaItemType.TYPE_DEVICE, + ) as MediaDeviceViewHolder + + var sliderTouchListener: Slider.OnSliderTouchListener? = null + viewHolder.mSlider = + object : Slider(contextWithTheme(mContext)) { + override fun addOnSliderTouchListener(listener: OnSliderTouchListener) { + sliderTouchListener = listener + } + } + mMediaOutputAdapter.onBindViewHolder(viewHolder, 0) + sliderTouchListener?.onStopTrackingTouch(viewHolder.mSlider) + + verify(mMediaSwitchingController).logInteractionAdjustVolume(mMediaDevice1) + } + + @Test + fun onBindViewHolder_bindSelectableDevice_verifyView() { + mMediaSwitchingController.stub { + on { selectableMediaDevice } doReturn listOf(mMediaDevice2) + } + updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 1).apply { + assertThat(mLoadingIndicator.visibility).isEqualTo(GONE) + assertThat(mDivider.visibility).isEqualTo(VISIBLE) + assertThat(mGroupButton.visibility).isEqualTo(VISIBLE) + assertThat(mGroupButton.contentDescription) + .isEqualTo(mContext.getString(R.string.accessibility_add_device_to_group)) + assertThat(mTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2) + + mGroupButton.performClick() + } + verify(mMediaSwitchingController).addDeviceToPlayMedia(mMediaDevice2) + } + + @Test + fun onBindViewHolder_bindDeselectableDevice_verifyView() { + mMediaSwitchingController.stub { + on { selectedMediaDevice } doReturn listOf(mMediaDevice1, mMediaDevice2) + on { deselectableMediaDevice } doReturn listOf(mMediaDevice1, mMediaDevice2) + } + updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 1).apply { + assertThat(mGroupButton.visibility).isEqualTo(VISIBLE) + assertThat(mGroupButton.contentDescription) + .isEqualTo(mContext.getString(R.string.accessibility_remove_device_from_group)) + mGroupButton.performClick() + } + + verify(mMediaSwitchingController).removeDeviceFromPlayMedia(mMediaDevice2) + } + + @Test + fun onBindViewHolder_bindNonDeselectableDevice_verifyView() { + mMediaSwitchingController.stub { + on { selectedMediaDevice } doReturn listOf(mMediaDevice1) + on { deselectableMediaDevice } doReturn ArrayList() + } + updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1) + assertThat(mGroupButton.visibility).isEqualTo(GONE) + } + } + + @Test + fun onBindViewHolder_bindFailedStateDevice_verifyView() { + mMediaDevice2.stub { on { state } doReturn STATE_CONNECTING_FAILED } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mStatusIcon.visibility).isEqualTo(VISIBLE) + assertThat(mSubTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mSubTitleText.text.toString()) + .isEqualTo(mContext.getText(R.string.media_output_dialog_connect_failed).toString()) + } + } + + @Test + fun onBindViewHolder_deviceHasSubtext_displaySubtitle() { + mMediaDevice2.stub { + on { state } doReturn STATE_DISCONNECTED + on { hasSubtext() } doReturn true + on { subtextString } doReturn TEST_CUSTOM_SUBTEXT + } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2) + assertThat(mSubTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mSubTitleText.text.toString()).isEqualTo(TEST_CUSTOM_SUBTEXT) + } + } + + @Test + fun onBindViewHolder_deviceWithOngoingSession_displaysGoToAppButton() { + mMediaDevice2.stub { + on { state } doReturn STATE_DISCONNECTED + on { hasOngoingSession() } doReturn true + } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + val viewHolder = + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2) + assertThat(mOngoingSessionButton.visibility).isEqualTo(VISIBLE) + assertThat(mOngoingSessionButton.contentDescription) + .isEqualTo(mContext.getString(R.string.accessibility_open_application)) + mOngoingSessionButton.performClick() + } + + verify(mMediaSwitchingController) + .tryToLaunchInAppRoutingIntent(TEST_DEVICE_ID_2, viewHolder.mOngoingSessionButton) + } + + @Test + fun onItemClick_selectionBehaviorTransfer_connectsDevice() { + mMediaDevice2.stub { + on { state } doReturn STATE_DISCONNECTED + on { selectionBehavior } doReturn SELECTION_BEHAVIOR_TRANSFER + } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 0).apply { mMainContent.performClick() } + + verify(mMediaSwitchingController).connectDevice(mMediaDevice2) + } + + @Test + fun onItemClick_selectionBehaviorTransferAndSessionHost_showsEndSessionDialog() { + mMediaSwitchingController.stub { + on { isCurrentOutputDeviceHasSessionOngoing() } doReturn true + } + mMediaDevice2.stub { + on { state } doReturn STATE_DISCONNECTED + on { selectionBehavior } doReturn SELECTION_BEHAVIOR_TRANSFER + } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + val viewHolder = + mMediaOutputAdapter.onCreateViewHolder( + LinearLayout(mContext), + MediaItemType.TYPE_DEVICE, + ) as MediaDeviceViewHolder + val spyMediaDeviceViewHolder = spy(viewHolder) + doNothing().whenever(spyMediaDeviceViewHolder).showCustomEndSessionDialog(mMediaDevice2) + + mMediaOutputAdapter.onBindViewHolder(spyMediaDeviceViewHolder, 0) + spyMediaDeviceViewHolder.mMainContent.performClick() + + verify(mMediaSwitchingController, never()).connectDevice(ArgumentMatchers.any()) + verify(spyMediaDeviceViewHolder).showCustomEndSessionDialog(mMediaDevice2) + } + + @Test + fun onItemClick_selectionBehaviorGoToApp_sendsLaunchIntent() { + mMediaDevice2.stub { + on { state } doReturn STATE_DISCONNECTED + on { selectionBehavior } doReturn SELECTION_BEHAVIOR_GO_TO_APP + } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + val viewHolder = + createAndBindDeviceViewHolder(position = 0).apply { mMainContent.performClick() } + verify(mMediaSwitchingController) + .tryToLaunchInAppRoutingIntent(TEST_DEVICE_ID_2, viewHolder.mMainContent) + } + + @Test + fun onItemClick_selectionBehaviorNone_doesNothing() { + mMediaDevice2.stub { + on { state } doReturn STATE_DISCONNECTED + on { selectionBehavior } doReturn SELECTION_BEHAVIOR_NONE + } + updateAdapterWithDevices(listOf(mMediaDevice2)) + createAndBindDeviceViewHolder(position = 0).apply { mMainContent.performClick() } + + verify(mMediaSwitchingController, never()).tryToLaunchInAppRoutingIntent(any(), any()) + verify(mMediaSwitchingController, never()).connectDevice(any()) + } + + @DisableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT) + @Test + fun clickFullItemOfSelectableDevice_flagOff_verifyConnectDevice() { + mMediaSwitchingController.stub { + on { selectableMediaDevice } doReturn listOf(mMediaDevice2) + } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2) + mMainContent.performClick() + } + verify(mMediaSwitchingController).connectDevice(mMediaDevice2) + } + + @EnableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT) + @Test + fun clickFullItemOfSelectableDevice_flagOn_hasListingPreference_verifyConnectDevice() { + mMediaDevice2.stub { on { hasRouteListingPreferenceItem() } doReturn true } + mMediaSwitchingController.stub { + on { selectableMediaDevice } doReturn listOf(mMediaDevice2) + } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2) + mMainContent.performClick() + } + verify(mMediaSwitchingController).connectDevice(mMediaDevice2) + } + + @EnableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT) + @Test + fun clickFullItemOfSelectableDevice_flagOn_isTransferable_verifyConnectDevice() { + mMediaSwitchingController.stub { + on { selectableMediaDevice } doReturn listOf(mMediaDevice2) + on { transferableMediaDevices } doReturn listOf(mMediaDevice2) + } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2) + mMainContent.performClick() + } + verify(mMediaSwitchingController).connectDevice(mMediaDevice2) + } + + @EnableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT) + @Test + fun clickFullItemOfSelectableDevice_flagOn_notTransferable_verifyNotConnectDevice() { + mMediaDevice2.stub { on { hasRouteListingPreferenceItem() } doReturn false } + mMediaSwitchingController.stub { + on { selectableMediaDevice } doReturn listOf(mMediaDevice2) + on { transferableMediaDevices } doReturn listOf() + } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2) + mMainContent.performClick() + } + verify(mMediaSwitchingController, never()).connectDevice(any()) + } + + @Test + fun onBindViewHolder_inTransferring_bindTransferringDevice_verifyView() { + mMediaSwitchingController.stub { on { isAnyDeviceTransferring() } doReturn true } + mMediaDevice2.stub { on { state } doReturn STATE_CONNECTING } + updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2)) + + // Connected device, looks like disconnected during transfer + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1) + assertThat(mSlider.visibility).isEqualTo(GONE) + assertThat(mLoadingIndicator.visibility).isEqualTo(GONE) + } + + // Connecting device + createAndBindDeviceViewHolder(position = 1).apply { + assertThat(mTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2) + assertThat(mSlider.visibility).isEqualTo(GONE) + assertThat(mLoadingIndicator.visibility).isEqualTo(VISIBLE) + } + } + + @Test + fun onBindViewHolder_bindGroupingDevice_verifyView() { + mMediaDevice1.stub { on { state } doReturn STATE_GROUPING } + updateAdapterWithDevices(listOf(mMediaDevice1)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1) + assertThat(mSlider.visibility).isEqualTo(GONE) + assertThat(mSubTitleText.visibility).isEqualTo(GONE) + assertThat(mGroupButton.visibility).isEqualTo(GONE) + assertThat(mLoadingIndicator.visibility).isEqualTo(VISIBLE) + } + } + + @Test + fun onItemClick_clicksWithMutingExpectedDeviceExist_cancelsMuteAwaitConnection() { + mMediaSwitchingController.stub { + on { hasMutingExpectedDevice() } doReturn true + on { isCurrentConnectedDeviceRemote() } doReturn false + } + mMediaDevice1.stub { on { isMutingExpectedDevice } doReturn false } + updateAdapterWithDevices(listOf(mMediaDevice1)) + + createAndBindDeviceViewHolder(position = 0).apply { mMainContent.performClick() } + verify(mMediaSwitchingController).cancelMuteAwaitConnection() + } + + @Test + fun onGroupActionTriggered_clicksSelectableDevice_triggerGrouping() { + mMediaSwitchingController.stub { + on { selectableMediaDevice } doReturn listOf(mMediaDevice2) + } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 0).apply { mGroupButton.performClick() } + verify(mMediaSwitchingController).addDeviceToPlayMedia(mMediaDevice2) + } + + @Test + fun onGroupActionTriggered_clickSelectedRemoteDevice_triggerUngrouping() { + mMediaSwitchingController.stub { + on { selectableMediaDevice } doReturn listOf(mMediaDevice2) + on { selectedMediaDevice } doReturn listOf(mMediaDevice1) + on { deselectableMediaDevice } doReturn listOf(mMediaDevice1) + on { isCurrentConnectedDeviceRemote } doReturn true + } + updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 0).apply { mGroupButton.performClick() } + verify(mMediaSwitchingController).removeDeviceFromPlayMedia(mMediaDevice1) + } + + @Test + fun onBindViewHolder_hasVolumeAdjustmentRestriction_verifySeekbarDisabled() { + mMediaSwitchingController.stub { + on { isCurrentConnectedDeviceRemote } doReturn true + on { hasAdjustVolumeUserRestriction() } doReturn true + } + mMediaDevice1.stub { on { state } doReturn STATE_CONNECTED } + updateAdapterWithDevices(listOf(mMediaDevice1)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mSlider.visibility).isEqualTo(GONE) + } + } + + @Test + fun onBindViewHolder_volumeControlChangeToEnabled_enableSeekbarAgain() { + mMediaSwitchingController.stub { + on { isVolumeControlEnabled(mMediaDevice1) } doReturn false + } + mMediaDevice1.stub { + on { state } doReturn STATE_CONNECTED + on { currentVolume } doReturn TEST_CURRENT_VOLUME + on { maxVolume } doReturn TEST_MAX_VOLUME + } + updateAdapterWithDevices(listOf(mMediaDevice1)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mSlider.visibility).isEqualTo(VISIBLE) + assertThat(mSlider.isEnabled).isFalse() + } + + mMediaSwitchingController.stub { + on { isVolumeControlEnabled(mMediaDevice1) } doReturn true + } + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mSlider.visibility).isEqualTo(VISIBLE) + assertThat(mSlider.isEnabled).isTrue() + } + } + + @Test + fun updateItems_controllerItemsUpdated_notUpdatesInAdapterUntilUpdateItems() { + mMediaOutputAdapter.updateItems() + val updatedList: MutableList<MediaItem> = ArrayList() + updatedList.add(MediaItem.createDeviceGroupMediaItem()) + whenever(mMediaSwitchingController.getMediaItemList(false)).doReturn(updatedList) + assertThat(mMediaOutputAdapter.itemCount).isEqualTo(mMediaItems.size) + + mMediaOutputAdapter.updateItems() + assertThat(mMediaOutputAdapter.itemCount).isEqualTo(updatedList.size) + } + + @Test + fun multipleSelectedDevices_listCollapsed_verifyItemTypes() { + mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn true } + initializeSession() + + with(mMediaOutputAdapter) { + assertThat(itemCount).isEqualTo(2) + assertThat(getItemViewType(0)).isEqualTo(MediaItemType.TYPE_GROUP_DIVIDER) + assertThat(getItemViewType(1)).isEqualTo(MediaItemType.TYPE_DEVICE_GROUP) + } + } + + @Test + fun multipleSelectedDevices_listCollapsed_verifySessionControl() { + mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn true } + initializeSession() + + createAndBindDeviceViewHolder(position = 1).apply { + assertThat(mTitleText.text.toString()).isEqualTo(TEST_SESSION_NAME) + assertThat(mSlider.visibility).isEqualTo(VISIBLE) + assertThat(mTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mSlider.value).isEqualTo(TEST_CURRENT_VOLUME) + } + + val viewHolder = + mMediaOutputAdapter.onCreateViewHolder( + LinearLayout(mContext), + MediaItemType.TYPE_DEVICE_GROUP, + ) as MediaDeviceViewHolder + + var sliderChangeListener: Slider.OnChangeListener? = null + viewHolder.mSlider = + object : Slider(contextWithTheme(mContext)) { + override fun addOnChangeListener(listener: OnChangeListener) { + sliderChangeListener = listener + } + } + mMediaOutputAdapter.onBindViewHolder(viewHolder, 1) + sliderChangeListener?.onValueChange(viewHolder.mSlider, 7f, true) + + verify(mMediaSwitchingController).adjustSessionVolume(7) + } + + @Test + fun multipleSelectedDevices_expandIconClicked_verifyIndividualDevices() { + mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn true } + initializeSession() + + val groupDividerViewHolder = + mMediaOutputAdapter.onCreateViewHolder( + LinearLayout(mContext), + MediaItemType.TYPE_GROUP_DIVIDER, + ) as MediaGroupDividerViewHolder + mMediaOutputAdapter.onBindViewHolder(groupDividerViewHolder, 0) + + mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn false } + groupDividerViewHolder.mExpandButton.performClick() + + createAndBindDeviceViewHolder(position = 1).apply { + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1) + assertThat(mSlider.visibility).isEqualTo(VISIBLE) + assertThat(mTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mGroupButton.visibility).isEqualTo(VISIBLE) + } + + createAndBindDeviceViewHolder(position = 2).apply { + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2) + assertThat(mSlider.visibility).isEqualTo(VISIBLE) + assertThat(mTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mGroupButton.visibility).isEqualTo(VISIBLE) + } + } + + private fun contextWithTheme(context: Context) = + ContextThemeWrapper( + context, + com.google.android.material.R.style.Theme_Material3_DynamicColors_DayNight, + ) + + private fun updateAdapterWithDevices(deviceList: List<MediaDevice>) { + for (device in deviceList) { + mMediaItems.add(createDeviceMediaItem(device)) + } + mMediaOutputAdapter.updateItems() + } + + private fun createAndBindDeviceViewHolder(position: Int): MediaDeviceViewHolder { + val viewHolder = + mMediaOutputAdapter.onCreateViewHolder( + LinearLayout(mContext), + mMediaOutputAdapter.getItemViewType(position), + ) + if (viewHolder is MediaDeviceViewHolder) { + mMediaOutputAdapter.onBindViewHolder(viewHolder, position) + return viewHolder + } else { + throw RuntimeException("ViewHolder for position $position is not MediaDeviceViewHolder") + } + } + + private fun initializeSession() { + val selectedDevices = listOf(mMediaDevice1, mMediaDevice2) + mMediaSwitchingController.stub { + on { selectableMediaDevice } doReturn selectedDevices + on { selectedMediaDevice } doReturn selectedDevices + on { deselectableMediaDevice } doReturn selectedDevices + } + mMediaOutputAdapter = MediaOutputAdapter(mMediaSwitchingController) + updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2)) + } + + companion object { + private const val TEST_DEVICE_NAME_1 = "test_device_name_1" + private const val TEST_DEVICE_NAME_2 = "test_device_name_2" + private const val TEST_DEVICE_ID_1 = "test_device_id_1" + private const val TEST_DEVICE_ID_2 = "test_device_id_2" + private const val TEST_SESSION_NAME = "test_session_name" + private const val TEST_CUSTOM_SUBTEXT = "custom subtext" + + private const val TEST_MAX_VOLUME = 20 + private const val TEST_CURRENT_VOLUME = 10 + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt index 04ef1be9c057..ab605c0ea14e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt @@ -18,12 +18,17 @@ package com.android.systemui.mediaprojection.permission import android.app.AlertDialog import android.media.projection.MediaProjectionConfig +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.TestableLooper import android.view.WindowManager import android.widget.Spinner import android.widget.TextView import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger import com.android.systemui.res.R @@ -32,6 +37,7 @@ import com.android.systemui.statusbar.phone.SystemUIDialog import com.google.common.truth.Truth.assertThat import kotlin.test.assertEquals import org.junit.After +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock @@ -41,6 +47,8 @@ import org.mockito.kotlin.mock @TestableLooper.RunWithLooper(setAsMainLooper = true) class ShareToAppPermissionDialogDelegateTest : SysuiTestCase() { + @get:Rule val checkFlagRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + private lateinit var dialog: AlertDialog private val appName = "Test App" @@ -51,6 +59,8 @@ class ShareToAppPermissionDialogDelegateTest : SysuiTestCase() { R.string.media_projection_entry_app_permission_dialog_option_text_entire_screen private val resIdSingleAppDisabled = R.string.media_projection_entry_app_permission_dialog_single_app_disabled + private val resIdSingleAppNotSupported = + R.string.media_projection_entry_app_permission_dialog_single_app_not_supported @After fun teardown() { @@ -78,6 +88,7 @@ class ShareToAppPermissionDialogDelegateTest : SysuiTestCase() { } @Test + @RequiresFlagsDisabled(Flags.FLAG_MEDIA_PROJECTION_GREY_ERROR_TEXT) fun showDialog_disableSingleApp() { setUpAndShowDialog( mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay() @@ -98,10 +109,34 @@ class ShareToAppPermissionDialogDelegateTest : SysuiTestCase() { } @Test + @RequiresFlagsEnabled(Flags.FLAG_MEDIA_PROJECTION_GREY_ERROR_TEXT) + fun showDialog_disableSingleApp_appNotSupported() { + setUpAndShowDialog( + mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay() + ) + + val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options) + val secondOptionWarningText = + spinner.adapter + .getDropDownView(1, null, spinner) + .findViewById<TextView>(android.R.id.text2) + ?.text + + // check that the first option is full screen and enabled + assertEquals(context.getString(resIdFullScreen), spinner.selectedItem) + + // check that the second option is single app and disabled + assertEquals( + context.getString(resIdSingleAppNotSupported, appName), + secondOptionWarningText, + ) + } + + @Test fun showDialog_disableSingleApp_forceShowPartialScreenShareTrue() { setUpAndShowDialog( mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay(), - overrideDisableSingleAppOption = true + overrideDisableSingleAppOption = true, ) val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options) @@ -161,7 +196,7 @@ class ShareToAppPermissionDialogDelegateTest : SysuiTestCase() { appName, overrideDisableSingleAppOption, hostUid = 12345, - mediaProjectionMetricsLogger = mock<MediaProjectionMetricsLogger>() + mediaProjectionMetricsLogger = mock<MediaProjectionMetricsLogger>(), ) dialog = AlertDialogWithDelegate(context, R.style.Theme_SystemUI_Dialog, delegate) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.kt index 6495b66cc148..17cdb8dd592d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.kt @@ -18,12 +18,17 @@ package com.android.systemui.mediaprojection.permission import android.app.AlertDialog import android.media.projection.MediaProjectionConfig +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.TestableLooper import android.view.WindowManager import android.widget.Spinner import android.widget.TextView import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger import com.android.systemui.res.R @@ -32,6 +37,7 @@ import com.android.systemui.statusbar.phone.SystemUIDialog import com.google.common.truth.Truth.assertThat import kotlin.test.assertEquals import org.junit.After +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock @@ -41,6 +47,8 @@ import org.mockito.kotlin.mock @TestableLooper.RunWithLooper(setAsMainLooper = true) class SystemCastPermissionDialogDelegateTest : SysuiTestCase() { + @get:Rule val checkFlagRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + private lateinit var dialog: AlertDialog private val appName = "Test App" @@ -51,6 +59,8 @@ class SystemCastPermissionDialogDelegateTest : SysuiTestCase() { R.string.media_projection_entry_cast_permission_dialog_option_text_entire_screen private val resIdSingleAppDisabled = R.string.media_projection_entry_app_permission_dialog_single_app_disabled + private val resIdSingleAppNotSupported = + R.string.media_projection_entry_app_permission_dialog_single_app_not_supported @After fun teardown() { @@ -78,6 +88,7 @@ class SystemCastPermissionDialogDelegateTest : SysuiTestCase() { } @Test + @RequiresFlagsDisabled(Flags.FLAG_MEDIA_PROJECTION_GREY_ERROR_TEXT) fun showDialog_disableSingleApp() { setUpAndShowDialog( mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay() @@ -98,6 +109,30 @@ class SystemCastPermissionDialogDelegateTest : SysuiTestCase() { } @Test + @RequiresFlagsEnabled(Flags.FLAG_MEDIA_PROJECTION_GREY_ERROR_TEXT) + fun showDialog_disableSingleApp_appNotSupported() { + setUpAndShowDialog( + mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay() + ) + + val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options) + val secondOptionWarningText = + spinner.adapter + .getDropDownView(1, null, spinner) + .findViewById<TextView>(android.R.id.text2) + ?.text + + // check that the first option is full screen and enabled + assertEquals(context.getString(resIdFullScreen), spinner.selectedItem) + + // check that the second option is single app and disabled + assertEquals( + context.getString(resIdSingleAppNotSupported, appName), + secondOptionWarningText, + ) + } + + @Test fun showDialog_disableSingleApp_forceShowPartialScreenShareTrue() { setUpAndShowDialog( mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay(), @@ -169,7 +204,7 @@ class SystemCastPermissionDialogDelegateTest : SysuiTestCase() { SystemUIDialog.setDialogSize(dialog) dialog.window?.addSystemFlags( - WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS, + WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS ) delegate.onCreate(dialog, savedInstanceState = null) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt index ffcd95bc7a4a..cd7b658518b6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt @@ -38,13 +38,10 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.domain.startable.sceneContainerStartable import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.enableDualShade import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayContentViewModel import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository -import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository -import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -116,38 +113,6 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() { } @Test - fun showClock_showsOnNarrowScreen() = - testScope.runTest { - kosmos.shadeRepository.setShadeLayoutWide(false) - - // Shown when notifications are present. - kosmos.activeNotificationListRepository.setActiveNotifs(1) - runCurrent() - assertThat(underTest.showClock).isTrue() - - // Hidden when notifications are not present. - kosmos.activeNotificationListRepository.setActiveNotifs(0) - runCurrent() - assertThat(underTest.showClock).isFalse() - } - - @Test - fun showClock_hidesOnWideScreen() = - testScope.runTest { - kosmos.shadeRepository.setShadeLayoutWide(true) - - // Hidden when notifications are present. - kosmos.activeNotificationListRepository.setActiveNotifs(1) - runCurrent() - assertThat(underTest.showClock).isFalse() - - // Hidden when notifications are not present. - kosmos.activeNotificationListRepository.setActiveNotifs(0) - runCurrent() - assertThat(underTest.showClock).isFalse() - } - - @Test fun showMedia_activeMedia_true() = testScope.runTest { kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = true)) 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 6d4fffdefb1b..00710dc037fa 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 @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.scene.domain.startable import android.app.StatusBarManager @@ -121,6 +123,7 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository import com.google.android.msdl.data.model.MSDLToken import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @@ -2608,6 +2611,75 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test + fun handleDeviceUnlockStatus_returnsToLsFromBouncer_whenGoesToSleep() = + testScope.runTest { + val authMethod by collectLastValue(kosmos.authenticationInteractor.authenticationMethod) + val isUnlocked by + collectLastValue( + kosmos.deviceUnlockedInteractor.deviceUnlockStatus.map { it.isUnlocked } + ) + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + val isAwake by collectLastValue(powerInteractor.isAwake) + prepareState( + isDeviceUnlocked = false, + initialSceneKey = Scenes.Lockscreen, + authenticationMethod = AuthenticationMethodModel.Pin, + startsAwake = true, + ) + underTest.start() + assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Pin) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + assertThat(currentOverlays).doesNotContain(Overlays.Bouncer) + assertThat(isAwake).isTrue() + + sceneInteractor.showOverlay(Overlays.Bouncer, "") + assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Pin) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + assertThat(currentOverlays).contains(Overlays.Bouncer) + assertThat(isAwake).isTrue() + + powerInteractor.setAsleepForTest() + assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Pin) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + assertThat(currentOverlays).doesNotContain(Overlays.Bouncer) + assertThat(isAwake).isFalse() + } + + @Test + fun hidesBouncer_whenAuthMethodChangesToNonSecure() = + testScope.runTest { + val authMethod by collectLastValue(kosmos.authenticationInteractor.authenticationMethod) + val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene) + val currentOverlays by collectLastValue(kosmos.sceneInteractor.currentOverlays) + prepareState( + authenticationMethod = AuthenticationMethodModel.Password, + initialSceneKey = Scenes.Lockscreen, + ) + underTest.start() + assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Password) + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + assertThat(currentOverlays).doesNotContain(Overlays.Bouncer) + + sceneInteractor.showOverlay(Overlays.Bouncer, "") + assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Password) + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + assertThat(currentOverlays).contains(Overlays.Bouncer) + + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) + runCurrent() + + assertThat(authMethod).isEqualTo(AuthenticationMethodModel.None) + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + assertThat(currentOverlays).doesNotContain(Overlays.Bouncer) + } + + @Test fun replacesLockscreenSceneOnBackStack_whenFaceUnlocked_fromShade_noAlternateBouncer() = testScope.runTest { val transitionState = @@ -2898,7 +2970,10 @@ class SceneContainerStartableTest : SysuiTestCase() { sceneInteractor.changeScene(it, "prepareState, initialSceneKey isn't null") } for (overlay in initialOverlays) { - sceneInteractor.showOverlay(overlay, "prepareState, initialOverlays isn't empty") + sceneInteractor.instantlyShowOverlay( + overlay, + "prepareState, initialOverlays isn't empty", + ) } if (startsAwake) { powerInteractor.setAwakeForTest() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/scroll/ScrollCaptureControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/scroll/ScrollCaptureControllerTest.java index a831e6344a66..fd796a56652b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/scroll/ScrollCaptureControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/scroll/ScrollCaptureControllerTest.java @@ -204,6 +204,21 @@ public class ScrollCaptureControllerTest extends SysuiTestCase { assertEquals("bottom", 200, screenshot.getBottom()); } + @Test + public void testCancellation() { + ScrollCaptureController controller = new TestScenario() + .withPageHeight(100) + .withMaxPages(2.5f) + .withTileHeight(10) + .withAvailableRange(-10, Integer.MAX_VALUE) + .createController(mContext); + + ScrollCaptureController.LongScreenshot screenshot = + getUnchecked(controller.run(EMPTY_RESPONSE)); + + assertEquals("top", -10, screenshot.getTop()); + assertEquals("bottom", 240, screenshot.getBottom()); + } /** * Build and configure a stubbed controller for each test case. */ diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 676e1ea5321a..579c242f974a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -41,16 +41,12 @@ import static org.mockito.Mockito.when; import android.animation.Animator; import android.annotation.IdRes; -import android.content.ContentResolver; import android.content.res.Configuration; import android.content.res.Resources; import android.os.Handler; import android.os.Looper; -import android.os.PowerManager; -import android.os.UserManager; import android.util.DisplayMetrics; import android.view.Display; -import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; @@ -65,10 +61,8 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.testing.UiEventLoggerFake; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.util.LatencyTracker; -import com.android.keyguard.KeyguardSliceViewController; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.dagger.KeyguardStatusBarViewComponent; -import com.android.keyguard.logging.KeyguardLogger; import com.android.systemui.SysuiTestCase; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.classifier.FalsingCollectorFake; @@ -142,7 +136,6 @@ import com.android.systemui.statusbar.notification.stack.AmbientState; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; -import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator; import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.ConfigurationControllerImpl; @@ -164,9 +157,7 @@ import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; -import com.android.systemui.statusbar.policy.SplitShadeStateController; import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository; -import com.android.systemui.statusbar.window.StatusBarWindowStateController; import com.android.systemui.unfold.SysUIUnfoldComponent; import com.android.systemui.user.domain.interactor.UserSwitcherInteractor; import com.android.systemui.util.kotlin.JavaAdapter; @@ -214,7 +205,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { @Mock protected DozeParameters mDozeParameters; @Mock protected ScreenOffAnimationController mScreenOffAnimationController; @Mock protected NotificationPanelView mView; - @Mock protected LayoutInflater mLayoutInflater; @Mock protected DynamicPrivacyController mDynamicPrivacyController; @Mock protected ShadeTouchableRegionManager mShadeTouchableRegionManager; @Mock protected KeyguardStateController mKeyguardStateController; @@ -223,7 +213,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { @Mock protected CommandQueue mCommandQueue; @Mock protected VibratorHelper mVibratorHelper; @Mock protected LatencyTracker mLatencyTracker; - @Mock protected PowerManager mPowerManager; @Mock protected AccessibilityManager mAccessibilityManager; @Mock protected MetricsLogger mMetricsLogger; @Mock protected Resources mResources; @@ -242,14 +231,12 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { @Mock protected ScrimController mScrimController; @Mock protected MediaDataManager mMediaDataManager; @Mock protected AmbientState mAmbientState; - @Mock protected UserManager mUserManager; @Mock protected UiEventLogger mUiEventLogger; @Mock protected KeyguardMediaController mKeyguardMediaController; @Mock protected NavigationModeController mNavigationModeController; @Mock protected NavigationBarController mNavigationBarController; @Mock protected QuickSettingsControllerImpl mQsController; @Mock protected ShadeHeaderController mShadeHeaderController; - @Mock protected ContentResolver mContentResolver; @Mock protected TapAgainViewController mTapAgainViewController; @Mock protected KeyguardIndicationController mKeyguardIndicationController; @Mock protected FragmentService mFragmentService; @@ -261,12 +248,10 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { @Mock protected DumpManager mDumpManager; @Mock protected NotificationsQSContainerController mNotificationsQSContainerController; @Mock protected QsFrameTranslateController mQsFrameTranslateController; - @Mock protected StatusBarWindowStateController mStatusBarWindowStateController; @Mock protected KeyguardUnlockAnimationController mKeyguardUnlockAnimationController; @Mock protected NotificationShadeWindowController mNotificationShadeWindowController; @Mock protected SysUiState mSysUiState; @Mock protected NotificationListContainer mNotificationListContainer; - @Mock protected NotificationStackSizeCalculator mNotificationStackSizeCalculator; @Mock protected UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController; @Mock protected QS mQs; @Mock protected QSFragmentLegacy mQSFragment; @@ -281,8 +266,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { @Mock protected AlternateBouncerInteractor mAlternateBouncerInteractor; @Mock protected MotionEvent mDownMotionEvent; @Mock protected CoroutineDispatcher mMainDispatcher; - @Mock protected KeyguardSliceViewController mKeyguardSliceViewController; - private final KeyguardLogger mKeyguardLogger = new KeyguardLogger(logcatLogBuffer()); @Captor protected ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener> mEmptySpaceClickListenerCaptor; @@ -363,9 +346,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mock(DeviceEntryUdfpsInteractor.class); when(deviceEntryUdfpsInteractor.isUdfpsSupported()).thenReturn(MutableStateFlow(false)); - final SplitShadeStateController splitShadeStateController = - new ResourcesSplitShadeStateController(); - mShadeInteractor = new ShadeInteractorImpl( mTestScope.getBackgroundScope(), mKosmos.getDeviceProvisioningInteractor(), @@ -380,8 +360,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mTestScope.getBackgroundScope(), mFakeKeyguardRepository, mShadeRepository - ), - mKosmos.getShadeModeInteractor()); + )); SystemClock systemClock = new FakeSystemClock(); mStatusBarStateController = new StatusBarStateControllerImpl( mUiEventLogger, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java index db0c07c50dc6..348eee8de313 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java @@ -199,8 +199,7 @@ public class QuickSettingsControllerImplBaseTest extends SysuiTestCase { mTestScope.getBackgroundScope(), mKeyguardRepository, mShadeRepository - ), - mKosmos.getShadeModeInteractor()); + )); when(mResources.getDimensionPixelSize( R.dimen.lockscreen_shade_qs_transition_distance)).thenReturn(DEFAULT_HEIGHT); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt index b376558371f3..0289c58f6e93 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt @@ -34,6 +34,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.disableDualShade import com.android.systemui.shade.domain.interactor.enableDualShade import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.CommandQueue @@ -214,6 +215,21 @@ class ShadeControllerSceneImplTest : SysuiTestCase() { assertThat(currentOverlays).isEmpty() } + @Test + fun instantCollapseShade_singleShade_doesntSwitchToShadeScene() = + testScope.runTest { + kosmos.disableDualShade() + runCurrent() + val currentScene by collectLastValue(sceneInteractor.currentScene) + val homeScene = currentScene + sceneInteractor.changeScene(Scenes.QuickSettings, "") + assertThat(currentScene).isEqualTo(Scenes.QuickSettings) + + underTest.instantCollapseShade() + + assertThat(currentScene).isEqualTo(homeScene) + } + private fun setScene(key: SceneKey) { sceneInteractor.changeScene(key, "test") sceneInteractor.setTransitionState( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/BlurUtilsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/BlurUtilsTest.kt index 402b53c12bda..7664e2ad3072 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/BlurUtilsTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/BlurUtilsTest.kt @@ -19,22 +19,23 @@ package com.android.systemui.statusbar import android.content.res.Resources import android.view.CrossWindowBlurListeners import android.view.SurfaceControl +import android.view.SyncRtSurfaceTransactionApplier import android.view.ViewRootImpl import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.ui.transitions.BlurConfig +import com.google.common.truth.Truth.assertThat +import junit.framework.TestCase.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor import org.mockito.Mock -import org.mockito.Mockito.any -import org.mockito.Mockito.anyInt import org.mockito.Mockito.clearInvocations -import org.mockito.Mockito.eq import org.mockito.Mockito.mock -import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @@ -45,10 +46,13 @@ class BlurUtilsTest : SysuiTestCase() { val blurConfig: BlurConfig = BlurConfig(minBlurRadiusPx = 1.0f, maxBlurRadiusPx = 100.0f) @Mock lateinit var dumpManager: DumpManager - @Mock lateinit var transaction: SurfaceControl.Transaction @Mock lateinit var crossWindowBlurListeners: CrossWindowBlurListeners @Mock lateinit var resources: Resources - lateinit var blurUtils: TestableBlurUtils + @Mock lateinit var syncRTTransactionApplier: SyncRtSurfaceTransactionApplier + @Mock lateinit var transaction: SurfaceControl.Transaction + @Captor + private lateinit var captor: ArgumentCaptor<SyncRtSurfaceTransactionApplier.SurfaceParams> + private lateinit var blurUtils: TestableBlurUtils @Before fun setup() { @@ -77,9 +81,10 @@ class BlurUtilsTest : SysuiTestCase() { `when`(viewRootImpl.surfaceControl).thenReturn(surfaceControl) `when`(surfaceControl.isValid).thenReturn(true) blurUtils.applyBlur(viewRootImpl, radius, true /* opaque */) - verify(transaction).setBackgroundBlurRadius(eq(surfaceControl), eq(radius)) - verify(transaction).setOpaque(eq(surfaceControl), eq(true)) - verify(transaction).apply() + + verify(syncRTTransactionApplier).scheduleApply(captor.capture()) + assertThat(captor.value.opaque).isTrue() + assertEquals(radius, captor.value.backgroundBlurRadius) } @Test @@ -92,9 +97,10 @@ class BlurUtilsTest : SysuiTestCase() { blurUtils.blursEnabled = false blurUtils.applyBlur(viewRootImpl, radius, true /* opaque */) - verify(transaction).setOpaque(eq(surfaceControl), eq(true)) - verify(transaction, never()).setBackgroundBlurRadius(any(), anyInt()) - verify(transaction).apply() + + verify(syncRTTransactionApplier).scheduleApply(captor.capture()) + assertThat(captor.value.opaque).isTrue() + assertEquals(0 /* unset value */, captor.value.backgroundBlurRadius) } @Test @@ -102,24 +108,32 @@ class BlurUtilsTest : SysuiTestCase() { val radius = 10 val surfaceControl = mock(SurfaceControl::class.java) val viewRootImpl = mock(ViewRootImpl::class.java) + val tmpFloatArray = FloatArray(0) `when`(viewRootImpl.surfaceControl).thenReturn(surfaceControl) `when`(surfaceControl.isValid).thenReturn(true) blurUtils.applyBlur(viewRootImpl, radius, true /* opaque */) + + verify(syncRTTransactionApplier).scheduleApply(captor.capture()) + assertThat(captor.value.opaque).isTrue() + SyncRtSurfaceTransactionApplier.applyParams(transaction, captor.value, tmpFloatArray) verify(transaction).setEarlyWakeupStart() + + clearInvocations(syncRTTransactionApplier) clearInvocations(transaction) blurUtils.applyBlur(viewRootImpl, 0, true /* opaque */) + verify(syncRTTransactionApplier).scheduleApply(captor.capture()) + SyncRtSurfaceTransactionApplier.applyParams(transaction, captor.value, tmpFloatArray) verify(transaction).setEarlyWakeupEnd() } - inner class TestableBlurUtils : BlurUtils(resources, blurConfig, crossWindowBlurListeners, dumpManager) { + inner class TestableBlurUtils : + BlurUtils(resources, blurConfig, crossWindowBlurListeners, dumpManager) { var blursEnabled = true + override val transactionApplier: SyncRtSurfaceTransactionApplier + get() = syncRTTransactionApplier override fun supportsBlursOnWindows(): Boolean { return blursEnabled } - - override fun createTransaction(): SurfaceControl.Transaction { - return transaction - } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt index 67af7a54988e..b4c6b33463b0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt @@ -48,6 +48,7 @@ import com.android.wm.shell.appzoomout.AppZoomOut import com.google.common.truth.Truth.assertThat import java.util.Optional import java.util.function.Consumer +import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Before import org.junit.Rule import org.junit.Test @@ -120,6 +121,8 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { `when`(blurUtils.supportsBlursOnWindows()).thenReturn(true) `when`(blurUtils.maxBlurRadius).thenReturn(maxBlur.toFloat()) `when`(blurUtils.maxBlurRadius).thenReturn(maxBlur.toFloat()) + `when`(windowRootViewBlurInteractor.isBlurCurrentlySupported) + .thenReturn(MutableStateFlow(true)) notificationShadeDepthController = NotificationShadeDepthController( @@ -353,7 +356,6 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { notificationShadeDepthController.addListener(listener) notificationShadeDepthController.updateBlurCallback.doFrame(0) verify(wallpaperController).setNotificationShadeZoom(anyFloat()) - verify(listener).onWallpaperZoomOutChanged(anyFloat()) verify(blurUtils).applyBlur(any(), anyInt(), eq(false)) } @@ -369,7 +371,6 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { notificationShadeDepthController.updateBlurCallback.doFrame(0) verify(wallpaperController).setNotificationShadeZoom(eq(0f)) - verify(listener).onWallpaperZoomOutChanged(eq(0f)) } @Test @@ -384,7 +385,6 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { notificationShadeDepthController.updateBlurCallback.doFrame(0) verify(wallpaperController).setNotificationShadeZoom(floatThat { it != 0f }) - verify(listener).onWallpaperZoomOutChanged(floatThat { it != 0f }) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt index 72d21f1064af..81f2bc94a307 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt @@ -16,10 +16,12 @@ package com.android.systemui.statusbar +import android.content.res.Resources import android.telephony.ServiceState import android.telephony.SubscriptionInfo import android.telephony.TelephonyManager import android.telephony.telephonyManager +import android.testing.TestableResources import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.keyguard.keyguardUpdateMonitor @@ -70,10 +72,18 @@ class OperatorNameViewControllerTest : SysuiTestCase() { private val airplaneModeRepository = FakeAirplaneModeRepository() private val connectivityRepository = FakeConnectivityRepository() + @Mock private lateinit var resources: Resources + private lateinit var testableResources: TestableResources + @Before fun setup() { MockitoAnnotations.initMocks(this) + testableResources = mContext.getOrCreateTestableResources() + testableResources.addOverride( + com.android.internal.R.integer.config_showOperatorNameDefault, + 1) + airplaneModeInteractor = AirplaneModeInteractor( airplaneModeRepository, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt index 5ec9b601bcca..cbef24a2f2d2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt @@ -24,6 +24,7 @@ import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import android.view.View import androidx.test.filters.SmallTest +import com.android.internal.logging.InstanceId import com.android.systemui.SysuiTestCase import com.android.systemui.activity.data.repository.activityManagerRepository import com.android.systemui.activity.data.repository.fake @@ -44,8 +45,6 @@ import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.core.StatusBarRootModernization -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.phone.ongoingcall.DisableChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization @@ -99,39 +98,57 @@ class CallChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test - fun chip_inCall_zeroStartTime_isShownAsIconOnly() = + fun chip_inCall_hasKeyWithPrefix() = kosmos.runTest { val latest by collectLastValue(underTest.chip) addOngoingCallState(startTimeMs = 0, isAppVisible = false) + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active::class.java) + assertThat((latest as OngoingActivityChipModel.Active).key) + .startsWith(CallChipViewModel.KEY_PREFIX) + } + + @Test + fun chip_inCall_zeroStartTime_isShownAsIconOnly_withData() = + kosmos.runTest { + val latest by collectLastValue(underTest.chip) + + val instanceId = InstanceId.fakeInstanceId(10) + addOngoingCallState(startTimeMs = 0, isAppVisible = false, instanceId = instanceId) + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse() assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isFalse() + assertThat((latest as OngoingActivityChipModel.Active).instanceId).isEqualTo(instanceId) } @Test - fun chip_inCall_negativeStartTime_isShownAsIconOnly() = + fun chip_inCall_negativeStartTime_isShownAsIconOnly_withData() = kosmos.runTest { val latest by collectLastValue(underTest.chip) - addOngoingCallState(startTimeMs = -2, isAppVisible = false) + val instanceId = InstanceId.fakeInstanceId(10) + addOngoingCallState(startTimeMs = -2, isAppVisible = false, instanceId = instanceId) assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse() assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isFalse() + assertThat((latest as OngoingActivityChipModel.Active).instanceId).isEqualTo(instanceId) } @Test - fun chip_inCall_positiveStartTime_isShownAsTimer() = + fun chip_inCall_positiveStartTime_isShownAsTimer_withData() = kosmos.runTest { val latest by collectLastValue(underTest.chip) - addOngoingCallState(startTimeMs = 345, isAppVisible = false) + val instanceId = InstanceId.fakeInstanceId(10) + addOngoingCallState(startTimeMs = 345, isAppVisible = false, instanceId = instanceId) assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse() assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isFalse() + assertThat((latest as OngoingActivityChipModel.Active).instanceId).isEqualTo(instanceId) } @Test @@ -799,17 +816,6 @@ class CallChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { mock<StatusBarIconView>() } - private val PROMOTED_CONTENT_WITH_COLOR = - PromotedNotificationContentBuilder("notif") - .applyToShared { - this.colors = - PromotedNotificationContentModel.Colors( - backgroundColor = PROMOTED_BACKGROUND_COLOR, - primaryTextColor = PROMOTED_PRIMARY_TEXT_COLOR, - ) - } - .build() - private const val NOTIFICATION_KEY = "testKey" private const val NOTIFICATION_UID = 12345 private const val PROMOTED_BACKGROUND_COLOR = 65 diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt index a60e7c19d8bd..e1b4496606e5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt @@ -136,6 +136,8 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue() + assertThat((latest as OngoingActivityChipModel.Active).instanceId).isNotNull() + val icon = (((latest as OngoingActivityChipModel.Active).icon) as OngoingActivityChipModel.ChipIcon.SingleColorIcon) @@ -159,6 +161,8 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue() + assertThat((latest as OngoingActivityChipModel.Active).instanceId).isNotNull() + val icon = (((latest as OngoingActivityChipModel.Active).icon) as OngoingActivityChipModel.ChipIcon.SingleColorIcon) @@ -180,6 +184,8 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue() + assertThat((latest as OngoingActivityChipModel.Active).instanceId).isNotNull() + val icon = (((latest as OngoingActivityChipModel.Active).icon) as OngoingActivityChipModel.ChipIcon.SingleColorIcon) @@ -219,6 +225,8 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue() + assertThat((latest as OngoingActivityChipModel.Active).instanceId).isNotNull() + val icon = (((latest as OngoingActivityChipModel.Active).icon) as OngoingActivityChipModel.ChipIcon.SingleColorIcon) @@ -250,6 +258,8 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { // Only the projection info will show a timer assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue() + assertThat((latest as OngoingActivityChipModel.Active).instanceId).isNotNull() + val icon = (((latest as OngoingActivityChipModel.Active).icon) as OngoingActivityChipModel.ChipIcon.SingleColorIcon) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt index 9ce43a0792c2..3bed69bac15d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt @@ -20,6 +20,7 @@ import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.internal.logging.InstanceId import com.android.systemui.SysuiTestCase import com.android.systemui.activity.data.repository.activityManagerRepository import com.android.systemui.activity.data.repository.fake @@ -49,12 +50,14 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { fun notificationChip_startsWithStartingModel() = kosmos.runTest { val icon = mock<StatusBarIconView>() + val instanceId = InstanceId.fakeInstanceId(4) val startingNotif = activeNotificationModel( key = "notif1", appName = "Fake Name", statusBarChipIcon = icon, promotedContent = PROMOTED_CONTENT, + instanceId = instanceId, ) val underTest = factory.create(startingNotif, creationTime = 1) @@ -65,12 +68,14 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { assertThat(latest!!.appName).isEqualTo("Fake Name") assertThat(latest!!.statusBarChipIconView).isEqualTo(icon) assertThat(latest!!.promotedContent).isEqualTo(PROMOTED_CONTENT) + assertThat(latest!!.instanceId).isEqualTo(instanceId) } @Test fun notificationChip_updatesAfterSet() = kosmos.runTest { val originalIconView = mock<StatusBarIconView>() + val originalInstanceId = InstanceId.fakeInstanceId(4) val underTest = factory.create( activeNotificationModel( @@ -78,6 +83,7 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { appName = "Fake Name", statusBarChipIcon = originalIconView, promotedContent = PROMOTED_CONTENT, + instanceId = originalInstanceId, ), creationTime = 1, ) @@ -85,18 +91,21 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { val latest by collectLastValue(underTest.notificationChip) val newIconView = mock<StatusBarIconView>() + val newInstanceId = InstanceId.fakeInstanceId(4) underTest.setNotification( activeNotificationModel( key = "notif1", appName = "New Name", statusBarChipIcon = newIconView, promotedContent = PROMOTED_CONTENT, + instanceId = newInstanceId, ) ) assertThat(latest!!.key).isEqualTo("notif1") assertThat(latest!!.appName).isEqualTo("New Name") assertThat(latest!!.statusBarChipIconView).isEqualTo(newIconView) + assertThat(latest!!.instanceId).isEqualTo(newInstanceId) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt index 660f0b52f464..1ee4b0f4d96f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt @@ -22,6 +22,7 @@ import android.platform.test.annotations.EnableFlags import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.internal.logging.InstanceId import com.android.systemui.Flags.FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY import com.android.systemui.SysuiTestCase import com.android.systemui.activity.data.repository.activityManagerRepository @@ -396,6 +397,33 @@ class NotifChipsViewModelTest : SysuiTestCase() { @Test @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_shortCriticalText_usesInstanceId() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + + val promotedContentBuilder = + PromotedNotificationContentBuilder("notif").applyToShared { + this.shortCriticalText = "Arrived" + } + val instanceId = InstanceId.fakeInstanceId(30) + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = promotedContentBuilder.build(), + instanceId = instanceId, + ) + ) + ) + + assertThat(latest).hasSize(1) + assertThat(latest!![0]).isInstanceOf(OngoingActivityChipModel.Active::class.java) + assertThat(latest!![0].instanceId).isEqualTo(instanceId) + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_noTime_isIconOnly() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -774,6 +802,41 @@ class NotifChipsViewModelTest : SysuiTestCase() { @Test @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_countDownTime_usesInstanceId() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + + val instanceId = InstanceId.fakeInstanceId(20) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) + + val currentElapsed = + currentTime + fakeSystemClock.elapsedRealtime() - + fakeSystemClock.currentTimeMillis() + val whenElapsed = currentElapsed + 10.minutes.inWholeMilliseconds + val promotedContentBuilder = + PromotedNotificationContentBuilder("notif").applyToShared { + this.time = + When.Chronometer(elapsedRealtimeMillis = whenElapsed, isCountDown = true) + } + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = promotedContentBuilder.build(), + instanceId = instanceId, + ) + ) + ) + + assertThat(latest).hasSize(1) + assertThat(latest!![0]).isInstanceOf(OngoingActivityChipModel.Active::class.java) + assertThat((latest!![0]).instanceId).isEqualTo(instanceId) + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_noHeadsUp_showsTime() = kosmos.runTest { val latest by collectLastValue(underTest.chips) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt index 91942213ce75..3e67e983a680 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt @@ -111,6 +111,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Countdown::class.java) assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue() + assertThat((latest as OngoingActivityChipModel.Active).instanceId).isNotNull() assertThat((latest as OngoingActivityChipModel.Active).icon).isNull() assertThat((latest as OngoingActivityChipModel.Active).onClickListenerLegacy).isNull() } @@ -158,6 +159,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue() + assertThat((latest as OngoingActivityChipModel.Active).instanceId).isNotNull() val icon = (((latest as OngoingActivityChipModel.Active).icon) as OngoingActivityChipModel.ChipIcon.SingleColorIcon) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt index 99e378c78ee2..ad556b7d4675 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt @@ -385,6 +385,8 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue() + assertThat((latest as OngoingActivityChipModel.Active).instanceId).isNotNull() + val icon = (((latest as OngoingActivityChipModel.Active).icon) as OngoingActivityChipModel.ChipIcon.SingleColorIcon) @@ -409,6 +411,8 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue() + assertThat((latest as OngoingActivityChipModel.Active).instanceId).isNotNull() + val icon = (((latest as OngoingActivityChipModel.Active).icon) as OngoingActivityChipModel.ChipIcon.SingleColorIcon) @@ -427,6 +431,8 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue() + assertThat((latest as OngoingActivityChipModel.Active).instanceId).isNotNull() + val icon = (((latest as OngoingActivityChipModel.Active).icon) as OngoingActivityChipModel.ChipIcon.SingleColorIcon) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt index 39b19d3c4191..0eddcd563663 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt @@ -20,6 +20,7 @@ import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.jank.Cuj +import com.android.internal.logging.InstanceId import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator @@ -29,9 +30,11 @@ import com.android.systemui.res.R import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickCallback import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener +import com.android.systemui.statusbar.chips.uievents.statusBarChipsUiEventLogger import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.ongoingcall.DisableChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization +import com.android.systemui.testKosmos import kotlin.test.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyBoolean @@ -44,6 +47,7 @@ import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) class OngoingActivityChipViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() private val mockSystemUIDialog = mock<SystemUIDialog>() private val dialogDelegate = SystemUIDialog.Delegate { mockSystemUIDialog } private val dialogTransitionAnimator = mock<DialogTransitionAnimator>() @@ -70,8 +74,10 @@ class OngoingActivityChipViewModelTest : SysuiTestCase() { dialogDelegate, dialogTransitionAnimator, cuj, - logcatLogBuffer("OngoingActivityChipViewModelTest"), - "tag", + instanceId = InstanceId.fakeInstanceId(0), + uiEventLogger = kosmos.statusBarChipsUiEventLogger, + logger = logcatLogBuffer("OngoingActivityChipViewModelTest"), + tag = "tag", ) clickListener.onClick(chipView) @@ -88,8 +94,10 @@ class OngoingActivityChipViewModelTest : SysuiTestCase() { dialogDelegate, dialogTransitionAnimator, cuj, - logcatLogBuffer("OngoingActivityChipViewModelTest"), - "tag", + instanceId = InstanceId.fakeInstanceId(0), + uiEventLogger = kosmos.statusBarChipsUiEventLogger, + logger = logcatLogBuffer("OngoingActivityChipViewModelTest"), + tag = "tag", ) clickCallback.invoke(mockExpandable) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt index e39fa7099953..4f0dfd373116 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt @@ -41,6 +41,7 @@ import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager import com.android.systemui.res.R import com.android.systemui.screenrecord.data.model.ScreenRecordModel import com.android.systemui.screenrecord.data.repository.screenRecordRepository +import com.android.systemui.statusbar.chips.call.ui.viewmodel.CallChipViewModel import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips @@ -402,7 +403,8 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { context: Context, ) { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active::class.java) - assertThat((latest as OngoingActivityChipModel.Active).key).isEqualTo(notificationKey) + assertThat((latest as OngoingActivityChipModel.Active).key) + .isEqualTo("${CallChipViewModel.KEY_PREFIX}$notificationKey") if (StatusBarConnectedDisplays.isEnabled) { assertNotificationIcon(latest, notificationKey) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt index 83b3c9cae3c1..826f94408715 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt @@ -42,6 +42,7 @@ import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager import com.android.systemui.res.R import com.android.systemui.screenrecord.data.model.ScreenRecordModel import com.android.systemui.screenrecord.data.repository.screenRecordRepository +import com.android.systemui.statusbar.chips.call.ui.viewmodel.CallChipViewModel import com.android.systemui.statusbar.chips.call.ui.viewmodel.CallChipViewModelTest.Companion.createStatusBarIconViewOrNull import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection @@ -269,7 +270,10 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { addOngoingCallState(callNotificationKey) assertThat(latest) - .containsExactly(ScreenRecordChipViewModel.KEY, callNotificationKey) + .containsExactly( + ScreenRecordChipViewModel.KEY, + "${CallChipViewModel.KEY_PREFIX}$callNotificationKey", + ) .inOrder() } @@ -1103,7 +1107,10 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { ) assertThat(latest) - .containsExactly(ScreenRecordChipViewModel.KEY, callNotificationKey) + .containsExactly( + ScreenRecordChipViewModel.KEY, + "${CallChipViewModel.KEY_PREFIX}$callNotificationKey", + ) .inOrder() } @@ -1132,7 +1139,11 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { ) assertThat(latest) - .containsExactly(ScreenRecordChipViewModel.KEY, callNotificationKey, "notif1") + .containsExactly( + ScreenRecordChipViewModel.KEY, + "${CallChipViewModel.KEY_PREFIX}$callNotificationKey", + "notif1", + ) .inOrder() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/ConversationNotificationProcessorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/ConversationNotificationProcessorTest.kt index 0caddf46cd3a..d56890dc5d3f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/ConversationNotificationProcessorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/ConversationNotificationProcessorTest.kt @@ -137,9 +137,28 @@ class ConversationNotificationProcessorTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_NM_SUMMARIZATION) + fun processNotification_messagingStyleUpdateSummarizationToNull() { + val nb = getMessagingNotification() + val newRow: ExpandableNotificationRow = testHelper.createRow(nb.build()) + newRow.entry.setRanking( + RankingBuilder(newRow.entry.ranking).setSummarization("hello").build() + ) + assertThat(conversationNotificationProcessor.processNotification(newRow.entry, nb, logger)) + .isNotNull() + + newRow.entry.setRanking(RankingBuilder(newRow.entry.ranking).setSummarization(null).build()) + + assertThat(conversationNotificationProcessor.processNotification(newRow.entry, nb, logger)) + .isNotNull() + assertThat(nb.build().extras.getCharSequence(EXTRA_SUMMARIZED_CONTENT)).isNull() + } + + @Test + @EnableFlags(Flags.FLAG_NM_SUMMARIZATION) fun processNotification_messagingStyleWithoutSummarization() { val nb = getMessagingNotification() val newRow: ExpandableNotificationRow = testHelper.createRow(nb.build()) + assertThat(conversationNotificationProcessor.processNotification(newRow.entry, nb, logger)) .isNotNull() assertThat(nb.build().extras.getCharSequence(EXTRA_SUMMARIZED_CONTENT)).isNull() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimatorTest.kt index 0bb473721446..c22b03cc1a1b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimatorTest.kt @@ -207,6 +207,21 @@ class PhysicsPropertyAnimatorTest : SysuiTestCase() { } @Test + fun testCancelAnimationResetsOffset() { + PhysicsPropertyAnimator.setProperty( + view, + property, + 200f, + animationProperties, + true, + finishListener, + ) + val propertyData = ViewState.getChildTag(view, property.tag) as PropertyData + propertyData.animator?.cancel() + Assert.assertTrue(propertyData.offset == 0f) + } + + @Test fun testUsingListenerProperties() { val finishListener2 = Mockito.mock(DynamicAnimation.OnAnimationEndListener::class.java) val animationProperties: AnimationProperties = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapterTest.kt index 52f68bf4d729..2bb17e110974 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapterTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapterTest.kt @@ -37,7 +37,10 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) @RunWithLooper +@EnableFlags(NotificationBundleUi.FLAG_NAME) class BundleEntryAdapterTest : SysuiTestCase() { + private lateinit var entry: BundleEntry + private val kosmos = testKosmos() private lateinit var underTest: BundleEntryAdapter @@ -46,120 +49,107 @@ class BundleEntryAdapterTest : SysuiTestCase() { @Before fun setUp() { - underTest = factory.create(BundleEntry("key")) as BundleEntryAdapter + entry = BundleEntry("key") + underTest = factory.create(entry) as BundleEntryAdapter + } + + @Test + fun getBackingHashCode() { + assertThat(underTest.backingHashCode).isEqualTo(entry.hashCode()) } @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) fun getParent_adapter() { assertThat(underTest.parent).isEqualTo(GroupEntry.ROOT_ENTRY) } @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) fun isTopLevelEntry_adapter() { assertThat(underTest.isTopLevelEntry).isTrue() } @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) fun getRow_adapter() { assertThat(underTest.row).isNull() } @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) fun isGroupRoot_adapter() { assertThat(underTest.isGroupRoot).isTrue() } @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) fun getKey_adapter() { assertThat(underTest.key).isEqualTo("key") } @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) fun isClearable_adapter() { assertThat(underTest.isClearable).isTrue() } @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) fun getSummarization_adapter() { assertThat(underTest.summarization).isNull() } @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) fun getContrastedColor_adapter() { assertThat(underTest.getContrastedColor(context, false, Color.WHITE)) .isEqualTo(Notification.COLOR_DEFAULT) } @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) fun canPeek_adapter() { assertThat(underTest.canPeek()).isFalse() } @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) fun getWhen_adapter() { assertThat(underTest.`when`).isEqualTo(0) } @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) fun isColorized() { assertThat(underTest.isColorized).isFalse() } @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) fun getSbn() { assertThat(underTest.sbn).isNull() } @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) fun canDragAndDrop() { assertThat(underTest.canDragAndDrop()).isFalse() } @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) fun isBubble() { assertThat(underTest.isBubble).isFalse() } @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) fun getStyle() { assertThat(underTest.style).isNull() } @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) fun getSectionBucket() { assertThat(underTest.sectionBucket).isEqualTo(underTest.entry.bucket) } @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) fun isAmbient() { assertThat(underTest.isAmbient).isFalse() } @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) fun canShowFullScreen() { assertThat(underTest.isFullScreenCapable()).isFalse() } @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) fun getPeopleNotificationType() { assertThat(underTest.getPeopleNotificationType()).isEqualTo(TYPE_NON_PERSON) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java index 4c18025c4cb7..2869979a230f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java @@ -1804,7 +1804,8 @@ public class NotifCollectionTest extends SysuiTestCase { } private static EntryWithDismissStats entryWithDefaultStats(NotificationEntry entry) { - return new EntryWithDismissStats(entry, defaultStats(entry)); + return new EntryWithDismissStats( + entry, defaultStats(entry), entry.getKey(), entry.hashCode()); } private CollectionEvent postNotif(NotificationEntryBuilder builder) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt index 02c6a484bd43..25b5d68cfbfa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt @@ -64,6 +64,17 @@ class NotificationEntryAdapterTest : SysuiTestCase() { @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getBackingHashCode() { + val entry = + NotificationEntryBuilder() + .build() + + underTest = factory.create(entry) as NotificationEntryAdapter + assertThat(underTest.backingHashCode).isEqualTo(entry.hashCode()) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) fun getParent_adapter() { val ge = GroupEntryBuilder().build() val notification: Notification = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java index bfd700dcc302..52996ee1e369 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java @@ -309,24 +309,6 @@ public class NotificationEntryTest extends SysuiTestCase { } @Test - @EnableFlags(PromotedNotificationUi.FLAG_NAME) - @DisableFlags(StatusBarNotifChips.FLAG_NAME) - public void isPromotedOngoing_uiFlagOnAndNotifHasFlag_true() { - mEntry.getSbn().getNotification().flags |= FLAG_PROMOTED_ONGOING; - - assertTrue(mEntry.isPromotedOngoing()); - } - - @Test - @EnableFlags(StatusBarNotifChips.FLAG_NAME) - @DisableFlags(PromotedNotificationUi.FLAG_NAME) - public void isPromotedOngoing_statusBarNotifChipsFlagOnAndNotifHasFlag_true() { - mEntry.getSbn().getNotification().flags |= FLAG_PROMOTED_ONGOING; - - assertTrue(mEntry.isPromotedOngoing()); - } - - @Test public void testIsNotificationVisibilityPrivate_true() { assertTrue(mEntry.isNotificationVisibilityPrivate()); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index 44d88c31c5f1..247c66aebad7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -17,7 +17,9 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.app.Notification.GROUP_ALERT_ALL import android.app.Notification.GROUP_ALERT_SUMMARY +import android.app.NotificationChannel.SYSTEM_RESERVED_IDS import android.platform.test.annotations.EnableFlags +import android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION import android.testing.TestableLooper.RunWithLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -28,6 +30,7 @@ import com.android.systemui.log.logcatLogBuffer import com.android.systemui.statusbar.NotificationRemoteInputManager import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips +import com.android.systemui.statusbar.chips.uievents.statusBarChipsUiEventLogger import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.NotifPipeline @@ -49,6 +52,7 @@ import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinde import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.DecisionImpl import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.FullScreenIntentDecisionImpl +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionLogger import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider import com.android.systemui.statusbar.notification.row.mockNotificationActionClickManager import com.android.systemui.statusbar.notification.shared.NotificationBundleUi @@ -60,7 +64,6 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.withArgCaptor import com.android.systemui.util.time.FakeSystemClock -import java.util.ArrayList import java.util.function.Consumer import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals @@ -104,6 +107,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { private val notifPipeline: NotifPipeline = mock() private val logger = HeadsUpCoordinatorLogger(logcatLogBuffer(), verbose = true) + private val interruptLogger: VisualInterruptionDecisionLogger = mock() private val headsUpManager: HeadsUpManagerImpl = mock() private val headsUpViewBinder: HeadsUpViewBinder = mock() private val visualInterruptionDecisionProvider: VisualInterruptionDecisionProvider = mock() @@ -134,6 +138,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { HeadsUpCoordinator( kosmos.applicationCoroutineScope, logger, + interruptLogger, systemClock, notifCollection, headsUpManager, @@ -144,6 +149,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { launchFullScreenIntentProvider, flags, statusBarNotificationChipsInteractor, + kosmos.statusBarChipsUiEventLogger, headerController, executor, ) @@ -164,15 +170,15 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(notifPipeline).addOnBeforeFinalizeFilterListener(capture()) } onHeadsUpChangedListener = withArgCaptor { verify(headsUpManager).addListener(capture()) } - actionPressListener = if (NotificationBundleUi.isEnabled) { - withArgCaptor { - verify(kosmos.mockNotificationActionClickManager).addActionClickListener(capture()) + actionPressListener = + if (NotificationBundleUi.isEnabled) { + withArgCaptor { + verify(kosmos.mockNotificationActionClickManager) + .addActionClickListener(capture()) + } + } else { + withArgCaptor { verify(remoteInputManager).addActionPressListener(capture()) } } - } else { - withArgCaptor { - verify(remoteInputManager).addActionPressListener(capture()) - } - } given(headsUpManager.allEntries).willAnswer { huns.stream() } given(headsUpManager.isHeadsUpEntry(anyString())).willAnswer { invocation -> val key = invocation.getArgument<String>(0) @@ -918,6 +924,48 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { assertFalse(groupSummary.hasInterrupted()) } + private fun helpTestNoTransferToBundleChildForChannel(channelId: String) { + // Set up for normal alert transfer from summary to child + // but here child is classified so it should not happen + val bundleChild = + helper.createClassifiedEntry(/* isSummary= */ false, GROUP_ALERT_SUMMARY, channelId); + setShouldHeadsUp(bundleChild, true) + setShouldHeadsUp(groupSummary, true) + whenever(notifPipeline.allNotifs).thenReturn(listOf(groupSummary, bundleChild)) + + collectionListener.onEntryAdded(groupSummary) + collectionListener.onEntryAdded(bundleChild) + + beforeTransformGroupsListener.onBeforeTransformGroups(listOf(groupSummary, bundleChild)) + beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupSummary, bundleChild)) + + verify(headsUpViewBinder, never()).bindHeadsUpView(eq(groupSummary), any(), any()) + verify(headsUpViewBinder, never()).bindHeadsUpView(eq(bundleChild), any(), any()) + + verify(headsUpManager, never()).showNotification(groupSummary) + verify(headsUpManager, never()).showNotification(bundleChild) + + // Capture last param + val decision = withArgCaptor { + verify(interruptLogger) + .logDecision(capture(), capture(), capture()) + } + assertFalse(decision.shouldInterrupt) + assertEquals(decision.logReason, "disqualified-transfer-target") + + // Must clear invocations, otherwise these calls get stored for the next call from the same + // test, which complains that there are more invocations than expected + clearInvocations(interruptLogger) + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_CLASSIFICATION) + fun testNoTransfer_toBundleChild() { + for (id in SYSTEM_RESERVED_IDS) { + helpTestNoTransferToBundleChildForChannel(id) + } + } + @Test fun testOnRankingApplied_newEntryShouldAlert() { // GIVEN that mEntry has never interrupted in the past, and now should diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java index f9405af3f85d..340ce673e01e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.collection.coordinator; import static android.app.NotificationChannel.SYSTEM_RESERVED_IDS; import static android.app.NotificationManager.IMPORTANCE_DEFAULT; import static android.app.NotificationManager.IMPORTANCE_LOW; +import static android.app.NotificationManager.IMPORTANCE_MIN; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST; @@ -36,7 +37,6 @@ import static org.mockito.Mockito.when; import android.app.Notification; import android.app.NotificationChannel; -import android.app.NotificationManager; import android.platform.test.annotations.EnableFlags; import androidx.annotation.Nullable; @@ -265,18 +265,35 @@ public class RankingCoordinatorTest extends SysuiTestCase { } @Test - public void testIncludeInSectionSilent() { - // GIVEN the entry isn't high priority + public void testSilentSectioner_accepts_highPriorityFalse_ambientFalse() { when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false); setRankingAmbient(false); + assertOnlyInSection(mEntry, mSilentSectioner); + } + + @Test + public void testSilentSectioner_rejects_highPriorityFalse_ambientTrue() { + when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false); + setRankingAmbient(true); + assertFalse(mSilentSectioner.isInSection(mEntry)); + } + + @Test + public void testSilentSectioner_rejects_highPriorityTrue_ambientFalse() { + when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(true); + setRankingAmbient(false); + assertFalse(mSilentSectioner.isInSection(mEntry)); + } - // THEN entry is in the silent section - assertFalse(mAlertingSectioner.isInSection(mEntry)); - assertTrue(mSilentSectioner.isInSection(mEntry)); + @Test + public void testSilentSectioner_rejects_highPriorityTrue_ambientTrue() { + when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(true); + setRankingAmbient(true); + assertFalse(mSilentSectioner.isInSection(mEntry)); } @Test - public void testSilentSectioner_acceptsBundle() { + public void testSilentSectioner_accepts_bundle() { BundleEntry bundleEntry = new BundleEntry("testBundleKey"); assertTrue(mSilentSectioner.isInSection(bundleEntry)); } @@ -291,14 +308,7 @@ public class RankingCoordinatorTest extends SysuiTestCase { public void testMinSection() { when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false); setRankingAmbient(true); - assertInSection(mEntry, mMinimizedSectioner); - } - - @Test - public void testSilentSection() { - when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false); - setRankingAmbient(false); - assertInSection(mEntry, mSilentSectioner); + assertOnlyInSection(mEntry, mMinimizedSectioner); } @Test @@ -344,7 +354,8 @@ public class RankingCoordinatorTest extends SysuiTestCase { @Test public void testAlertingSectioner_rejectsBundle() { for (String id : SYSTEM_RESERVED_IDS) { - assertFalse(mAlertingSectioner.isInSection(makeClassifiedNotifEntry(id))); + assertFalse( + mAlertingSectioner.isInSection(makeClassifiedNotifEntry(id, IMPORTANCE_LOW))); } } @@ -369,7 +380,7 @@ public class RankingCoordinatorTest extends SysuiTestCase { reset(mInvalidationListener); } - private void assertInSection(NotificationEntry entry, NotifSectioner section) { + private void assertOnlyInSection(NotificationEntry entry, NotifSectioner section) { for (NotifSectioner current: mSections) { if (current == section) { assertTrue(current.isInSection(entry)); @@ -396,16 +407,17 @@ public class RankingCoordinatorTest extends SysuiTestCase { private void setRankingAmbient(boolean ambient) { mEntry.setRanking(new RankingBuilder(mEntry.getRanking()) .setImportance(ambient - ? NotificationManager.IMPORTANCE_MIN + ? IMPORTANCE_MIN : IMPORTANCE_DEFAULT) .build()); assertEquals(ambient, mEntry.getRanking().isAmbient()); } - private NotificationEntry makeClassifiedNotifEntry(String channelId) { - NotificationChannel channel = new NotificationChannel(channelId, channelId, IMPORTANCE_LOW); + private NotificationEntry makeClassifiedNotifEntry(String channelId, int importance) { + NotificationChannel channel = new NotificationChannel(channelId, channelId, importance); return new NotificationEntryBuilder() - .updateRanking((rankingBuilder -> rankingBuilder.setChannel(channel))) + .updateRanking((rankingBuilder -> + rankingBuilder.setChannel(channel).setImportance(importance))) .build(); } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt index 8560b66d961f..5b0e4e139d4e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt @@ -749,20 +749,6 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(getIsSticky_promotedAndExpanded()).isFalse() } - @Test - @EnableFlags(PromotedNotificationUi.FLAG_NAME) - @DisableFlags(StatusBarNotifChips.FLAG_NAME) - fun testIsSticky_promotedAndExpanded_promotedUiFlagOn_false() { - assertThat(getIsSticky_promotedAndExpanded()).isFalse() - } - - @Test - @EnableFlags(StatusBarNotifChips.FLAG_NAME) - @DisableFlags(PromotedNotificationUi.FLAG_NAME) - fun testIsSticky_promotedAndExpanded_notifChipsFlagOn_false() { - assertThat(getIsSticky_promotedAndExpanded()).isFalse() - } - private fun getIsSticky_promotedAndExpanded(): Boolean { val notif = Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_person).build() notif.flags = FLAG_PROMOTED_ONGOING diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java index d61fc05c699f..28ca891b2771 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.logging; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.logging.nano.Notifications; @@ -49,6 +50,11 @@ public class NotificationPanelLoggerFake implements NotificationPanelLogger { public void logNotificationDrag(NotificationEntry draggedNotification) { } + @Override + public void logNotificationDrag(EntryAdapter draggedNotification) { + + } + public static class CallRecord { public boolean isLockscreen; public Notifications.NotificationList list; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt index cc016b9768b7..df77b5ad46e8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt @@ -33,6 +33,8 @@ import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.runTest import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips @@ -58,132 +60,122 @@ import org.junit.runner.RunWith class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { private val kosmos = testKosmos().apply { systemClock = fakeSystemClock } - private val underTest = kosmos.promotedNotificationContentExtractor - private val systemClock = kosmos.fakeSystemClock - private val rowImageInflater = - RowImageInflater.newInstance(previousIndex = null, reinflating = false) - private val imageModelProvider by lazy { rowImageInflater.useForContentModel() } + private val Kosmos.underTest by Kosmos.Fixture { promotedNotificationContentExtractor } + private val Kosmos.rowImageInflater by + Kosmos.Fixture { RowImageInflater.newInstance(previousIndex = null, reinflating = false) } + private val Kosmos.imageModelProvider by + Kosmos.Fixture { rowImageInflater.useForContentModel() } @Test @DisableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun shouldNotExtract_bothFlagsDisabled() { - val notif = createEntry() - val content = extractContent(notif) - assertThat(content).isNull() - } - - @Test - @EnableFlags(PromotedNotificationUi.FLAG_NAME) - @DisableFlags(StatusBarNotifChips.FLAG_NAME) - fun shouldExtract_promotedNotificationUiFlagEnabled() { - val entry = createEntry() - val content = extractContent(entry) - assertThat(content).isNotNull() - } - - @Test - @EnableFlags(StatusBarNotifChips.FLAG_NAME) - @DisableFlags(PromotedNotificationUi.FLAG_NAME) - fun shouldExtract_statusBarNotifChipsFlagEnabled() { - val entry = createEntry() - val content = extractContent(entry) - assertThat(content).isNotNull() - } + fun shouldNotExtract_bothFlagsDisabled() = + kosmos.runTest { + val notif = createEntry() + val content = extractContent(notif) + assertThat(content).isNull() + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun shouldExtract_bothFlagsEnabled() { - val entry = createEntry() - val content = extractContent(entry) - assertThat(content).isNotNull() - } + fun shouldExtract_bothFlagsEnabled() = + kosmos.runTest { + val entry = createEntry() + val content = extractContent(entry) + assertThat(content).isNotNull() + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun shouldNotExtract_becauseNotPromoted() { - val entry = createEntry(promoted = false) - val content = extractContent(entry) - assertThat(content).isNull() - } + fun shouldNotExtract_becauseNotPromoted() = + kosmos.runTest { + val entry = createEntry(promoted = false) + val content = extractContent(entry) + assertThat(content).isNull() + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractsContent_commonFields() { - val entry = createEntry { - setSubText(TEST_SUB_TEXT) - setContentTitle(TEST_CONTENT_TITLE) - setContentText(TEST_CONTENT_TEXT) - } + fun extractsContent_commonFields() = + kosmos.runTest { + val entry = createEntry { + setSubText(TEST_SUB_TEXT) + setContentTitle(TEST_CONTENT_TITLE) + setContentText(TEST_CONTENT_TEXT) + } - val content = requireContent(entry) + val content = requireContent(entry) - content.privateVersion.apply { - assertThat(subText).isEqualTo(TEST_SUB_TEXT) - assertThat(title).isEqualTo(TEST_CONTENT_TITLE) - assertThat(text).isEqualTo(TEST_CONTENT_TEXT) - } + content.privateVersion.apply { + assertThat(subText).isEqualTo(TEST_SUB_TEXT) + assertThat(title).isEqualTo(TEST_CONTENT_TITLE) + assertThat(text).isEqualTo(TEST_CONTENT_TEXT) + } - content.publicVersion.apply { - assertThat(subText).isNull() - assertThat(title).isNull() - assertThat(text).isNull() + content.publicVersion.apply { + assertThat(subText).isNull() + assertThat(title).isNull() + assertThat(text).isNull() + } } - } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractsContent_commonFields_noRedaction() { - val entry = createEntry { - setSubText(TEST_SUB_TEXT) - setContentTitle(TEST_CONTENT_TITLE) - setContentText(TEST_CONTENT_TEXT) - } + fun extractsContent_commonFields_noRedaction() = + kosmos.runTest { + val entry = createEntry { + setSubText(TEST_SUB_TEXT) + setContentTitle(TEST_CONTENT_TITLE) + setContentText(TEST_CONTENT_TEXT) + } - val content = requireContent(entry, redactionType = REDACTION_TYPE_NONE) + val content = requireContent(entry, redactionType = REDACTION_TYPE_NONE) - content.privateVersion.apply { - assertThat(subText).isEqualTo(TEST_SUB_TEXT) - assertThat(title).isEqualTo(TEST_CONTENT_TITLE) - assertThat(text).isEqualTo(TEST_CONTENT_TEXT) - } + content.privateVersion.apply { + assertThat(subText).isEqualTo(TEST_SUB_TEXT) + assertThat(title).isEqualTo(TEST_CONTENT_TITLE) + assertThat(text).isEqualTo(TEST_CONTENT_TEXT) + } - content.publicVersion.apply { - assertThat(subText).isEqualTo(TEST_SUB_TEXT) - assertThat(title).isEqualTo(TEST_CONTENT_TITLE) - assertThat(text).isEqualTo(TEST_CONTENT_TEXT) + content.publicVersion.apply { + assertThat(subText).isEqualTo(TEST_SUB_TEXT) + assertThat(title).isEqualTo(TEST_CONTENT_TITLE) + assertThat(text).isEqualTo(TEST_CONTENT_TEXT) + } } - } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractContent_wasPromotedAutomatically_false() { - val entry = createEntry { extras.putBoolean(EXTRA_WAS_AUTOMATICALLY_PROMOTED, false) } + fun extractContent_wasPromotedAutomatically_false() = + kosmos.runTest { + val entry = createEntry { extras.putBoolean(EXTRA_WAS_AUTOMATICALLY_PROMOTED, false) } - val content = requireContent(entry).privateVersion + val content = requireContent(entry).privateVersion - assertThat(content.wasPromotedAutomatically).isFalse() - } + assertThat(content.wasPromotedAutomatically).isFalse() + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractContent_wasPromotedAutomatically_true() { - val entry = createEntry { extras.putBoolean(EXTRA_WAS_AUTOMATICALLY_PROMOTED, true) } + fun extractContent_wasPromotedAutomatically_true() = + kosmos.runTest { + val entry = createEntry { extras.putBoolean(EXTRA_WAS_AUTOMATICALLY_PROMOTED, true) } - val content = requireContent(entry).privateVersion + val content = requireContent(entry).privateVersion - assertThat(content.wasPromotedAutomatically).isTrue() - } + assertThat(content.wasPromotedAutomatically).isTrue() + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) @DisableFlags(android.app.Flags.FLAG_API_RICH_ONGOING) - fun extractContent_apiFlagOff_shortCriticalTextNotExtracted() { - val entry = createEntry { setShortCriticalText(TEST_SHORT_CRITICAL_TEXT) } + fun extractContent_apiFlagOff_shortCriticalTextNotExtracted() = + kosmos.runTest { + val entry = createEntry { setShortCriticalText(TEST_SHORT_CRITICAL_TEXT) } - val content = requireContent(entry).privateVersion + val content = requireContent(entry).privateVersion - assertThat(content.text).isNull() - } + assertThat(content.text).isNull() + } @Test @EnableFlags( @@ -191,13 +183,14 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { StatusBarNotifChips.FLAG_NAME, android.app.Flags.FLAG_API_RICH_ONGOING, ) - fun extractContent_apiFlagOn_shortCriticalTextExtracted() { - val entry = createEntry { setShortCriticalText(TEST_SHORT_CRITICAL_TEXT) } + fun extractContent_apiFlagOn_shortCriticalTextExtracted() = + kosmos.runTest { + val entry = createEntry { setShortCriticalText(TEST_SHORT_CRITICAL_TEXT) } - val content = requireContent(entry).privateVersion + val content = requireContent(entry).privateVersion - assertThat(content.shortCriticalText).isEqualTo(TEST_SHORT_CRITICAL_TEXT) - } + assertThat(content.shortCriticalText).isEqualTo(TEST_SHORT_CRITICAL_TEXT) + } @Test @EnableFlags( @@ -205,165 +198,188 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { StatusBarNotifChips.FLAG_NAME, android.app.Flags.FLAG_API_RICH_ONGOING, ) - fun extractContent_noShortCriticalTextSet_textIsNull() { - val entry = createEntry { setShortCriticalText(null) } + fun extractContent_noShortCriticalTextSet_textIsNull() = + kosmos.runTest { + val entry = createEntry { setShortCriticalText(null) } - val content = requireContent(entry).privateVersion + val content = requireContent(entry).privateVersion - assertThat(content.shortCriticalText).isNull() - } + assertThat(content.shortCriticalText).isNull() + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractTime_none() { - assertExtractedTime(hasTime = false, hasChronometer = false, expected = ExpectedTime.Null) - } + fun extractTime_none() = + kosmos.runTest { + assertExtractedTime( + hasTime = false, + hasChronometer = false, + expected = ExpectedTime.Null, + ) + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractTime_basicTimeZero() { - assertExtractedTime( - hasTime = true, - hasChronometer = false, - provided = ProvidedTime.Value(0L), - expected = ExpectedTime.Time, - ) - } + fun extractTime_basicTimeZero() = + kosmos.runTest { + assertExtractedTime( + hasTime = true, + hasChronometer = false, + provided = ProvidedTime.Value(0L), + expected = ExpectedTime.Time, + ) + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractTime_basicTimeNow() { - assertExtractedTime( - hasTime = true, - hasChronometer = false, - provided = ProvidedTime.Offset(Duration.ZERO), - expected = ExpectedTime.Time, - ) - } + fun extractTime_basicTimeNow() = + kosmos.runTest { + assertExtractedTime( + hasTime = true, + hasChronometer = false, + provided = ProvidedTime.Offset(Duration.ZERO), + expected = ExpectedTime.Time, + ) + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractTime_basicTimePast() { - assertExtractedTime( - hasTime = true, - hasChronometer = false, - provided = ProvidedTime.Offset((-5).minutes), - expected = ExpectedTime.Time, - ) - } + fun extractTime_basicTimePast() = + kosmos.runTest { + assertExtractedTime( + hasTime = true, + hasChronometer = false, + provided = ProvidedTime.Offset((-5).minutes), + expected = ExpectedTime.Time, + ) + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractTime_basicTimeFuture() { - assertExtractedTime( - hasTime = true, - hasChronometer = false, - provided = ProvidedTime.Offset(5.minutes), - expected = ExpectedTime.Time, - ) - } + fun extractTime_basicTimeFuture() = + kosmos.runTest { + assertExtractedTime( + hasTime = true, + hasChronometer = false, + provided = ProvidedTime.Offset(5.minutes), + expected = ExpectedTime.Time, + ) + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractTime_countUpZero() { - assertExtractedTime( - hasTime = false, - hasChronometer = true, - isCountDown = false, - provided = ProvidedTime.Value(0L), - expected = ExpectedTime.CountUp, - ) - } + fun extractTime_countUpZero() = + kosmos.runTest { + assertExtractedTime( + hasTime = false, + hasChronometer = true, + isCountDown = false, + provided = ProvidedTime.Value(0L), + expected = ExpectedTime.CountUp, + ) + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractTime_countUpNow() { - assertExtractedTime( - hasTime = false, - hasChronometer = true, - isCountDown = false, - provided = ProvidedTime.Offset(Duration.ZERO), - expected = ExpectedTime.CountUp, - ) - } + fun extractTime_countUpNow() = + kosmos.runTest { + assertExtractedTime( + hasTime = false, + hasChronometer = true, + isCountDown = false, + provided = ProvidedTime.Offset(Duration.ZERO), + expected = ExpectedTime.CountUp, + ) + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractTime_countUpPast() { - assertExtractedTime( - hasTime = false, - hasChronometer = true, - isCountDown = false, - provided = ProvidedTime.Offset((-5).minutes), - expected = ExpectedTime.CountUp, - ) - } + fun extractTime_countUpPast() = + kosmos.runTest { + assertExtractedTime( + hasTime = false, + hasChronometer = true, + isCountDown = false, + provided = ProvidedTime.Offset((-5).minutes), + expected = ExpectedTime.CountUp, + ) + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractTime_countUpFuture() { - assertExtractedTime( - hasTime = false, - hasChronometer = true, - isCountDown = false, - provided = ProvidedTime.Offset(5.minutes), - expected = ExpectedTime.CountUp, - ) - } + fun extractTime_countUpFuture() = + kosmos.runTest { + assertExtractedTime( + hasTime = false, + hasChronometer = true, + isCountDown = false, + provided = ProvidedTime.Offset(5.minutes), + expected = ExpectedTime.CountUp, + ) + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractTime_countDownZero() { - assertExtractedTime( - hasTime = false, - hasChronometer = true, - isCountDown = true, - provided = ProvidedTime.Value(0L), - expected = ExpectedTime.CountDown, - ) - } + fun extractTime_countDownZero() = + kosmos.runTest { + assertExtractedTime( + hasTime = false, + hasChronometer = true, + isCountDown = true, + provided = ProvidedTime.Value(0L), + expected = ExpectedTime.CountDown, + ) + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractTime_countDownNow() { - assertExtractedTime( - hasTime = false, - hasChronometer = true, - isCountDown = true, - provided = ProvidedTime.Offset(Duration.ZERO), - expected = ExpectedTime.CountDown, - ) - } + fun extractTime_countDownNow() = + kosmos.runTest { + assertExtractedTime( + hasTime = false, + hasChronometer = true, + isCountDown = true, + provided = ProvidedTime.Offset(Duration.ZERO), + expected = ExpectedTime.CountDown, + ) + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractTime_countDownPast() { - assertExtractedTime( - hasTime = false, - hasChronometer = true, - isCountDown = true, - provided = ProvidedTime.Offset((-5).minutes), - expected = ExpectedTime.CountDown, - ) - } + fun extractTime_countDownPast() = + kosmos.runTest { + assertExtractedTime( + hasTime = false, + hasChronometer = true, + isCountDown = true, + provided = ProvidedTime.Offset((-5).minutes), + expected = ExpectedTime.CountDown, + ) + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractTime_countDownFuture() { - assertExtractedTime( - hasTime = false, - hasChronometer = true, - isCountDown = true, - provided = ProvidedTime.Offset(5.minutes), - expected = ExpectedTime.CountDown, - ) - } + fun extractTime_countDownFuture() = + kosmos.runTest { + assertExtractedTime( + hasTime = false, + hasChronometer = true, + isCountDown = true, + provided = ProvidedTime.Offset(5.minutes), + expected = ExpectedTime.CountDown, + ) + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractTime_prefersChronometerToWhen() { - assertExtractedTime(hasTime = true, hasChronometer = true, expected = ExpectedTime.CountUp) - } + fun extractTime_prefersChronometerToWhen() = + kosmos.runTest { + assertExtractedTime( + hasTime = true, + hasChronometer = true, + expected = ExpectedTime.CountUp, + ) + } private sealed class ProvidedTime { data class Value(val value: Long) : ProvidedTime() @@ -378,7 +394,7 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { CountDown, } - private fun assertExtractedTime( + private fun Kosmos.assertExtractedTime( hasTime: Boolean = false, hasChronometer: Boolean = false, isCountDown: Boolean = false, @@ -387,8 +403,8 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { ) { // Set the two timebases to different (arbitrary) numbers, so we can verify whether the // extractor is doing the timebase adjustment correctly. - systemClock.setCurrentTimeMillis(1_739_570_992_579L) - systemClock.setElapsedRealtime(1_380_967_080L) + fakeSystemClock.setCurrentTimeMillis(1_739_570_992_579L) + fakeSystemClock.setElapsedRealtime(1_380_967_080L) val providedCurrentTime = when (provided) { @@ -437,122 +453,130 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractContent_fromBaseStyle() { - val entry = createEntry { setStyle(null) } + fun extractContent_fromBaseStyle() = + kosmos.runTest { + val entry = createEntry { setStyle(null) } - val content = requireContent(entry) + val content = requireContent(entry) - assertThat(content.privateVersion.style).isEqualTo(Style.Base) - assertThat(content.publicVersion.style).isEqualTo(Style.Base) - } + assertThat(content.privateVersion.style).isEqualTo(Style.Base) + assertThat(content.publicVersion.style).isEqualTo(Style.Base) + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractContent_fromBigPictureStyle() { - val entry = createEntry { setStyle(BigPictureStyle()) } + fun extractContent_fromBigPictureStyle() = + kosmos.runTest { + val entry = createEntry { setStyle(BigPictureStyle()) } - val content = requireContent(entry) + val content = requireContent(entry) - assertThat(content.privateVersion.style).isEqualTo(Style.BigPicture) - assertThat(content.publicVersion.style).isEqualTo(Style.Base) - } + assertThat(content.privateVersion.style).isEqualTo(Style.BigPicture) + assertThat(content.publicVersion.style).isEqualTo(Style.Base) + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractContent_fromBigTextStyle() { - val entry = createEntry { - setContentTitle(TEST_CONTENT_TITLE) - setContentText(TEST_CONTENT_TEXT) - setStyle( - BigTextStyle() - .bigText(TEST_BIG_TEXT) - .setBigContentTitle(TEST_BIG_CONTENT_TITLE) - .setSummaryText(TEST_SUMMARY_TEXT) - ) - } + fun extractContent_fromBigTextStyle() = + kosmos.runTest { + val entry = createEntry { + setContentTitle(TEST_CONTENT_TITLE) + setContentText(TEST_CONTENT_TEXT) + setStyle( + BigTextStyle() + .bigText(TEST_BIG_TEXT) + .setBigContentTitle(TEST_BIG_CONTENT_TITLE) + .setSummaryText(TEST_SUMMARY_TEXT) + ) + } - val content = requireContent(entry) + val content = requireContent(entry) - assertThat(content.privateVersion.style).isEqualTo(Style.BigText) - assertThat(content.privateVersion.title).isEqualTo(TEST_BIG_CONTENT_TITLE) - assertThat(content.privateVersion.text).isEqualTo(TEST_BIG_TEXT) + assertThat(content.privateVersion.style).isEqualTo(Style.BigText) + assertThat(content.privateVersion.title).isEqualTo(TEST_BIG_CONTENT_TITLE) + assertThat(content.privateVersion.text).isEqualTo(TEST_BIG_TEXT) - assertThat(content.publicVersion.style).isEqualTo(Style.Base) - assertThat(content.publicVersion.title).isNull() - assertThat(content.publicVersion.text).isNull() - } + assertThat(content.publicVersion.style).isEqualTo(Style.Base) + assertThat(content.publicVersion.title).isNull() + assertThat(content.publicVersion.text).isNull() + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractContent_fromBigTextStyle_fallbackToContentTitle() { - val entry = createEntry { - setContentTitle(TEST_CONTENT_TITLE) - setContentText(TEST_CONTENT_TEXT) - setStyle( - BigTextStyle() - .bigText(TEST_BIG_TEXT) - // bigContentTitle unset - .setSummaryText(TEST_SUMMARY_TEXT) - ) - } + fun extractContent_fromBigTextStyle_fallbackToContentTitle() = + kosmos.runTest { + val entry = createEntry { + setContentTitle(TEST_CONTENT_TITLE) + setContentText(TEST_CONTENT_TEXT) + setStyle( + BigTextStyle() + .bigText(TEST_BIG_TEXT) + // bigContentTitle unset + .setSummaryText(TEST_SUMMARY_TEXT) + ) + } - val content = requireContent(entry) + val content = requireContent(entry) - assertThat(content.privateVersion.style).isEqualTo(Style.BigText) - assertThat(content.privateVersion.title).isEqualTo(TEST_CONTENT_TITLE) - assertThat(content.privateVersion.text).isEqualTo(TEST_BIG_TEXT) + assertThat(content.privateVersion.style).isEqualTo(Style.BigText) + assertThat(content.privateVersion.title).isEqualTo(TEST_CONTENT_TITLE) + assertThat(content.privateVersion.text).isEqualTo(TEST_BIG_TEXT) - assertThat(content.publicVersion.style).isEqualTo(Style.Base) - assertThat(content.publicVersion.title).isNull() - assertThat(content.publicVersion.text).isNull() - } + assertThat(content.publicVersion.style).isEqualTo(Style.Base) + assertThat(content.publicVersion.title).isNull() + assertThat(content.publicVersion.text).isNull() + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractContent_fromBigTextStyle_fallbackToContentText() { - val entry = createEntry { - setContentTitle(TEST_CONTENT_TITLE) - setContentText(TEST_CONTENT_TEXT) - setStyle( - BigTextStyle() - // bigText unset - .setBigContentTitle(TEST_BIG_CONTENT_TITLE) - .setSummaryText(TEST_SUMMARY_TEXT) - ) - } + fun extractContent_fromBigTextStyle_fallbackToContentText() = + kosmos.runTest { + val entry = createEntry { + setContentTitle(TEST_CONTENT_TITLE) + setContentText(TEST_CONTENT_TEXT) + setStyle( + BigTextStyle() + // bigText unset + .setBigContentTitle(TEST_BIG_CONTENT_TITLE) + .setSummaryText(TEST_SUMMARY_TEXT) + ) + } - val content = requireContent(entry) + val content = requireContent(entry) - assertThat(content.privateVersion.style).isEqualTo(Style.BigText) - assertThat(content.privateVersion.title).isEqualTo(TEST_BIG_CONTENT_TITLE) - assertThat(content.privateVersion.text).isEqualTo(TEST_CONTENT_TEXT) + assertThat(content.privateVersion.style).isEqualTo(Style.BigText) + assertThat(content.privateVersion.title).isEqualTo(TEST_BIG_CONTENT_TITLE) + assertThat(content.privateVersion.text).isEqualTo(TEST_CONTENT_TEXT) - assertThat(content.publicVersion.style).isEqualTo(Style.Base) - assertThat(content.publicVersion.title).isNull() - assertThat(content.publicVersion.text).isNull() - } + assertThat(content.publicVersion.style).isEqualTo(Style.Base) + assertThat(content.publicVersion.title).isNull() + assertThat(content.publicVersion.text).isNull() + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractContent_fromCallStyle() { - val hangUpIntent = - PendingIntent.getBroadcast( - context, - 0, - Intent("hangup_action"), - PendingIntent.FLAG_IMMUTABLE, - ) - val entry = createEntry { setStyle(CallStyle.forOngoingCall(TEST_PERSON, hangUpIntent)) } + fun extractContent_fromCallStyle() = + kosmos.runTest { + val hangUpIntent = + PendingIntent.getBroadcast( + context, + 0, + Intent("hangup_action"), + PendingIntent.FLAG_IMMUTABLE, + ) + val entry = createEntry { + setStyle(CallStyle.forOngoingCall(TEST_PERSON, hangUpIntent)) + } - val content = requireContent(entry) + val content = requireContent(entry) - assertThat(content.privateVersion.style).isEqualTo(Style.Call) - assertThat(content.privateVersion.title).isEqualTo(TEST_PERSON_NAME) + assertThat(content.privateVersion.style).isEqualTo(Style.Call) + assertThat(content.privateVersion.title).isEqualTo(TEST_PERSON_NAME) - assertThat(content.publicVersion.style).isEqualTo(Style.Base) - assertThat(content.publicVersion.title).isNull() - assertThat(content.publicVersion.text).isNull() - } + assertThat(content.publicVersion.style).isEqualTo(Style.Base) + assertThat(content.publicVersion.title).isNull() + assertThat(content.publicVersion.text).isNull() + } @Test @EnableFlags( @@ -560,75 +584,79 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { StatusBarNotifChips.FLAG_NAME, android.app.Flags.FLAG_API_RICH_ONGOING, ) - fun extractContent_fromProgressStyle() { - val entry = createEntry { - setStyle(ProgressStyle().addProgressSegment(Segment(100)).setProgress(75)) - } + fun extractContent_fromProgressStyle() = + kosmos.runTest { + val entry = createEntry { + setStyle(ProgressStyle().addProgressSegment(Segment(100)).setProgress(75)) + } - val content = requireContent(entry) + val content = requireContent(entry) - assertThat(content.privateVersion.style).isEqualTo(Style.Progress) - val newProgress = assertNotNull(content.privateVersion.newProgress) - assertThat(newProgress.progress).isEqualTo(75) - assertThat(newProgress.progressMax).isEqualTo(100) + assertThat(content.privateVersion.style).isEqualTo(Style.Progress) + val newProgress = assertNotNull(content.privateVersion.newProgress) + assertThat(newProgress.progress).isEqualTo(75) + assertThat(newProgress.progressMax).isEqualTo(100) - assertThat(content.publicVersion.style).isEqualTo(Style.Base) - assertThat(content.publicVersion.title).isNull() - assertThat(content.publicVersion.text).isNull() - assertThat(content.publicVersion.newProgress).isNull() - } + assertThat(content.publicVersion.style).isEqualTo(Style.Base) + assertThat(content.publicVersion.title).isNull() + assertThat(content.publicVersion.text).isNull() + assertThat(content.publicVersion.newProgress).isNull() + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractContent_fromIneligibleStyle() { - val entry = createEntry { - setStyle(MessagingStyle(TEST_PERSON).addMessage("message text", 0L, TEST_PERSON)) - } + fun extractContent_fromIneligibleStyle() = + kosmos.runTest { + val entry = createEntry { + setStyle(MessagingStyle(TEST_PERSON).addMessage("message text", 0L, TEST_PERSON)) + } - val content = requireContent(entry) + val content = requireContent(entry) - assertThat(content.privateVersion.style).isEqualTo(Style.Ineligible) + assertThat(content.privateVersion.style).isEqualTo(Style.Ineligible) - assertThat(content.publicVersion.style).isEqualTo(Style.Ineligible) - } + assertThat(content.publicVersion.style).isEqualTo(Style.Ineligible) + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractContent_fromOldProgressDeterminate() { - val entry = createEntry { - setProgress(TEST_PROGRESS_MAX, TEST_PROGRESS, /* indeterminate= */ false) - } + fun extractContent_fromOldProgressDeterminate() = + kosmos.runTest { + val entry = createEntry { + setProgress(TEST_PROGRESS_MAX, TEST_PROGRESS, /* indeterminate= */ false) + } - val content = requireContent(entry) + val content = requireContent(entry) - val oldProgress = assertNotNull(content.privateVersion.oldProgress) + val oldProgress = assertNotNull(content.privateVersion.oldProgress) - assertThat(oldProgress.progress).isEqualTo(TEST_PROGRESS) - assertThat(oldProgress.max).isEqualTo(TEST_PROGRESS_MAX) - assertThat(oldProgress.isIndeterminate).isFalse() - } + assertThat(oldProgress.progress).isEqualTo(TEST_PROGRESS) + assertThat(oldProgress.max).isEqualTo(TEST_PROGRESS_MAX) + assertThat(oldProgress.isIndeterminate).isFalse() + } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractContent_fromOldProgressIndeterminate() { - val entry = createEntry { - setProgress(TEST_PROGRESS_MAX, TEST_PROGRESS, /* indeterminate= */ true) - } + fun extractContent_fromOldProgressIndeterminate() = + kosmos.runTest { + val entry = createEntry { + setProgress(TEST_PROGRESS_MAX, TEST_PROGRESS, /* indeterminate= */ true) + } - val content = requireContent(entry) - val oldProgress = assertNotNull(content.privateVersion.oldProgress) + val content = requireContent(entry) + val oldProgress = assertNotNull(content.privateVersion.oldProgress) - assertThat(oldProgress.progress).isEqualTo(TEST_PROGRESS) - assertThat(oldProgress.max).isEqualTo(TEST_PROGRESS_MAX) - assertThat(oldProgress.isIndeterminate).isTrue() - } + assertThat(oldProgress.progress).isEqualTo(TEST_PROGRESS) + assertThat(oldProgress.max).isEqualTo(TEST_PROGRESS_MAX) + assertThat(oldProgress.isIndeterminate).isTrue() + } - private fun requireContent( + private fun Kosmos.requireContent( entry: NotificationEntry, redactionType: Int = REDACTION_TYPE_PUBLIC, ): PromotedNotificationContentModels = assertNotNull(extractContent(entry, redactionType)) - private fun extractContent( + private fun Kosmos.extractContent( entry: NotificationEntry, redactionType: Int = REDACTION_TYPE_PUBLIC, ): PromotedNotificationContentModels? { @@ -636,7 +664,7 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { return underTest.extractContent(entry, recoveredBuilder, redactionType, imageModelProvider) } - private fun createEntry( + private fun Kosmos.createEntry( promoted: Boolean = true, builderBlock: Notification.Builder.() -> Unit = {}, ): NotificationEntry { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationsInteractorTest.kt new file mode 100644 index 000000000000..915edc03952d --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationsInteractorTest.kt @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.promoted.domain.interactor + +import android.app.Notification +import android.content.applicationContext +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.dumpManager +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.buildPromotedOngoingEntry +import com.android.systemui.statusbar.notification.domain.interactor.renderNotificationListInteractor +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi +import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization +import com.android.systemui.statusbar.policy.domain.interactor.sensitiveNotificationProtectionInteractor +import com.android.systemui.statusbar.policy.mockSensitiveNotificationProtectionController +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) +@EnableChipsModernization +class AODPromotedNotificationsInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val Kosmos.underTest by Fixture { + AODPromotedNotificationInteractor( + promotedNotificationsInteractor = promotedNotificationsInteractor, + keyguardInteractor = keyguardInteractor, + sensitiveNotificationProtectionInteractor = sensitiveNotificationProtectionInteractor, + dumpManager = dumpManager, + ) + } + + @Before + fun setUp() { + kosmos.statusBarNotificationChipsInteractor.start() + } + + private fun Kosmos.buildPublicPrivatePromotedOngoing(): NotificationEntry = + buildPromotedOngoingEntry { + modifyNotification(applicationContext) + .setContentTitle("SENSITIVE") + .setPublicVersion( + Notification.Builder(applicationContext, "channel") + .setContentTitle("REDACTED") + .build() + ) + } + + @Test + fun content_sensitive_unlocked() = + kosmos.runTest { + // GIVEN a promoted entry + val ronEntry = buildPublicPrivatePromotedOngoing() + + setKeyguardLocked(false) + setScreenSharingProtectionActive(false) + + renderNotificationListInteractor.setRenderedList(listOf(ronEntry)) + + // THEN aod content is sensitive + val content by collectLastValue(underTest.content) + assertThat(content?.title).isEqualTo("SENSITIVE") + } + + @Test + fun content_sensitive_locked() = + kosmos.runTest { + // GIVEN a promoted entry + val ronEntry = buildPublicPrivatePromotedOngoing() + + setKeyguardLocked(true) + setScreenSharingProtectionActive(false) + + renderNotificationListInteractor.setRenderedList(listOf(ronEntry)) + + // THEN aod content is redacted + val content by collectLastValue(underTest.content) + assertThat(content).isNotNull() + assertThat(content!!.title).isEqualTo("REDACTED") + } + + @Test + fun content_sensitive_unlocked_screensharing() = + kosmos.runTest { + // GIVEN a promoted entry + val ronEntry = buildPublicPrivatePromotedOngoing() + + setKeyguardLocked(false) + setScreenSharingProtectionActive(true) + + renderNotificationListInteractor.setRenderedList(listOf(ronEntry)) + + // THEN aod content is redacted + val content by collectLastValue(underTest.content) + assertThat(content).isNotNull() + assertThat(content!!.title).isEqualTo("REDACTED") + } + + private fun Kosmos.setKeyguardLocked(locked: Boolean) { + fakeKeyguardRepository.setKeyguardDismissible(!locked) + } + + private fun Kosmos.setScreenSharingProtectionActive(active: Boolean) { + whenever(mockSensitiveNotificationProtectionController.isSensitiveStateActive) + .thenReturn(active) + whenever(mockSensitiveNotificationProtectionController.shouldProtectNotification(any())) + .thenReturn(active) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java index fd49f60e7ae1..bbff9cf2f616 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java @@ -40,9 +40,12 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.shade.ShadeController; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.headsup.PinnedStatus; import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger; import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import org.junit.Before; import org.junit.Test; @@ -99,7 +102,13 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase { mRow.doDragCallback(0, 0); verify(controller).startDragAndDrop(mRow); verify(mHeadsUpManager, times(1)).releaseAllImmediately(); - verify(mNotificationPanelLogger, times(1)).logNotificationDrag(any()); + if (NotificationBundleUi.isEnabled()) { + verify(mNotificationPanelLogger, times(1)) + .logNotificationDrag(any(EntryAdapter.class)); + } else { + verify(mNotificationPanelLogger, times(1)) + .logNotificationDrag(any(NotificationEntry.class)); + } } @Test @@ -111,7 +120,13 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase { verify(controller).startDragAndDrop(mRow); verify(mShadeController).animateCollapseShade(eq(0), eq(true), eq(false), anyFloat()); - verify(mNotificationPanelLogger, times(1)).logNotificationDrag(any()); + if (NotificationBundleUi.isEnabled()) { + verify(mNotificationPanelLogger, times(1)) + .logNotificationDrag(any(EntryAdapter.class)); + } else { + verify(mNotificationPanelLogger, times(1)) + .logNotificationDrag(any(NotificationEntry.class)); + } } @Test @@ -129,8 +144,13 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase { // Verify that we never start the actual drag since there is no content verify(mRow, never()).startDragAndDrop(any(), any(), any(), anyInt()); - verify(mNotificationPanelLogger, never()).logNotificationDrag(any()); - } + if (NotificationBundleUi.isEnabled()) { + verify(mNotificationPanelLogger, never()) + .logNotificationDrag(any(EntryAdapter.class)); + } else { + verify(mNotificationPanelLogger, never()) + .logNotificationDrag(any(NotificationEntry.class)); + } } private ExpandableNotificationRowDragController createSpyController() { return spy(mController); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java index 19b1046f1931..4aa21a68b2e0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java @@ -399,35 +399,6 @@ public class NotificationContentInflaterTest extends SysuiTestCase { } @Test - @EnableFlags(PromotedNotificationUi.FLAG_NAME) - @DisableFlags(StatusBarNotifChips.FLAG_NAME) - public void testExtractsPromotedContent_whePromotedNotificationUiFlagEnabled() - throws Exception { - final PromotedNotificationContentModels content = - new PromotedNotificationContentBuilder("key").build(); - mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content); - - inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); - - mPromotedNotificationContentExtractor.verifyOneExtractCall(); - assertEquals(content, mRow.getEntry().getPromotedNotificationContentModels()); - } - - @Test - @EnableFlags(StatusBarNotifChips.FLAG_NAME) - @DisableFlags(PromotedNotificationUi.FLAG_NAME) - public void testExtractsPromotedContent_whenStatusBarNotifChipsFlagEnabled() throws Exception { - final PromotedNotificationContentModels content = - new PromotedNotificationContentBuilder("key").build(); - mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content); - - inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); - - mPromotedNotificationContentExtractor.verifyOneExtractCall(); - assertEquals(content, mRow.getEntry().getPromotedNotificationContentModels()); - } - - @Test @EnableFlags({PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME}) public void testExtractsPromotedContent_whenBothFlagsEnabled() throws Exception { final PromotedNotificationContentModels content = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt index dcba3e447dda..21b0c9013b5f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt @@ -465,32 +465,6 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { } @Test - @EnableFlags(PromotedNotificationUi.FLAG_NAME) - @DisableFlags(StatusBarNotifChips.FLAG_NAME) - fun testExtractsPromotedContent_whenPromotedNotificationUiFlagEnabled() { - val content = PromotedNotificationContentBuilder("key").build() - promotedNotificationContentExtractor.resetForEntry(row.entry, content) - - inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row) - - promotedNotificationContentExtractor.verifyOneExtractCall() - Assert.assertEquals(content, row.entry.promotedNotificationContentModels) - } - - @Test - @EnableFlags(StatusBarNotifChips.FLAG_NAME) - @DisableFlags(PromotedNotificationUi.FLAG_NAME) - fun testExtractsPromotedContent_whenStatusBarNotifChipsFlagEnabled() { - val content = PromotedNotificationContentBuilder("key").build() - promotedNotificationContentExtractor.resetForEntry(row.entry, content) - - inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row) - - promotedNotificationContentExtractor.verifyOneExtractCall() - Assert.assertEquals(content, row.entry.promotedNotificationContentModels) - } - - @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) fun testExtractsPromotedContent_whenBothFlagsEnabled() { val content = PromotedNotificationContentBuilder("key").build() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java index 761ed6186afc..ca4dc0e5e546 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.stack; +import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL; + import static org.junit.Assert.assertNull; import android.app.Notification; @@ -64,6 +66,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { mContext, mDependency, TestableLooper.get(this)); + mNotificationTestHelper.setDefaultInflationFlags(FLAG_CONTENT_VIEW_ALL); mGroup = mNotificationTestHelper.createGroup(); mChildrenContainer = mGroup.getChildrenContainer(); } @@ -172,9 +175,12 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { @Test @EnableFlags(AsyncGroupHeaderViewInflation.FLAG_NAME) public void testSetLowPriorityWithAsyncInflation_noHeaderReInflation() { + mChildrenContainer.setLowPriorityGroupHeader(null, null); mChildrenContainer.setIsMinimized(true); + + // THEN assertNull("We don't inflate header from the main thread with Async " - + "Inflation enabled", mChildrenContainer.getCurrentHeaderView()); + + "Inflation enabled", mChildrenContainer.getMinimizedNotificationHeader()); } @Test @@ -182,7 +188,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { public void setLowPriorityBeforeLowPriorityHeaderSet() { //Given: the children container does not have a low-priority header, and is not low-priority - assertNull(mChildrenContainer.getMinimizedGroupHeaderWrapper()); + mChildrenContainer.setLowPriorityGroupHeader(null, null); mGroup.setIsMinimized(false); //When: set the children container to be low-priority and set the low-priority header @@ -214,8 +220,8 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { public void changeLowPriorityAfterHeaderSet() { //Given: the children container does not have headers, and is not low-priority - assertNull(mChildrenContainer.getMinimizedGroupHeaderWrapper()); - assertNull(mChildrenContainer.getNotificationHeaderWrapper()); + mChildrenContainer.setLowPriorityGroupHeader(null, null); + mChildrenContainer.setGroupHeader(null, null); mGroup.setIsMinimized(false); //When: set the set the normal header diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt index f7bbf989ad3f..e03dbf54e101 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt @@ -32,7 +32,7 @@ import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.notification.collection.EntryAdapter import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController @@ -155,7 +155,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { } @Test - @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + @EnableFlags(PromotedNotificationUi.FLAG_NAME) fun maxKeyguardNotificationsForPromotedOngoing_onLockscreenSpaceForMinHeightButNotIntrinsicHeight_returnsOne() { setGapHeight(0f) // No divider height since we're testing one element where index = 0 @@ -283,7 +283,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { } @Test - @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + @EnableFlags(PromotedNotificationUi.FLAG_NAME) fun getSpaceNeeded_onLockscreenEnoughSpacePromotedOngoing_intrinsicHeight() { setGapHeight(0f) // No divider height since we're testing one element where index = 0 @@ -342,7 +342,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { } @Test - @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + @EnableFlags(PromotedNotificationUi.FLAG_NAME) fun getSpaceNeeded_onLockscreenSavingSpacePromotedOngoing_minHeight() { setGapHeight(0f) // No divider height since we're testing one element where index = 0 diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java index 716353945be2..999a78af0c68 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.phone; +import static android.service.dreams.Flags.FLAG_DREAMS_V2; + import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_NONE; import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK; @@ -38,6 +40,7 @@ import android.hardware.fingerprint.FingerprintManager; import android.os.Handler; import android.os.PowerManager; import android.os.UserHandle; +import android.platform.test.annotations.EnableFlags; import android.testing.TestableLooper.RunWithLooper; import android.testing.TestableResources; import android.view.ViewRootImpl; @@ -337,6 +340,38 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { } @Test + public void onBiometricAuthenticated_whenFaceAndDreaming_dontDismissKeyguard() { + when(mUpdateMonitor.isDeviceInteractive()).thenReturn(true); + when(mUpdateMonitor.isDreaming()).thenReturn(true); + when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); + // the value of isStrongBiometric doesn't matter here since we only care about the returned + // value of isUnlockingWithBiometricAllowed() + mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT, + BiometricSourceType.FACE, true /* isStrongBiometric */); + + verify(mStatusBarKeyguardViewManager, never()).notifyKeyguardAuthenticated(anyBoolean()); + assertThat(mBiometricUnlockController.getMode()) + .isEqualTo(BiometricUnlockController.MODE_ONLY_WAKE); + } + + @Test + @EnableFlags(FLAG_DREAMS_V2) + public void onBiometricAuthenticated_whenFaceOnBouncerAndDreaming_dismissKeyguard() { + when(mUpdateMonitor.isDeviceInteractive()).thenReturn(true); + when(mUpdateMonitor.isDreaming()).thenReturn(true); + when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); + when(mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()).thenReturn(true); + // the value of isStrongBiometric doesn't matter here since we only care about the returned + // value of isUnlockingWithBiometricAllowed() + mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT, + BiometricSourceType.FACE, true /* isStrongBiometric */); + + verify(mStatusBarKeyguardViewManager, never()).notifyKeyguardAuthenticated(anyBoolean()); + assertThat(mBiometricUnlockController.getMode()) + .isEqualTo(BiometricUnlockController.MODE_WAKE_AND_UNLOCK_FROM_DREAM); + } + + @Test public void onBiometricAuthenticated_onLockScreen() { // GIVEN not dozing when(mUpdateMonitor.isDeviceInteractive()).thenReturn(true); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/NotificationGroupTestHelper.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/NotificationGroupTestHelper.java index 19e98387a120..533b7a6a6acf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/NotificationGroupTestHelper.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/NotificationGroupTestHelper.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.phone; +import static android.app.NotificationManager.IMPORTANCE_LOW; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; @@ -23,6 +24,7 @@ import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.app.Notification; +import android.app.NotificationChannel; import android.content.Context; import android.os.UserHandle; import android.service.notification.StatusBarNotification; @@ -85,6 +87,33 @@ public final class NotificationGroupTestHelper { return entry; } + public NotificationEntry createClassifiedEntry(boolean isSummary, + int groupAlertBehavior, String channelId) { + + Notification notif = new Notification.Builder(mContext, TEST_CHANNEL_ID) + .setContentTitle("Title") + .setSmallIcon(R.drawable.ic_person) + .setGroupAlertBehavior(groupAlertBehavior) + .setGroupSummary(isSummary) + .setGroup(TEST_GROUP_ID) + .build(); + + NotificationChannel channel = new NotificationChannel(channelId, channelId, IMPORTANCE_LOW); + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setId(mId++) + .setNotification(notif) + .updateRanking((rankingBuilder -> rankingBuilder.setChannel(channel))) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .build(); + + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + entry.setRow(row); + when(row.getEntryLegacy()).thenReturn(entry); + return entry; + } + public NotificationEntry createEntry(int id, String tag, boolean isSummary, int groupAlertBehavior) { Notification notif = new Notification.Builder(mContext, TEST_CHANNEL_ID) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapterTest.kt index e72d0c27e632..8aff622ee772 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapterTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapterTest.kt @@ -97,7 +97,7 @@ class MobileIconInteractorKairosAdapterTest : MobileIconInteractorTestBase() { } .asIncremental() .applyLatestSpecForKey(), - isStackable = interactor.isStackable.toState(), + isStackable = interactor.isStackable.toState(false), activeDataConnectionHasDataEnabled = interactor.activeDataConnectionHasDataEnabled.toState(), activeDataIconInteractor = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt index 3d37914b1a7d..7dbcb270190c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt @@ -60,7 +60,6 @@ class MobileIconsViewModelTest : SysuiTestCase() { private val interactor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) private lateinit var airplaneModeInteractor: AirplaneModeInteractor - @Mock private lateinit var constants: ConnectivityConstants @Mock private lateinit var logger: MobileViewLogger @Mock private lateinit var verboseLogger: VerboseMobileViewLogger @@ -84,7 +83,10 @@ class MobileIconsViewModelTest : SysuiTestCase() { verboseLogger, interactor, airplaneModeInteractor, - constants, + object : ConnectivityConstants { + override val hasDataCapabilities = true + override val shouldShowActivityConfig = false + }, testScope.backgroundScope, ) @@ -349,7 +351,42 @@ class MobileIconsViewModelTest : SysuiTestCase() { // WHEN sub2 becomes last and sub2 has a network type icon interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) - // THEN the flow updates + assertThat(latest).isTrue() + job.cancel() + } + + @Test + fun isStackable_apmEnabled_false() = + testScope.runTest { + var latest: Boolean? = null + val job = underTest.isStackable.onEach { latest = it }.launchIn(this) + + // Set the interactor to true to test APM + interactor.isStackable.value = true + + // Enable APM + airplaneModeInteractor.setIsAirplaneMode(true) + + interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + + assertThat(latest).isFalse() + job.cancel() + } + + @Test + fun isStackable_apmDisabled_true() = + testScope.runTest { + var latest: Boolean? = null + val job = underTest.isStackable.onEach { latest = it }.launchIn(this) + + // Set the interactor to true to test APM + interactor.isStackable.value = true + + // Disable APM + airplaneModeInteractor.setIsAirplaneMode(false) + + interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + assertThat(latest).isTrue() job.cancel() diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockAnimations.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockAnimations.kt index 77c18eac289c..894327690b5e 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockAnimations.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockAnimations.kt @@ -61,4 +61,7 @@ interface ClockAnimations { /** Runs when an animation when the view is tapped on the lockscreen */ fun onFidgetTap(x: Float, y: Float) + + /** Update reactive axes for this clock */ + fun onFontAxesChanged(style: ClockAxisStyle) } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockEvents.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockEvents.kt index 0fc3470716fe..7c0e4866b00d 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockEvents.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockEvents.kt @@ -42,7 +42,4 @@ interface ClockEvents { /** Call with zen/dnd information */ fun onZenDataChanged(data: ZenData) - - /** Update reactive axes for this clock */ - fun onFontAxesChanged(axes: ClockAxisStyle) } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt index 5b67edda73cc..4f04aaa33b24 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt @@ -90,10 +90,11 @@ class ClockLogger(private val view: View?, buffer: MessageBuffer, tag: String) : } } - fun updateAxes(lsFVar: String, aodFVar: String) { - i({ "updateAxes(LS = $str1, AOD = $str2)" }) { + fun updateAxes(lsFVar: String, aodFVar: String, isAnimated: Boolean) { + i({ "updateAxes(LS = $str1, AOD = $str2, isAnimated=$bool1)" }) { str1 = lsFVar str2 = aodFVar + bool1 = isAnimated } } diff --git a/packages/SystemUI/res-keyguard/values/styles.xml b/packages/SystemUI/res-keyguard/values/styles.xml index e7d6b2fe08f4..877360594fc6 100644 --- a/packages/SystemUI/res-keyguard/values/styles.xml +++ b/packages/SystemUI/res-keyguard/values/styles.xml @@ -137,7 +137,13 @@ <item name="android:gravity">start</item> <item name="android:ellipsize">end</item> <item name="android:maxLines">2</item> - <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item> + <item name="android:fontFamily" android:featureFlag="!com.android.systemui.lockscreen_font"> + @*android:string/config_headlineFontFamily + </item> + <item name="android:fontFamily" android:featureFlag="com.android.systemui.lockscreen_font"> + variable-title-small + </item> + <item name="android:fontFamily"></item> <item name="android:shadowColor">@color/keyguard_shadow_color</item> <item name="android:shadowRadius">?attr/shadowRadius</item> </style> diff --git a/packages/SystemUI/res/drawable/magic_action_button_background.xml b/packages/SystemUI/res/drawable/animated_action_button_background.xml index 7199b2dfbe5a..1cecec535ad9 100644 --- a/packages/SystemUI/res/drawable/magic_action_button_background.xml +++ b/packages/SystemUI/res/drawable/animated_action_button_background.xml @@ -10,10 +10,10 @@ android:insetRight="0dp" android:insetTop="8dp"> <shape android:shape="rectangle"> - <corners android:radius="@dimen/magic_action_button_corner_radius" /> + <corners android:radius="@dimen/animated_action_button_corner_radius" /> <solid android:color="@androidprv:color/materialColorPrimaryContainer" /> <stroke - android:width="@dimen/magic_action_button_outline_stroke_width" + android:width="@dimen/animated_action_button_outline_stroke_width" android:color="@androidprv:color/materialColorOutlineVariant" /> </shape> </inset> diff --git a/packages/SystemUI/res/drawable/ic_add_circle_rounded.xml b/packages/SystemUI/res/drawable/ic_add_circle_rounded.xml new file mode 100644 index 000000000000..467a3813e461 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_add_circle_rounded.xml @@ -0,0 +1,27 @@ +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <group> + <path + android:pathData="M11,13V16C11,16.283 11.092,16.525 11.275,16.725C11.475,16.908 11.717,17 12,17C12.283,17 12.517,16.908 12.7,16.725C12.9,16.525 13,16.283 13,16V13H16C16.283,13 16.517,12.908 16.7,12.725C16.9,12.525 17,12.283 17,12C17,11.717 16.9,11.483 16.7,11.3C16.517,11.1 16.283,11 16,11H13V8C13,7.717 12.9,7.483 12.7,7.3C12.517,7.1 12.283,7 12,7C11.717,7 11.475,7.1 11.275,7.3C11.092,7.483 11,7.717 11,8V11H8C7.717,11 7.475,11.1 7.275,11.3C7.092,11.483 7,11.717 7,12C7,12.283 7.092,12.525 7.275,12.725C7.475,12.908 7.717,13 8,13H11ZM12,22C10.617,22 9.317,21.742 8.1,21.225C6.883,20.692 5.825,19.975 4.925,19.075C4.025,18.175 3.308,17.117 2.775,15.9C2.258,14.683 2,13.383 2,12C2,10.617 2.258,9.317 2.775,8.1C3.308,6.883 4.025,5.825 4.925,4.925C5.825,4.025 6.883,3.317 8.1,2.8C9.317,2.267 10.617,2 12,2C13.383,2 14.683,2.267 15.9,2.8C17.117,3.317 18.175,4.025 19.075,4.925C19.975,5.825 20.683,6.883 21.2,8.1C21.733,9.317 22,10.617 22,12C22,13.383 21.733,14.683 21.2,15.9C20.683,17.117 19.975,18.175 19.075,19.075C18.175,19.975 17.117,20.692 15.9,21.225C14.683,21.742 13.383,22 12,22ZM12,20C14.233,20 16.125,19.225 17.675,17.675C19.225,16.125 20,14.233 20,12C20,9.767 19.225,7.875 17.675,6.325C16.125,4.775 14.233,4 12,4C9.767,4 7.875,4.775 6.325,6.325C4.775,7.875 4,9.767 4,12C4,14.233 4.775,16.125 6.325,17.675C7.875,19.225 9.767,20 12,20Z" + android:fillColor="#ffffff"/> + </group> +</vector> diff --git a/packages/SystemUI/res/drawable/ic_check_circle_filled.xml b/packages/SystemUI/res/drawable/ic_check_circle_filled.xml new file mode 100644 index 000000000000..935733c3333d --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_check_circle_filled.xml @@ -0,0 +1,27 @@ +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <group> + <path + android:pathData="M10.6,13.8L8.45,11.65C8.267,11.467 8.033,11.375 7.75,11.375C7.467,11.375 7.233,11.467 7.05,11.65C6.867,11.833 6.775,12.067 6.775,12.35C6.775,12.633 6.867,12.867 7.05,13.05L9.9,15.9C10.1,16.1 10.333,16.2 10.6,16.2C10.867,16.2 11.1,16.1 11.3,15.9L16.95,10.25C17.133,10.067 17.225,9.833 17.225,9.55C17.225,9.267 17.133,9.033 16.95,8.85C16.767,8.667 16.533,8.575 16.25,8.575C15.967,8.575 15.733,8.667 15.55,8.85L10.6,13.8ZM12,22C10.617,22 9.317,21.742 8.1,21.225C6.883,20.692 5.825,19.975 4.925,19.075C4.025,18.175 3.308,17.117 2.775,15.9C2.258,14.683 2,13.383 2,12C2,10.617 2.258,9.317 2.775,8.1C3.308,6.883 4.025,5.825 4.925,4.925C5.825,4.025 6.883,3.317 8.1,2.8C9.317,2.267 10.617,2 12,2C13.383,2 14.683,2.267 15.9,2.8C17.117,3.317 18.175,4.025 19.075,4.925C19.975,5.825 20.683,6.883 21.2,8.1C21.733,9.317 22,10.617 22,12C22,13.383 21.733,14.683 21.2,15.9C20.683,17.117 19.975,18.175 19.075,19.075C18.175,19.975 17.117,20.692 15.9,21.225C14.683,21.742 13.383,22 12,22Z" + android:fillColor="#ffffff"/> + </group> +</vector> diff --git a/packages/SystemUI/res/drawable/ic_expand_less_rounded.xml b/packages/SystemUI/res/drawable/ic_expand_less_rounded.xml new file mode 100644 index 000000000000..5570fcfdab28 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_expand_less_rounded.xml @@ -0,0 +1,25 @@ +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="16dp" + android:height="16dp" + android:viewportWidth="16" + android:viewportHeight="16"> + <path + android:pathData="M8,5.25C8.097,5.25 8.194,5.264 8.292,5.292C8.375,5.333 8.451,5.389 8.521,5.458L12.479,9.417C12.632,9.569 12.708,9.743 12.708,9.938C12.694,10.146 12.611,10.326 12.458,10.479C12.306,10.632 12.132,10.708 11.938,10.708C11.729,10.708 11.549,10.632 11.396,10.479L8,7.063L4.583,10.479C4.431,10.632 4.257,10.701 4.063,10.688C3.854,10.688 3.674,10.611 3.521,10.458C3.368,10.306 3.292,10.125 3.292,9.917C3.292,9.722 3.368,9.549 3.521,9.396L7.479,5.458C7.549,5.389 7.632,5.333 7.729,5.292C7.813,5.264 7.903,5.25 8,5.25Z" + android:fillColor="#ffffffff"/> +</vector> diff --git a/packages/SystemUI/res/drawable/ic_expand_more_rounded.xml b/packages/SystemUI/res/drawable/ic_expand_more_rounded.xml new file mode 100644 index 000000000000..dec620e54995 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_expand_more_rounded.xml @@ -0,0 +1,25 @@ +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="16dp" + android:height="16dp" + android:viewportHeight="16" + android:viewportWidth="16"> + <path + android:pathData="M8,10.75C7.903,10.75 7.806,10.736 7.708,10.708C7.625,10.667 7.549,10.611 7.479,10.542L3.521,6.583C3.368,6.431 3.292,6.257 3.292,6.063C3.306,5.854 3.389,5.674 3.542,5.521C3.694,5.368 3.868,5.292 4.063,5.292C4.271,5.292 4.451,5.368 4.604,5.521L8,8.938L11.417,5.521C11.569,5.368 11.743,5.299 11.938,5.313C12.146,5.313 12.326,5.389 12.479,5.542C12.632,5.694 12.708,5.875 12.708,6.083C12.708,6.278 12.632,6.451 12.479,6.604L8.521,10.542C8.451,10.611 8.368,10.667 8.271,10.708C8.188,10.736 8.097,10.75 8,10.75Z" + android:fillColor="#ffffffff"/> +</vector> diff --git a/packages/SystemUI/res/drawable/ic_qs_category_accessibility.xml b/packages/SystemUI/res/drawable/ic_qs_category_accessibility.xml new file mode 100644 index 000000000000..bc62d38f1932 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_category_accessibility.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:tint="?attr/colorControlNormal" + android:viewportHeight="960" + android:viewportWidth="960"> + <path + android:fillColor="@android:color/white" + android:pathData="M480,240Q447,240 423.5,216.5Q400,193 400,160Q400,127 423.5,103.5Q447,80 480,80Q513,80 536.5,103.5Q560,127 560,160Q560,193 536.5,216.5Q513,240 480,240ZM360,840L360,360Q311,356 261,349Q211,342 163,331Q146,327 135.5,312Q125,297 130,280Q135,263 151,255Q167,247 185,251Q255,266 330.5,273Q406,280 480,280Q554,280 629.5,273Q705,266 775,251Q793,247 809,255Q825,263 830,280Q835,297 824.5,312Q814,327 797,331Q749,342 699,349Q649,356 600,360L600,840Q600,857 588.5,868.5Q577,880 560,880Q543,880 531.5,868.5Q520,857 520,840L520,640L440,640L440,840Q440,857 428.5,868.5Q417,880 400,880Q383,880 371.5,868.5Q360,857 360,840Z" /> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_qs_category_connectivty.xml b/packages/SystemUI/res/drawable/ic_qs_category_connectivty.xml new file mode 100644 index 000000000000..91644873c064 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_category_connectivty.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:tint="?attr/colorControlNormal" + android:viewportHeight="960" + android:viewportWidth="960"> + <path + android:fillColor="@android:color/white" + android:pathData="M480,840Q438,840 409,811Q380,782 380,740Q380,698 409,669Q438,640 480,640Q522,640 551,669Q580,698 580,740Q580,782 551,811Q522,840 480,840ZM480,400Q555,400 622.5,424Q690,448 745,490Q765,505 765.5,529.5Q766,554 748,572Q731,589 706,589.5Q681,590 661,576Q623,550 577,535Q531,520 480,520Q429,520 383,535Q337,550 299,576Q279,590 254,589Q229,588 212,571Q195,553 195,528.5Q195,504 215,489Q270,447 337.5,423.5Q405,400 480,400ZM480,160Q605,160 715.5,201Q826,242 914,317Q934,334 935,359Q936,384 918,402Q901,419 876,419.5Q851,420 831,404Q759,345 669.5,312.5Q580,280 480,280Q380,280 290.5,312.5Q201,345 129,404Q109,420 84,419.5Q59,419 42,402Q24,384 25,359Q26,334 46,317Q134,242 244.5,201Q355,160 480,160Z" /> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_qs_category_display.xml b/packages/SystemUI/res/drawable/ic_qs_category_display.xml new file mode 100644 index 000000000000..c238e940eb01 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_category_display.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:tint="?attr/colorControlNormal" + android:viewportHeight="960" + android:viewportWidth="960"> + <path + android:fillColor="@android:color/white" + android:pathData="M346,800L240,800Q207,800 183.5,776.5Q160,753 160,720L160,614L83,536Q72,524 66,509.5Q60,495 60,480Q60,465 66,450.5Q72,436 83,424L160,346L160,240Q160,207 183.5,183.5Q207,160 240,160L346,160L424,83Q436,72 450.5,66Q465,60 480,60Q495,60 509.5,66Q524,72 536,83L614,160L720,160Q753,160 776.5,183.5Q800,207 800,240L800,346L877,424Q888,436 894,450.5Q900,465 900,480Q900,495 894,509.5Q888,524 877,536L800,614L800,720Q800,753 776.5,776.5Q753,800 720,800L614,800L536,877Q524,888 509.5,894Q495,900 480,900Q465,900 450.5,894Q436,888 424,877L346,800ZM380,720L480,820Q480,820 480,820Q480,820 480,820L580,720L720,720Q720,720 720,720Q720,720 720,720L720,580L820,480Q820,480 820,480Q820,480 820,480L720,380L720,240Q720,240 720,240Q720,240 720,240L580,240L480,140Q480,140 480,140Q480,140 480,140L380,240L240,240Q240,240 240,240Q240,240 240,240L240,380L140,480Q140,480 140,480Q140,480 140,480L240,580L240,720Q240,720 240,720Q240,720 240,720L380,720ZM480,680Q563,680 621.5,621.5Q680,563 680,480Q680,397 621.5,338.5Q563,280 480,280L480,680Z" /> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_qs_category_privacy.xml b/packages/SystemUI/res/drawable/ic_qs_category_privacy.xml new file mode 100644 index 000000000000..915cf41ba1f6 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_category_privacy.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:tint="?attr/colorControlNormal" + android:viewportHeight="960" + android:viewportWidth="960"> + <path + android:fillColor="@android:color/white" + android:pathData="M444,600L516,600Q525,600 531.5,592.5Q538,585 536,576L517,471Q537,461 548.5,442Q560,423 560,400Q560,367 536.5,343.5Q513,320 480,320Q447,320 423.5,343.5Q400,367 400,400Q400,423 411.5,442Q423,461 443,471L424,576Q422,585 428.5,592.5Q435,600 444,600ZM480,876Q473,876 467,875Q461,874 455,872Q320,827 240,705.5Q160,584 160,444L160,255Q160,230 174.5,210Q189,190 212,181L452,91Q466,86 480,86Q494,86 508,91L748,181Q771,190 785.5,210Q800,230 800,255L800,444Q800,584 720,705.5Q640,827 505,872Q499,874 493,875Q487,876 480,876Z" /> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_qs_category_provided_by_apps.xml b/packages/SystemUI/res/drawable/ic_qs_category_provided_by_apps.xml new file mode 100644 index 000000000000..cea43ae1bc2f --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_category_provided_by_apps.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:tint="?attr/colorControlNormal" + android:viewportHeight="960" + android:viewportWidth="960"> + <path + android:fillColor="@android:color/white" + android:pathData="M240,800Q207,800 183.5,776.5Q160,753 160,720Q160,687 183.5,663.5Q207,640 240,640Q273,640 296.5,663.5Q320,687 320,720Q320,753 296.5,776.5Q273,800 240,800ZM480,800Q447,800 423.5,776.5Q400,753 400,720Q400,687 423.5,663.5Q447,640 480,640Q513,640 536.5,663.5Q560,687 560,720Q560,753 536.5,776.5Q513,800 480,800ZM720,800Q687,800 663.5,776.5Q640,753 640,720Q640,687 663.5,663.5Q687,640 720,640Q753,640 776.5,663.5Q800,687 800,720Q800,753 776.5,776.5Q753,800 720,800ZM240,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,560ZM240,320Q207,320 183.5,296.5Q160,273 160,240Q160,207 183.5,183.5Q207,160 240,160Q273,160 296.5,183.5Q320,207 320,240Q320,273 296.5,296.5Q273,320 240,320ZM480,320Q447,320 423.5,296.5Q400,273 400,240Q400,207 423.5,183.5Q447,160 480,160Q513,160 536.5,183.5Q560,207 560,240Q560,273 536.5,296.5Q513,320 480,320ZM720,320Q687,320 663.5,296.5Q640,273 640,240Q640,207 663.5,183.5Q687,160 720,160Q753,160 776.5,183.5Q800,207 800,240Q800,273 776.5,296.5Q753,320 720,320Z" /> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_qs_category_unknown.xml b/packages/SystemUI/res/drawable/ic_qs_category_unknown.xml new file mode 100644 index 000000000000..ec2ce15e2d01 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_category_unknown.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:tint="?attr/colorControlNormal" + android:viewportHeight="960" + android:viewportWidth="960"> + <path + android:fillColor="@android:color/white" + android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,640Q120,623 131.5,611.5Q143,600 160,600Q177,600 188.5,611.5Q200,623 200,640L200,760Q200,760 200,760Q200,760 200,760L320,760Q337,760 348.5,771.5Q360,783 360,800Q360,817 348.5,828.5Q337,840 320,840L200,840ZM760,840L640,840Q623,840 611.5,828.5Q600,817 600,800Q600,783 611.5,771.5Q623,760 640,760L760,760Q760,760 760,760Q760,760 760,760L760,640Q760,623 771.5,611.5Q783,600 800,600Q817,600 828.5,611.5Q840,623 840,640L840,760Q840,793 816.5,816.5Q793,840 760,840ZM120,200Q120,167 143.5,143.5Q167,120 200,120L320,120Q337,120 348.5,131.5Q360,143 360,160Q360,177 348.5,188.5Q337,200 320,200L200,200Q200,200 200,200Q200,200 200,200L200,320Q200,337 188.5,348.5Q177,360 160,360Q143,360 131.5,348.5Q120,337 120,320L120,200ZM840,200L840,320Q840,337 828.5,348.5Q817,360 800,360Q783,360 771.5,348.5Q760,337 760,320L760,200Q760,200 760,200Q760,200 760,200L640,200Q623,200 611.5,188.5Q600,177 600,160Q600,143 611.5,131.5Q623,120 640,120L760,120Q793,120 816.5,143.5Q840,167 840,200ZM480,720Q501,720 515.5,705.5Q530,691 530,670Q530,649 515.5,634.5Q501,620 480,620Q459,620 444.5,634.5Q430,649 430,670Q430,691 444.5,705.5Q459,720 480,720ZM480,308Q506,308 525.5,324Q545,340 545,365Q545,388 530.5,406Q516,424 499,439Q473,462 459.5,482.5Q446,503 444,532Q443,546 454,556.5Q465,567 480,567Q494,567 505.5,557Q517,547 519,532Q521,515 531,502Q541,489 560,470Q595,435 606.5,413.5Q618,392 618,362Q618,308 579,274Q540,240 480,240Q439,240 406.5,258.5Q374,277 357,311Q351,323 356.5,335.5Q362,348 375,353Q388,358 401.5,353Q415,348 423,337Q434,323 448.5,315.5Q463,308 480,308Z" /> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_qs_category_utilities.xml b/packages/SystemUI/res/drawable/ic_qs_category_utilities.xml new file mode 100644 index 000000000000..4dfac8393b8e --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_category_utilities.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:tint="?attr/colorControlNormal" + android:viewportHeight="960" + android:viewportWidth="960"> + <path + android:fillColor="@android:color/white" + android:pathData="M433,880Q406,880 386.5,862Q367,844 363,818L354,752Q341,747 329.5,740Q318,733 307,725L245,751Q220,762 195,753Q170,744 156,721L109,639Q95,616 101,590Q107,564 128,547L181,507Q180,500 180,493.5Q180,487 180,480Q180,473 180,466.5Q180,460 181,453L128,413Q107,396 101,370Q95,344 109,321L156,239Q170,216 195,207Q220,198 245,209L307,235Q318,227 330,220Q342,213 354,208L363,142Q367,116 386.5,98Q406,80 433,80L527,80Q554,80 573.5,98Q593,116 597,142L606,208Q619,213 630.5,220Q642,227 653,235L715,209Q740,198 765,207Q790,216 804,239L851,321Q865,344 859,370Q853,396 832,413L779,453Q780,460 780,466.5Q780,473 780,480Q780,487 780,493.5Q780,500 778,507L831,547Q852,564 858,590Q864,616 850,639L802,721Q788,744 763,753Q738,762 713,751L653,725Q642,733 630,740Q618,747 606,752L597,818Q593,844 573.5,862Q554,880 527,880L433,880ZM482,620Q540,620 581,579Q622,538 622,480Q622,422 581,381Q540,340 482,340Q423,340 382.5,381Q342,422 342,480Q342,538 382.5,579Q423,620 482,620Z" /> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/media_output_dialog_background_reduced_radius.xml b/packages/SystemUI/res/drawable/media_output_dialog_background_reduced_radius.xml new file mode 100644 index 000000000000..f78212b44828 --- /dev/null +++ b/packages/SystemUI/res/drawable/media_output_dialog_background_reduced_radius.xml @@ -0,0 +1,20 @@ +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners android:radius="@dimen/media_output_dialog_corner_radius" /> + <solid android:color="@color/media_dialog_surface_container" /> +</shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/media_output_dialog_footer_background.xml b/packages/SystemUI/res/drawable/media_output_dialog_footer_background.xml new file mode 100644 index 000000000000..2d27ac1612a9 --- /dev/null +++ b/packages/SystemUI/res/drawable/media_output_dialog_footer_background.xml @@ -0,0 +1,22 @@ +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners + android:bottomLeftRadius="@dimen/media_output_dialog_corner_radius" + android:bottomRightRadius="@dimen/media_output_dialog_corner_radius" /> + <solid android:color="@color/media_dialog_surface_container" /> +</shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/media_output_dialog_item_fixed_volume_background.xml b/packages/SystemUI/res/drawable/media_output_dialog_item_fixed_volume_background.xml new file mode 100644 index 000000000000..38db2da37f7c --- /dev/null +++ b/packages/SystemUI/res/drawable/media_output_dialog_item_fixed_volume_background.xml @@ -0,0 +1,20 @@ +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners android:radius="20dp" /> + <solid android:color="@color/media_dialog_primary" /> +</shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/media_output_dialog_round_button_ripple.xml b/packages/SystemUI/res/drawable/media_output_dialog_round_button_ripple.xml new file mode 100644 index 000000000000..d23c1837c501 --- /dev/null +++ b/packages/SystemUI/res/drawable/media_output_dialog_round_button_ripple.xml @@ -0,0 +1,24 @@ +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<ripple android:color="?android:colorControlHighlight" + xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@android:id/mask"> + <shape android:shape="rectangle"> + <solid android:color="@android:color/white" /> + <corners android:radius="20dp" /> + </shape> + </item> +</ripple>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/media_output_item_expandable_button_background.xml b/packages/SystemUI/res/drawable/media_output_item_expandable_button_background.xml new file mode 100644 index 000000000000..8fc8744a3827 --- /dev/null +++ b/packages/SystemUI/res/drawable/media_output_item_expandable_button_background.xml @@ -0,0 +1,24 @@ +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners + android:radius="@dimen/media_output_item_expand_icon_height"/> + <size + android:width="@dimen/media_output_item_expand_icon_width" + android:height="@dimen/media_output_item_expand_icon_height" /> + <solid android:color="@color/media_dialog_on_surface" /> +</shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/notification_menu_button_background.xml b/packages/SystemUI/res/drawable/notification_menu_button_background.xml new file mode 100644 index 000000000000..a3014d9ea566 --- /dev/null +++ b/packages/SystemUI/res/drawable/notification_menu_button_background.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="#202124"/> + <corners android:radius="@dimen/notification_corner_radius"/> +</shape> diff --git a/packages/SystemUI/res/drawable/unpin_icon.xml b/packages/SystemUI/res/drawable/unpin_icon.xml index 4e2e15893884..979e8d440b78 100644 --- a/packages/SystemUI/res/drawable/unpin_icon.xml +++ b/packages/SystemUI/res/drawable/unpin_icon.xml @@ -1,10 +1,10 @@ <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" - android:viewportWidth="960" + android:tint="?attr/colorControlNormal" android:viewportHeight="960" - android:tint="?attr/colorControlNormal"> - <path - android:fillColor="@android:color/white" - android:pathData="M680,120L680,200L640,200L640,527L560,447L560,200L400,200L400,287L313,200L280,167L280,167L280,120L680,120ZM480,920L440,880L440,640L240,640L240,560L320,480L320,434L56,168L112,112L848,848L790,904L526,640L520,640L520,880L480,920ZM354,560L446,560L402,516L400,514L354,560ZM480,367L480,367L480,367L480,367ZM402,516L402,516L402,516L402,516Z"/> -</vector> + android:viewportWidth="960"> + <path + android:fillColor="@android:color/white" + android:pathData="M680,120L680,200L640,200L640,527L313,200L280,167L280,167L280,120L680,120ZM480,920L440,880L440,640L240,640L240,560L320,480L320,434L56,168L112,112L848,848L790,904L526,640L520,640L520,880L480,920Z" /> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/animated_action_button.xml b/packages/SystemUI/res/layout/animated_action_button.xml new file mode 100644 index 000000000000..3e5e35b815e1 --- /dev/null +++ b/packages/SystemUI/res/layout/animated_action_button.xml @@ -0,0 +1,17 @@ +<com.android.systemui.statusbar.notification.row.AnimatedActionButton + xmlns:android="http://schemas.android.com/apk/res/android" + style="@android:style/Widget.Material.Button" + android:layout_width="wrap_content" + android:layout_height="@dimen/animated_action_button_touch_target_height" + android:background="@drawable/animated_action_button_background" + android:drawablePadding="@dimen/animated_action_button_drawable_padding" + android:ellipsize="none" + android:fontFamily="google-sans-flex-medium" + android:gravity="center" + android:minWidth="0dp" + android:paddingHorizontal="@dimen/animated_action_button_padding_horizontal" + android:paddingVertical="@dimen/animated_action_button_inset_vertical" + android:stateListAnimator="@null" + android:textColor="@color/animated_action_button_text_color" + android:textSize="@dimen/animated_action_button_font_size" + android:textStyle="normal" /> diff --git a/packages/SystemUI/res/layout/battery_percentage_view.xml b/packages/SystemUI/res/layout/battery_percentage_view.xml index 8b5aeaac424a..8aa6930a09f9 100644 --- a/packages/SystemUI/res/layout/battery_percentage_view.xml +++ b/packages/SystemUI/res/layout/battery_percentage_view.xml @@ -22,7 +22,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:singleLine="true" - android:textAppearance="@style/TextAppearance.StatusBar.Clock" + android:textAppearance="@style/TextAppearance.StatusBar.Default" android:textColor="?android:attr/textColorPrimary" android:gravity="center_vertical|start" android:paddingStart="@dimen/battery_level_padding_start" diff --git a/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml index 4002f7808637..1e4a07f5fc30 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml @@ -22,7 +22,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:accessibilityLiveRegion="assertive" - android:importantForAccessibility="yes" + android:importantForAccessibility="auto" android:clickable="false" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/rightGuideline" diff --git a/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml index 3c8cb6860a41..8234c24a7e17 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml @@ -23,7 +23,7 @@ android:layout_height="match_parent"> android:layout_width="0dp" android:layout_height="0dp" android:accessibilityLiveRegion="assertive" - android:importantForAccessibility="yes" + android:importantForAccessibility="auto" android:clickable="false" android:paddingHorizontal="16dp" android:paddingVertical="16dp" diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml index 5c06585de99c..65d17042412b 100644 --- a/packages/SystemUI/res/layout/combined_qs_header.xml +++ b/packages/SystemUI/res/layout/combined_qs_header.xml @@ -85,7 +85,7 @@ frame when animating QS <-> QQS transition android:paddingEnd="@dimen/status_bar_left_clock_end_padding" android:singleLine="true" android:textDirection="locale" - android:textAppearance="@style/TextAppearance.QS.Status" + android:textAppearance="@style/TextAppearance.QS.Status.Clock" android:fontFeatureSettings="tnum" android:transformPivotX="0dp" android:transformPivotY="24dp" diff --git a/packages/SystemUI/res/layout/keyguard_status_bar.xml b/packages/SystemUI/res/layout/keyguard_status_bar.xml index d5f9d4cc0954..a81d90bd13fc 100644 --- a/packages/SystemUI/res/layout/keyguard_status_bar.xml +++ b/packages/SystemUI/res/layout/keyguard_status_bar.xml @@ -76,7 +76,7 @@ android:gravity="center_vertical" android:ellipsize="marquee" android:textDirection="locale" - android:textAppearance="@style/TextAppearance.StatusBar.Carrier" + android:textAppearance="@style/TextAppearance.StatusBar.Default" android:textColor="?attr/wallpaperTextColorSecondary" android:singleLine="true" systemui:showMissingSim="true" diff --git a/packages/SystemUI/res/layout/magic_action_button.xml b/packages/SystemUI/res/layout/magic_action_button.xml deleted file mode 100644 index 63fc1e485635..000000000000 --- a/packages/SystemUI/res/layout/magic_action_button.xml +++ /dev/null @@ -1,17 +0,0 @@ -<com.android.systemui.statusbar.notification.row.MagicActionButton - xmlns:android="http://schemas.android.com/apk/res/android" - style="@android:style/Widget.Material.Button" - android:layout_width="wrap_content" - android:layout_height="@dimen/magic_action_button_touch_target_height" - android:background="@drawable/magic_action_button_background" - android:drawablePadding="@dimen/magic_action_button_drawable_padding" - android:ellipsize="none" - android:fontFamily="google-sans-flex-medium" - android:gravity="center" - android:minWidth="0dp" - android:paddingHorizontal="@dimen/magic_action_button_padding_horizontal" - android:paddingVertical="@dimen/magic_action_button_inset_vertical" - android:stateListAnimator="@null" - android:textColor="@color/magic_action_button_text_color" - android:textSize="@dimen/magic_action_button_font_size" - android:textStyle="normal" /> diff --git a/packages/SystemUI/res/layout/media_output_dialog.xml b/packages/SystemUI/res/layout/media_output_dialog.xml index 9b629ace76af..15657284030d 100644 --- a/packages/SystemUI/res/layout/media_output_dialog.xml +++ b/packages/SystemUI/res/layout/media_output_dialog.xml @@ -97,6 +97,23 @@ </LinearLayout> </LinearLayout> + <LinearLayout + android:id="@+id/quick_access_shelf" + android:paddingHorizontal="@dimen/media_output_dialog_margin_horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:visibility="gone"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/connect_device" + app:icon="@drawable/ic_add" + style="@style/MediaOutput.Dialog.QuickAccessButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/media_output_dialog_button_connect_device" + android:layout_marginBottom="8dp"/> + </LinearLayout> + <ViewStub android:id="@+id/broadcast_qrcode" android:layout="@layout/media_output_broadcast_area" @@ -123,13 +140,15 @@ </androidx.constraintlayout.widget.ConstraintLayout> <LinearLayout + android:id="@+id/dialog_footer" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="4dp" - android:layout_marginStart="@dimen/dialog_side_padding" - android:layout_marginEnd="@dimen/dialog_side_padding" - android:layout_marginBottom="@dimen/dialog_bottom_padding" - android:orientation="horizontal"> + android:paddingTop="4dp" + android:paddingStart="@dimen/dialog_side_padding" + android:paddingEnd="@dimen/dialog_side_padding" + android:paddingBottom="@dimen/dialog_bottom_padding" + android:orientation="horizontal" + android:gravity="end"> <Button android:id="@+id/stop" @@ -140,6 +159,7 @@ android:visibility="gone"/> <Space + android:id="@+id/footer_spacer" android:layout_weight="1" android:layout_width="0dp" android:layout_height="match_parent"/> diff --git a/packages/SystemUI/res/layout/media_output_list_item_device.xml b/packages/SystemUI/res/layout/media_output_list_item_device.xml new file mode 100644 index 000000000000..29d5bfcc1743 --- /dev/null +++ b/packages/SystemUI/res/layout/media_output_list_item_device.xml @@ -0,0 +1,141 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/item_layout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:baselineAligned="false" + android:orientation="vertical"> + + <LinearLayout + android:id="@+id/main_content" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="start|center_vertical" + android:background="?android:attr/selectableItemBackground" + android:focusable="true" + android:orientation="horizontal" + android:layout_marginHorizontal="@dimen/media_output_dialog_margin_horizontal" + android:paddingVertical="@dimen/media_output_item_content_vertical_margin"> + + <ImageView + android:id="@+id/title_icon" + style="@style/MediaOutput.Item.Icon" + android:layout_marginEnd="@dimen/media_output_item_horizontal_gap" + android:importantForAccessibility="no" + tools:src="@drawable/ic_smartphone" + tools:visibility="visible"/> + + <LinearLayout + android:id="@+id/text_container" + android:layout_width="0dp" + android:layout_weight="1" + android:layout_height="wrap_content" + android:minHeight="@dimen/media_output_item_icon_size" + android:layout_gravity="start" + android:gravity="center_vertical|start" + android:orientation="vertical"> + + <TextView + android:id="@+id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:fontFamily="variable-title-small" + android:ellipsize="end" + android:maxLines="1" /> + + <TextView + android:id="@+id/subtitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:fontFamily="variable-title-small" + android:alpha="@dimen/media_output_item_subtitle_alpha" + android:maxLines="1" + android:singleLine="true" /> + </LinearLayout> + + <ImageView + android:id="@+id/status_icon" + style="@style/MediaOutput.Item.Icon" + android:layout_marginStart="@dimen/media_output_item_horizontal_gap" + android:importantForAccessibility="no" + android:visibility="gone" + app:tint="@color/media_dialog_on_surface_variant" + tools:src="@drawable/media_output_status_failed" + tools:visibility="visible" /> + + <ProgressBar + android:id="@+id/loading_indicator" + style="?android:attr/progressBarStyleSmallTitle" + android:layout_width="@dimen/media_output_item_icon_size" + android:layout_height="@dimen/media_output_item_icon_size" + android:padding="@dimen/media_output_item_icon_padding" + android:scaleType="fitCenter" + android:layout_marginStart="@dimen/media_output_item_horizontal_gap" + android:indeterminate="true" + android:indeterminateOnly="true" + android:visibility="gone" + tools:indeterminateTint="@color/media_dialog_on_surface_variant" + tools:visibility="visible" /> + + <View + android:id="@+id/divider" + android:layout_width="1dp" + android:layout_height="@dimen/media_output_item_icon_size" + android:layout_marginStart="@dimen/media_output_item_horizontal_gap" + android:background="@color/media_dialog_outline" + android:visibility="visible" + /> + + <ImageButton + android:id="@+id/ongoing_session_button" + style="@style/MediaOutput.Item.Icon" + android:src="@drawable/ic_sound_bars_anim" + android:background="?android:attr/selectableItemBackgroundBorderless" + android:focusable="true" + android:contentDescription="@string/accessibility_open_application" + android:layout_marginStart="@dimen/media_output_item_horizontal_gap" + android:visibility="gone" + tools:visibility="visible"/> + + <ImageButton + android:id="@+id/group_button" + style="@style/MediaOutput.Item.Icon" + android:layout_marginStart="@dimen/media_output_item_horizontal_gap" + android:src="@drawable/ic_add_circle_rounded" + android:background="@drawable/media_output_dialog_round_button_ripple" + android:focusable="true" + android:contentDescription="@null" + android:visibility="gone" + tools:visibility="visible"/> + </LinearLayout> + + <com.google.android.material.slider.Slider + android:id="@+id/volume_seekbar" + android:layout_width="match_parent" + android:layout_height="44dp" + android:layout_marginVertical="3dp" + android:theme="@style/Theme.Material3.DynamicColors.DayNight" + app:labelBehavior="gone" + app:tickVisible="false" + app:trackCornerSize="12dp" + app:trackHeight="32dp" + app:trackIconSize="20dp" + app:trackStopIndicatorSize="0dp" /> +</LinearLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/media_output_list_item_group_divider.xml b/packages/SystemUI/res/layout/media_output_list_item_group_divider.xml new file mode 100644 index 000000000000..f8c6c1f9f616 --- /dev/null +++ b/packages/SystemUI/res/layout/media_output_list_item_group_divider.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/media_output_dialog_margin_horizontal" + android:orientation="vertical"> + + <View + android:id="@+id/top_separator" + android:layout_width="match_parent" + android:layout_height="1dp" + android:layout_marginVertical="8dp" + android:background="@color/media_dialog_outline_variant" + android:visibility="gone" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="40dp" + android:orientation="horizontal" + android:gravity="center_vertical"> + + <TextView + android:id="@+id/title" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:accessibilityHeading="true" + android:ellipsize="end" + android:maxLines="1" + android:fontFamily="variable-label-large-emphasized" + android:gravity="center_vertical|start" /> + + <FrameLayout + android:id="@+id/expand_button" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_marginStart="@dimen/media_output_item_horizontal_gap" + android:contentDescription="@string/accessibility_open_application" + android:focusable="true" + android:visibility="gone"> + + <ImageView + android:id="@+id/expand_button_icon" + android:layout_width="@dimen/media_output_item_expand_icon_width" + android:layout_height="@dimen/media_output_item_expand_icon_height" + android:layout_gravity="center" + android:background="@drawable/media_output_item_expandable_button_background" + android:contentDescription="@null" + android:focusable="false" + android:scaleType="centerInside" /> + </FrameLayout> + </LinearLayout> +</LinearLayout> + diff --git a/packages/SystemUI/res/layout/notification_conversation_info.xml b/packages/SystemUI/res/layout/notification_conversation_info.xml index 9560be0d6969..56660139f823 100644 --- a/packages/SystemUI/res/layout/notification_conversation_info.xml +++ b/packages/SystemUI/res/layout/notification_conversation_info.xml @@ -391,6 +391,16 @@ android:paddingEnd="4dp" > <TextView + android:id="@+id/inline_dismiss" + android:text="@string/notification_inline_dismiss" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:gravity="center_vertical" + android:minWidth="@dimen/notification_importance_toggle_size" + android:minHeight="@dimen/notification_importance_toggle_size" + style="@style/TextAppearance.NotificationInfo.Button"/> + <TextView android:id="@+id/done" android:text="@string/inline_ok_button" android:layout_width="wrap_content" diff --git a/packages/SystemUI/res/layout/promoted_menu_item.xml b/packages/SystemUI/res/layout/promoted_menu_item.xml new file mode 100644 index 000000000000..ec52189585af --- /dev/null +++ b/packages/SystemUI/res/layout/promoted_menu_item.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:layout_width="wrap_content" + android:layout_height="match_parent"> + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_height="match_parent" + android:layout_width="@dimen/notification_menu_item_width" + android:background="@drawable/notification_menu_button_background" + android:backgroundTint="@androidprv:color/materialColorPrimaryContainer" + android:padding="@dimen/notification_menu_button_padding"> + <ImageView + android:id="@+id/promoted_menuitem_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:tint="@androidprv:color/materialColorPrimary" + android:src="@drawable/unpin_icon" /> + <TextView + android:id="@+id/promoted_menuitem_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/notification_inline_disable_promotion_button" + style="@style/TextAppearance.NotificationMenuButtonText"/> + <androidx.constraintlayout.helper.widget.Flow + android:id="@+id/flow3" + android:orientation="vertical" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:flow_verticalStyle="packed" + app:flow_horizontalAlign="center" + app:flow_verticalAlign="center" + app:constraint_referenced_ids="promoted_menuitem_icon,promoted_menuitem_text" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent"/> + </androidx.constraintlayout.widget.ConstraintLayout> +</FrameLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/promoted_permission_guts.xml b/packages/SystemUI/res/layout/promoted_permission_guts.xml index 50e5ae3c05ed..e962d3ad8679 100644 --- a/packages/SystemUI/res/layout/promoted_permission_guts.xml +++ b/packages/SystemUI/res/layout/promoted_permission_guts.xml @@ -54,7 +54,7 @@ android:textColor="@androidprv:color/materialColorOnSurface" android:minWidth="@dimen/min_clickable_item_size" android:minHeight="@dimen/min_clickable_item_size" - style="@style/TextAppearance.NotificationInfo.Button" /> + style="@style/TextAppearance.NotificationMenuButtonText" /> <TextView android:id="@+id/undo" diff --git a/packages/SystemUI/res/layout/status_bar.xml b/packages/SystemUI/res/layout/status_bar.xml index e4da4729ad0d..359a69ca1f94 100644 --- a/packages/SystemUI/res/layout/status_bar.xml +++ b/packages/SystemUI/res/layout/status_bar.xml @@ -96,7 +96,7 @@ android:layout_width="wrap_content" android:layout_height="@dimen/status_bar_system_icons_height" android:layout_gravity="center_vertical" - android:textAppearance="@style/TextAppearance.StatusBar.Clock" + android:textAppearance="@style/TextAppearance.StatusBar.Default.Clock" android:singleLine="true" android:paddingStart="@dimen/status_bar_left_clock_starting_padding" android:paddingEnd="@dimen/status_bar_left_clock_end_padding" diff --git a/packages/SystemUI/res/layout/system_icons.xml b/packages/SystemUI/res/layout/system_icons.xml index bb99d581c0b0..f3f4e880c121 100644 --- a/packages/SystemUI/res/layout/system_icons.xml +++ b/packages/SystemUI/res/layout/system_icons.xml @@ -45,6 +45,6 @@ android:clipChildren="false" android:paddingEnd="@dimen/status_bar_battery_end_padding" android:visibility="gone" - systemui:textAppearance="@style/TextAppearance.StatusBar.Clock" /> + systemui:textAppearance="@style/TextAppearance.StatusBar.Default" /> </LinearLayout> diff --git a/packages/SystemUI/res/values-af/strings.xml b/packages/SystemUI/res/values-af/strings.xml index 06192813672a..5ed44a804a0d 100644 --- a/packages/SystemUI/res/values-af/strings.xml +++ b/packages/SystemUI/res/values-af/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Klik om nuwe toestel saam te bind"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Kon nie voorafstelling opdateer nie"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Voorafstelling"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Verstekmikrofoon vir oproepe"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Gehoortoestelmikrofoon"</item> + <item msgid="8501466270452446450">"Hierdie foon se mikrofoon"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Gekies"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Omgewing"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Links"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Aan – gesiggegrond"</string> <string name="inline_done_button" msgid="6043094985588909584">"Klaar"</string> <string name="inline_ok_button" msgid="603075490581280343">"Pas toe"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Skakel af"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Stil"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Verstek"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Outomaties"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Regs-ikoon"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Hou en sleep om teëls by te voeg"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Hou en sleep om teëls te herrangskik"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Sleep hierheen om te verwyder"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Jy moet minstens <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> teëls hê"</string> <string name="qs_edit" msgid="5583565172803472437">"Wysig"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Ander"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"wissel die teël se grootte"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"verwyder teël"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"voeg teël by die laaste posisie"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Skuif teël"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Voeg teël by die verlangde posisie"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Onbekend"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Stel alle teëls terug?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Alle Kitsinstellingsteëls sal na die toestel se oorspronklike instellings teruggestel word"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-am/strings.xml b/packages/SystemUI/res/values-am/strings.xml index 3e4ca1c25453..de3e4370f4f7 100644 --- a/packages/SystemUI/res/values-am/strings.xml +++ b/packages/SystemUI/res/values-am/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"አዲስ መሣሪያ ለማጣመር ጠቅ ያድርጉ"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"ቅድመ-ቅምጥን ማዘመን አልተቻለም"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"ቅድመ-ቅምጥ"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"ለጥሪዎች ነባሪ ማይክሮፎን"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"የመስሚያ አጋዥ መሣሪያ ማይክሮፎን"</item> + <item msgid="8501466270452446450">"የዚህ ስልክ ማይክሮፎን"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"ተመርጧል"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"በዙሪያ ያሉ"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"ግራ"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"በርቷል - መልክ ላይ የተመሠረተ"</string> <string name="inline_done_button" msgid="6043094985588909584">"ተከናውኗል"</string> <string name="inline_ok_button" msgid="603075490581280343">"ተግብር"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"አጥፋ"</string> <string name="notification_silence_title" msgid="8608090968400832335">"ፀጥ ያለ"</string> <string name="notification_alert_title" msgid="3656229781017543655">"ነባሪ"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"ራስ-ሰር"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"የቀኝ አዶ"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"ፋይሎችን ለማከል ይዘት ይጎትቱ"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"ሰድሮችን ዳግም ለማስተካከል ይያዙ እና ይጎትቱ።"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"ለማስወገድ ወደዚህ ይጎትቱ"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"ቢያንስ <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> ሰቆች ያስፈልገዎታል"</string> <string name="qs_edit" msgid="5583565172803472437">"አርትዕ"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"ሌላ"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"የርዕሱን መጠን ይቀያይሩ"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"ሰቅ አስወግድ"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"በመጨረሻው ቦታ ላይ ሰቅ ያክሉ"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"ሰቁን ውሰድ"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"ወደተፈለገው ቦታ ሰቅ ያክሉ"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"ያልታወቀ"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"ሁሉም ሰቆች ዳግም ይጀምሩ?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"ሁሉም የፈጣን ቅንብሮች ሰቆች ወደ የመሣሪያው የመጀመሪያ ቅንብሮች ዳግም ይጀምራሉ"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>፣ <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-ar/strings.xml b/packages/SystemUI/res/values-ar/strings.xml index f9660a340b46..b0f96d666255 100644 --- a/packages/SystemUI/res/values-ar/strings.xml +++ b/packages/SystemUI/res/values-ar/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"انقر لإقران جهاز جديد"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"تعذَّر تعديل الإعداد المسبق"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"الإعدادات المسبقة"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"الميكروفون التلقائي لإجراء المكالمات"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"ميكروفون سماعة الأذن الطبية"</item> + <item msgid="8501466270452446450">"ميكروفون هذا الهاتف"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"تمّ اختياره"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"الأصوات المحيطة"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"اليسرى"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"ستتم مشاركة كل المحتوى المعروض أو المشغَّل على شاشة هذا التطبيق مع تطبيق \"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>\"، لذا يُرجى توخي الحذر بشأن المعلومات الظاهرة على الشاشة، مثل كلمات المرور وتفاصيل الدفع والرسائل والصور والمقاطع الصوتية والفيديوهات."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"مشاركة الشاشة"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"تم إيقاف هذا الخيار من خلال تطبيق <xliff:g id="APP_NAME">%1$s</xliff:g>."</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"لا يتيح التطبيق هذه الميزة"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"يُرجى اختيار تطبيق لمشاركة محتواه"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"هل تريد بث محتوى الشاشة؟"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"بث محتوى تطبيق واحد"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"تفعيل - استنادًا للوجه"</string> <string name="inline_done_button" msgid="6043094985588909584">"تمّ"</string> <string name="inline_ok_button" msgid="603075490581280343">"تطبيق"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"إيقاف"</string> <string name="notification_silence_title" msgid="8608090968400832335">"إشعارات صامتة"</string> <string name="notification_alert_title" msgid="3656229781017543655">"تلقائية"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"تلقائي"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"رمز اليمين"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"اضغط باستمرار مع السحب لإضافة المربّعات"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"اضغط باستمرار مع السحب لإعادة ترتيب الميزات"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"اسحب هنا للإزالة"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"الحدّ الأدنى من عدد المربعات الذي تحتاج إليه هو <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g>"</string> <string name="qs_edit" msgid="5583565172803472437">"تعديل"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"غير ذلك"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"تبديل حجم المربّع"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"إزالة بطاقة"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"إضافة مربّع إلى الموضع الأخير"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"نقل بطاقة"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"إضافة مربّع إلى الموضع المطلوب"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"غير معروفة"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"هل تريد إعادة ضبط كل المربّعات؟"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"ستتم إعادة ضبط جميع مربّعات \"الإعدادات السريعة\" إلى الإعدادات الأصلية للجهاز"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"\"<xliff:g id="STREAM_NAME">%1$s</xliff:g>\"، <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-ar/tiles_states_strings.xml b/packages/SystemUI/res/values-ar/tiles_states_strings.xml index 31607b9548a4..ba73b9bef9c1 100644 --- a/packages/SystemUI/res/values-ar/tiles_states_strings.xml +++ b/packages/SystemUI/res/values-ar/tiles_states_strings.xml @@ -56,9 +56,11 @@ <item msgid="5376619709702103243">"الميزة غير مفعّلة"</item> <item msgid="4875147066469902392">"الميزة مفعّلة"</item> </string-array> - <!-- no translation found for tile_states_modes_dnd:0 (6509540227356524582) --> - <!-- no translation found for tile_states_modes_dnd:1 (8589336868985358191) --> - <!-- no translation found for tile_states_modes_dnd:2 (726072717827778234) --> + <string-array name="tile_states_modes_dnd"> + <item msgid="6509540227356524582">"غير متوفّر"</item> + <item msgid="8589336868985358191">"غير مفعَّل"</item> + <item msgid="726072717827778234">"مفعّل"</item> + </string-array> <string-array name="tile_states_flashlight"> <item msgid="3465257127433353857">"الميزة غير متاحة"</item> <item msgid="5044688398303285224">"الميزة غير مفعّلة"</item> diff --git a/packages/SystemUI/res/values-as/strings.xml b/packages/SystemUI/res/values-as/strings.xml index 595432e06666..ccec3ddd1711 100644 --- a/packages/SystemUI/res/values-as/strings.xml +++ b/packages/SystemUI/res/values-as/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"নতুন ডিভাইচ পেয়াৰ কৰিবলৈ ক্লিক কৰক"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"প্ৰিছেট আপডে’ট কৰিব পৰা নগ’ল"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"প্ৰিছেট"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"কলৰ বাবে ডিফ’ল্ট মাইক্ৰ’ফ’ন"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"শ্ৰৱণ যন্ত্ৰৰ মাইক্ৰ’ফ’ন"</item> + <item msgid="8501466270452446450">"এই ফ’নটোৰ মাইক্ৰ’ফ’ন"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"বাছনি কৰা হৈছে"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"আশ-পাশ"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"বাওঁফাল"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"অন আছে - মুখাৱয়ব ভিত্তিক"</string> <string name="inline_done_button" msgid="6043094985588909584">"কৰা হ’ল"</string> <string name="inline_ok_button" msgid="603075490581280343">"প্ৰয়োগ কৰক"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"অফ কৰক"</string> <string name="notification_silence_title" msgid="8608090968400832335">"নীৰৱ"</string> <string name="notification_alert_title" msgid="3656229781017543655">"ডিফ’ল্ট"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"স্বয়ংক্ৰিয়"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"সোঁ আইকন"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"টাইল যোগ কৰিবলৈ হেঁচি ধৰি টানি আনক"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"টাইলসমূহ পুনৰ সজাবলৈ ধৰি টানক"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"আঁতৰাবৰ বাবে টানি আনি ইয়াত এৰি দিয়ক"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"আপোনাক অতিকমেও <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g>খন টাইল লাগিব"</string> <string name="qs_edit" msgid="5583565172803472437">"সম্পাদনা কৰক"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"অন্যান্য"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"টাইলৰ আকাৰ ট’গল কৰক"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"টাইল আঁতৰাবলৈ"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"অন্তিম স্থানত টাইল যোগ দিয়ক"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"টাইল স্থানান্তৰ কৰক"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"বিচৰা স্থানত টাইল যোগ দিয়ক"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"অজ্ঞাত"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"আটাইবোৰ টাইল ৰিছেট কৰিবনে?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"আটাইবোৰ ক্ষিপ্ৰ ছেটিঙৰ টাইল ডিভাইচৰ মূল ছেটিংছলৈ ৰিছেট হ’ব"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-az/strings.xml b/packages/SystemUI/res/values-az/strings.xml index 3cf675d3ab12..37091c38fba2 100644 --- a/packages/SystemUI/res/values-az/strings.xml +++ b/packages/SystemUI/res/values-az/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Yeni cihaz birləşdirmək üçün klikləyin"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Hazır ayar güncəllənmədi"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Hazır Ayar"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Zənglər üçün defolt mikrofon"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Eşitmə aparatı mikrofonu"</item> + <item msgid="8501466270452446450">"Bu telefonun mikrofonu"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Seçilib"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Ətraf mühit"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Sol"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"Tətbiq paylaşdığınız zaman həmin tətbiqdə göstərilən və ya işə salınan hər şey <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> üçün görünən olacaq. Parol, ödəniş məlumatı, mesaj, foto, habelə audio və video kimi məlumatlarla bağlı diqqətli olun."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"Ekranı paylaşın"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"<xliff:g id="APP_NAME">%1$s</xliff:g> bu seçimi deaktiv edib"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"Tətbiq tərəfindən dəstəklənmir"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"Paylaşmaq üçün tətbiq seçin"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"Ekran yayımlansın?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"Bir tətbiqi yayımlayın"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Aktiv - Üz əsaslı"</string> <string name="inline_done_button" msgid="6043094985588909584">"Hazırdır"</string> <string name="inline_ok_button" msgid="603075490581280343">"Tətbiq edin"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Söndürün"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Səssiz"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Defolt"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Avtomatik"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"Sağ ikona"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Mozaika əlavə etmək üçün basıb saxlayaraq çəkin"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"İkonları yenidən düzənləmək üçün saxlayaraq dartın"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Silmək üçün bura sürüşdürün"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Minimum <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> mozaika lazımdır"</string> <string name="qs_edit" msgid="5583565172803472437">"Redaktə edin"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"Digər"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"mozaik ölçüsünü aktiv/deaktiv edin"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"lövhəni silin"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"son mövqeyə mozaik əlavə edin"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Lövhəni köçürün"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"İstənilən mövqeyə mozaik əlavə edin"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Naməlum"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Bütün mozaiklər sıfırlansın?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Bütün Sürətli Ayarlar mozaiki cihazın orijinal ayarlarına sıfırlanacaq"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-b+sr+Latn/strings.xml b/packages/SystemUI/res/values-b+sr+Latn/strings.xml index b74094e54d36..98aabe744722 100644 --- a/packages/SystemUI/res/values-b+sr+Latn/strings.xml +++ b/packages/SystemUI/res/values-b+sr+Latn/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Kliknite da biste uparili nov uređaj"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Ažuriranje zadatih podešavanja nije uspelo"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Unapred određena podešavanja"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Podrazumevani mikrofon za pozive"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Mikrofon slušnog aparata"</item> + <item msgid="8501466270452446450">"Mikrofon ovog telefona"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Izabrano"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Okruženje"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Levo"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"Kada delite aplikaciju, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> vidi sav sadržaj koji se prikazuje ili pušta u njoj. Zato pazite na lozinke, informacije o plaćanju, poruke, slike, audio i video sadržaj."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"Deli ekran"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"Aplikacija <xliff:g id="APP_NAME">%1$s</xliff:g> je onemogućila ovu opciju"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"Aplikacija to ne podržava"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"Odaberite aplikaciju za deljenje"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"Želite da prebacite ekran?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"Prebaci jednu aplikaciju"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Uključeno – na osnovu lica"</string> <string name="inline_done_button" msgid="6043094985588909584">"Gotovo"</string> <string name="inline_ok_button" msgid="603075490581280343">"Primeni"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Isključi"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Nečujno"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Podrazumevano"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automatska"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"Desna ikona"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Zadržite i prevucite da biste dodali pločice"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Zadržite i prevucite da biste promenili raspored pločica"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Prevucite ovde da biste uklonili"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Minimalan broj pločica je <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g>"</string> <string name="qs_edit" msgid="5583565172803472437">"Izmeni"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"Drugo"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"uključivanje ili isključivanje veličine pločice"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"uklonili pločicu"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"dodali pločicu na poslednju poziciju"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Premestite pločicu"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Dodajte pločicu na željenu poziciju"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Nepoznato"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Želite da resetujete sve pločice?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Sve pločice Brzih podešavanja će se resetovati na prvobitna podešavanja uređaja"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-be/strings.xml b/packages/SystemUI/res/values-be/strings.xml index 11c2715fb2ff..bf9929e53ad4 100644 --- a/packages/SystemUI/res/values-be/strings.xml +++ b/packages/SystemUI/res/values-be/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Націсніце, каб спалучыць новую прыладу"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Не ўдалося абнавіць набор налад"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Набор налад"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Стандартны мікрафон для выклікаў"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Мікрафон слыхавога апарата"</item> + <item msgid="8501466270452446450">"Мікрафон гэтага тэлефона"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Выбрана"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Навакольныя гукі"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Левы бок"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"Калі вы абагульваеце праграму, праграма \"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>\" можа бачыць усё, што паказваецца ці прайграецца ў гэтай праграме. Таму прадухіліце паказ пароляў, плацежных рэквізітаў, паведамленняў, фота, відэа і аўдыя."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"Абагуліць экран"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"Праграма \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" адключыла гэты параметр"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"Не падтрымліваецца праграмай"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"Выберыце праграму для абагульвання"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"Уключыць трансляцыю экрана?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"Трансліраваць адну праграму"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Уключана – З улікам паставы галавы"</string> <string name="inline_done_button" msgid="6043094985588909584">"Гатова"</string> <string name="inline_ok_button" msgid="603075490581280343">"Прымяніць"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Выключыць"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Бязгучны рэжым"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Стандартна"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Аўтаматычна"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"Значок \"управа\""</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Перацягніце патрэбныя пліткі"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Каб змяніць парадак плітак, утрымлівайце і перацягвайце іх"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Перацягніце сюды, каб выдаліць"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Мінімальная колькасць плітак: <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g>"</string> <string name="qs_edit" msgid="5583565172803472437">"Рэдагаваць"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"Іншае"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"змяніць памер пліткі"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"выдаліць плітку"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"дадаць плітку ў апошнюю пазіцыю"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Перамясціць плітку"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Дадаць плітку ў патрэбную пазіцыю"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Невядома"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Скінуць усе пліткі?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Усе пліткі хуткіх налад будуць скінуты да першапачатковых налад прылады"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-bg/strings.xml b/packages/SystemUI/res/values-bg/strings.xml index a085b1545867..4a0b80bf29cb 100644 --- a/packages/SystemUI/res/values-bg/strings.xml +++ b/packages/SystemUI/res/values-bg/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Кликнете за сдвояване на ново устройство"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Предварително зададените настройки не бяха актуализирани"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Предварително зададено"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Микрофон по подразбиране за обажданията"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Микрофонът на слуховия апарат"</item> + <item msgid="8501466270452446450">"Микрофонът на този телефон"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Избрано"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Околни звуци"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Ляво"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"Когато споделяте приложение, всичко, което се показва или възпроизвежда в него, е видимо за <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>. Затова бъдете внимателни с неща като пароли, подробности за начини на плащане, съобщения, снимки, аудио и видео."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"Споделяне на екрана"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"<xliff:g id="APP_NAME">%1$s</xliff:g> деактивира тази опция"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"Не се поддържа от приложението"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"Изберете приложение за споделяне"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"Искате ли да предавате екрана си?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"Предаване на едно приложение"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Вкл. – въз основа на лицето"</string> <string name="inline_done_button" msgid="6043094985588909584">"Готово"</string> <string name="inline_ok_button" msgid="603075490581280343">"Прилагане"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Изключване"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Тих режим"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Стандартно"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Автоматично"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"Дясна икона"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Задръжте и плъзнете, за да добавите плочки"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Задръжте и плъзнете, за да пренаредите плочките"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Преместете тук с плъзгане за премахване"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Трябва да останат поне <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> плочки"</string> <string name="qs_edit" msgid="5583565172803472437">"Редактиране"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"Друго"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"превключване на размера на панела"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"премахване на панел"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"добавяне на панела на последната позиция"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Преместване на панел"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Добавяне на панела на желаната позиция"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Неизвестно"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Да се нулират ли всички панели?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Всички панели с бързи настройки ще бъдат нулирани до първоначалните настройки на устройството"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g> – <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-bn/strings.xml b/packages/SystemUI/res/values-bn/strings.xml index 18d632e95502..ac365f8356d1 100644 --- a/packages/SystemUI/res/values-bn/strings.xml +++ b/packages/SystemUI/res/values-bn/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"নতুন ডিভাইস পেয়ার করতে ক্লিক করুন"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"প্রিসেট আপডেট করা যায়নি"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"প্রিসেট"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"কলের জন্য ডিফল্ট মাইক্রোফোন"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"হিয়ারিং এড মাইক্রোফোন"</item> + <item msgid="8501466270452446450">"এই ফোনের মাইক্রোফোন"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"বেছে নেওয়া হয়েছে"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"সারাউন্ডিং"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"বাঁদিক"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"চালু আছে - মুখের হিসেবে"</string> <string name="inline_done_button" msgid="6043094985588909584">"হয়ে গেছে"</string> <string name="inline_ok_button" msgid="603075490581280343">"প্রয়োগ করুন"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"বন্ধ করুন"</string> <string name="notification_silence_title" msgid="8608090968400832335">"সাইলেন্ট"</string> <string name="notification_alert_title" msgid="3656229781017543655">"ডিফল্ট"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"অটোমেটিক"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"ডানদিকের আইকন"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"টাইল যোগ করতে ধরে থেকে টেনে আনুন"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"টাইলগুলি আবার সাজানোর জন্য ধরে থেকে টেনে আনুন"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"সরানোর জন্য এখানে টেনে আনুন"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"আপনাকে কমপক্ষে <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g>টি টাইল রাখতে হবে"</string> <string name="qs_edit" msgid="5583565172803472437">"এডিট করুন"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"অন্যান্য"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"টাইলের সাইজ টগল করুন"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"টাইল সরান"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"শেষ জায়গাতে টাইল যোগ করুন"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"টাইল সরান"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"আপনার পছন্দের জায়গাতে টাইল যোগ করুন"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"অজানা"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"সব টাইল রিসেট করবেন?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"সব কুইক সেটিংস টাইল, ডিভাইসের আসল সেটিংসে রিসেট হয়ে যাবে"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-bn/tiles_states_strings.xml b/packages/SystemUI/res/values-bn/tiles_states_strings.xml index 852986ccb0fd..95a567a8d47b 100644 --- a/packages/SystemUI/res/values-bn/tiles_states_strings.xml +++ b/packages/SystemUI/res/values-bn/tiles_states_strings.xml @@ -56,9 +56,11 @@ <item msgid="5376619709702103243">"বন্ধ আছে"</item> <item msgid="4875147066469902392">"চালু আছে"</item> </string-array> - <!-- no translation found for tile_states_modes_dnd:0 (6509540227356524582) --> - <!-- no translation found for tile_states_modes_dnd:1 (8589336868985358191) --> - <!-- no translation found for tile_states_modes_dnd:2 (726072717827778234) --> + <string-array name="tile_states_modes_dnd"> + <item msgid="6509540227356524582">"উপলভ্য নেই"</item> + <item msgid="8589336868985358191">"বন্ধ করুন"</item> + <item msgid="726072717827778234">"চালু করুন"</item> + </string-array> <string-array name="tile_states_flashlight"> <item msgid="3465257127433353857">"উপলভ্য নেই"</item> <item msgid="5044688398303285224">"বন্ধ আছে"</item> diff --git a/packages/SystemUI/res/values-bs/strings.xml b/packages/SystemUI/res/values-bs/strings.xml index c18af99aafc0..c026090f991d 100644 --- a/packages/SystemUI/res/values-bs/strings.xml +++ b/packages/SystemUI/res/values-bs/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Kliknite da uparite novi uređaj"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Ažuriranje zadane postavke nije uspjelo"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Zadana postavka"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Zadani mikrofon za pozive"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Mikrofon slušnog aparata"</item> + <item msgid="8501466270452446450">"Mikrofon telefona"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Odabrano"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Okruženje"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Lijevo"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Uključeno – na osnovu lica"</string> <string name="inline_done_button" msgid="6043094985588909584">"Gotovo"</string> <string name="inline_ok_button" msgid="603075490581280343">"Primijeni"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Isključi"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Nečujno"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Zadano"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automatski"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Ikona desno"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Držite i prevucite da dodate polja"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Držite i prevucite da preuredite polja"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Prevucite ovdje za uklanjanje"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Broj polja mora biti najmanje <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g>"</string> <string name="qs_edit" msgid="5583565172803472437">"Uredite"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Ostalo"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"uključivanje/isključivanje veličine kartice"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"uklanjanje kartice"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"dodavanje kartice na posljednji položaj"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Pomjeranje kartice"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Dodavanje kartice na željeni položaj"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Nepoznato"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Vratiti sve kartice na zadano?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Sve kartice Brze postavke će se vratiti na originalne postavke uređaja"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-ca/strings.xml b/packages/SystemUI/res/values-ca/strings.xml index 0404bc70b75e..ad1482490449 100644 --- a/packages/SystemUI/res/values-ca/strings.xml +++ b/packages/SystemUI/res/values-ca/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Fes clic per vincular un dispositiu nou"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"No s\'ha pogut actualitzar el valor predefinit"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Valors predefinits"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Micròfon predeterminat per a trucades"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Micròfon d\'audiòfon"</item> + <item msgid="8501466270452446450">"El micròfon d\'aquest telèfon"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Seleccionat"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Entorn"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Esquerra"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Activat: basat en cares"</string> <string name="inline_done_button" msgid="6043094985588909584">"Fet"</string> <string name="inline_ok_button" msgid="603075490581280343">"Aplica"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Desactiva"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Silenci"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Predeterminat"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automàtic"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Icona de la dreta"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Mantén premudes les icones i arrossega-les per afegir-les"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Mantén premudes les icones i arrossega-les per reordenar-les"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Arrossega aquí per suprimir"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Necessites com a mínim <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> mosaics"</string> <string name="qs_edit" msgid="5583565172803472437">"Edita"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Altres"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"commutar la mida de la icona"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"suprimir el mosaic"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"afegir una icona a la darrera posició"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Mou el mosaic"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Afegeix una icona a la posició que vulguis"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Desconegut"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Vols restablir totes les icones?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Totes les icones de configuració ràpida es restabliran a les opcions originals del dispositiu"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-cs/strings.xml b/packages/SystemUI/res/values-cs/strings.xml index e438d6332dd0..9fde63c12ffc 100644 --- a/packages/SystemUI/res/values-cs/strings.xml +++ b/packages/SystemUI/res/values-cs/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Kliknutím spárujete nové zařízení"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Předvolbu nelze aktualizovat"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Předvolba"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Výchozí mikrofon pro hovory"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Mikrofon naslouchátka"</item> + <item msgid="8501466270452446450">"Mikrofon tohohle telefonu"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Vybráno"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Okolí"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Vlevo"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Zapnuto – podle obličeje"</string> <string name="inline_done_button" msgid="6043094985588909584">"Hotovo"</string> <string name="inline_ok_button" msgid="603075490581280343">"Použít"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Vypnout"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Tichý režim"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Výchozí"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automaticky"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Ikona vpravo"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Dlaždice přidáte podržením a přetažením"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Dlaždice můžete uspořádat podržením a přetažením"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Přetažením sem dlaždice odstraníte"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Potřebujete alespoň tento počet dlaždic: <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g>"</string> <string name="qs_edit" msgid="5583565172803472437">"Upravit"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Jiné"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"přepnout velikost dlaždice"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"odstranit dlaždici"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"přidat dlaždici na poslední pozici"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Přesunout dlaždici"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Přidat dlaždici na požadované místo"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Neznámé"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Resetovat všechny dlaždice?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Všechny dlaždice Rychlého nastavení se resetují do původní konfigurace zařízení"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-da/strings.xml b/packages/SystemUI/res/values-da/strings.xml index 52abc4a2fda5..fbb293b29e20 100644 --- a/packages/SystemUI/res/values-da/strings.xml +++ b/packages/SystemUI/res/values-da/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Klik for at parre en ny enhed"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Forindstillingen kunne ikke opdateres"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Forindstilling"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Standardmikrofon til opkald"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Mikrofon i høreapparat"</item> + <item msgid="8501466270452446450">"Denne telefons mikrofon"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Valgt"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Omgivelser"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Venstre"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Til – ansigtsbaseret"</string> <string name="inline_done_button" msgid="6043094985588909584">"Udfør"</string> <string name="inline_ok_button" msgid="603075490581280343">"Anvend"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Deaktiver"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Lydløs"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Standard"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automatisk"</string> @@ -912,7 +912,7 @@ <string name="system_multitasking_rhs" msgid="8779289852395243004">"Brug opdelt skærm med appen til højre"</string> <string name="system_multitasking_lhs" msgid="7348595296208696452">"Brug opdelt skærm med appen til venstre"</string> <string name="system_multitasking_full_screen" msgid="4221409316059910349">"Brug fuld skærm"</string> - <string name="system_multitasking_desktop_view" msgid="8871367687089347180">"Brug vinduer på skrivebordet"</string> + <string name="system_multitasking_desktop_view" msgid="8871367687089347180">"Brug skrivebordsvinduer"</string> <string name="system_multitasking_splitscreen_focus_rhs" msgid="3838578650313318508">"Skift til en app til højre eller nedenfor, når du bruger opdelt skærm"</string> <string name="system_multitasking_splitscreen_focus_lhs" msgid="3164261844398662518">"Skift til en app til venstre eller ovenfor, når du bruger opdelt skærm"</string> <string name="system_multitasking_replace" msgid="7410071959803642125">"Ved opdelt skærm: Udskift én app med en anden"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Højre ikon"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Tilføj felter ved at holde dem nede og trække dem"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Flyt rundt på felterne ved at holde dem nede og trække"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Træk herhen for at fjerne"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Du skal bruge mindst <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> felter"</string> <string name="qs_edit" msgid="5583565172803472437">"Rediger"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Andet"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"ændre feltets størrelse"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"fjern felt"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"føj handlingsfeltet til den sidste position"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Flyt felt"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Føj handlingsfeltet til den ønskede placering"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Ukendt"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Vil du nulstille alle handlingsfelter?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Alle handlingsfelter i kvikmenuen nulstilles til enhedens oprindelige indstillinger"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-de/strings.xml b/packages/SystemUI/res/values-de/strings.xml index 612c52f5c127..86304f7b651c 100644 --- a/packages/SystemUI/res/values-de/strings.xml +++ b/packages/SystemUI/res/values-de/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Klicken, um neues Gerät zu koppeln"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Voreinstellung konnte nicht aktualisiert werden"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Voreinstellung"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Standardmikrofon für Anrufe"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Mikrofon des Hörgeräts"</item> + <item msgid="8501466270452446450">"Mikrofon dieses Smartphones"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Ausgewählt"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Umgebungsgeräusche"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Links"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"An – gesichtsbasiert"</string> <string name="inline_done_button" msgid="6043094985588909584">"Fertig"</string> <string name="inline_ok_button" msgid="603075490581280343">"Anwenden"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Deaktivieren"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Lautlos"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Standard"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automatisch"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Rechtes Symbol"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Zum Hinzufügen Kachel halten und ziehen"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Zum Verschieben Kachel halten und ziehen"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Zum Entfernen hierher ziehen"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Du brauchst mindestens <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> Kacheln"</string> <string name="qs_edit" msgid="5583565172803472437">"Bearbeiten"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Sonstiges"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"Größe der Kachel umschalten"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"Entfernen der Kachel"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"Kachel an letzter Position hinzufügen"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Kachel verschieben"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Kachel an gewünschter Position hinzufügen"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Unbekannt"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Alle Kacheln zurücksetzen?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Alle Schnelleinstellungen-Kacheln werden auf die Standardeinstellungen des Geräts zurückgesetzt"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-el/strings.xml b/packages/SystemUI/res/values-el/strings.xml index 2c17a394b022..478ac05eaa65 100644 --- a/packages/SystemUI/res/values-el/strings.xml +++ b/packages/SystemUI/res/values-el/strings.xml @@ -49,23 +49,23 @@ <string name="label_view" msgid="6815442985276363364">"Προβολή"</string> <string name="always_use_device" msgid="210535878779644679">"Να ανοίγει πάντα η εφ. <xliff:g id="APPLICATION">%1$s</xliff:g> όταν είναι συνδεδεμένo το <xliff:g id="USB_DEVICE">%2$s</xliff:g>"</string> <string name="always_use_accessory" msgid="1977225429341838444">"Να ανοίγει πάντα η εφ. <xliff:g id="APPLICATION">%1$s</xliff:g> όταν είναι συνδεδεμένο το <xliff:g id="USB_ACCESSORY">%2$s</xliff:g>"</string> - <string name="usb_debugging_title" msgid="8274884945238642726">"Να επιτρέπεται ο εντοπισμός σφαλμάτων USB;"</string> + <string name="usb_debugging_title" msgid="8274884945238642726">"Να επιτρέπεται η αποσφαλμάτωση USB;"</string> <string name="usb_debugging_message" msgid="5794616114463921773">"Το μοναδικό χαρακτηριστικό του κλειδιού RSA είναι:\n<xliff:g id="FINGERPRINT">%1$s</xliff:g>"</string> <string name="usb_debugging_always" msgid="4003121804294739548">"Να επιτρέπεται πάντα από αυτόν τον υπολογιστή"</string> <string name="usb_debugging_allow" msgid="1722643858015321328">"Να επιτρέπεται"</string> - <string name="usb_debugging_secondary_user_title" msgid="7843050591380107998">"Δεν επιτρέπεται ο εντοπισμός σφαλμάτων USB"</string> - <string name="usb_debugging_secondary_user_message" msgid="1888835696965417845">"Ο χρήστης που είναι συνδεδεμένος αυτήν τη στιγμή σε αυτήν τη συσκευή δεν μπορεί να ενεργοποιήσει τον εντοπισμό σφαλμάτων USB. Για να χρησιμοποιήσετε αυτήν τη λειτουργία, κάντε εναλλαγή σε έναν χρήστη με δικαιώματα διαχειριστή."</string> + <string name="usb_debugging_secondary_user_title" msgid="7843050591380107998">"Δεν επιτρέπεται η αποσφαλμάτωση USB"</string> + <string name="usb_debugging_secondary_user_message" msgid="1888835696965417845">"Ο χρήστης που είναι συνδεδεμένος αυτήν τη στιγμή σε αυτήν τη συσκευή δεν μπορεί να ενεργοποιήσει την αποσφαλμάτωση USB. Για να χρησιμοποιήσετε αυτήν τη λειτουργία, κάντε εναλλαγή σε έναν χρήστη με δικαιώματα διαχειριστή."</string> <string name="hdmi_cec_set_menu_language_title" msgid="1259765420091503742">"Θέλετε να αλλάξετε τη γλώσσα συστήματος σε <xliff:g id="LANGUAGE">%1$s</xliff:g>;"</string> <string name="hdmi_cec_set_menu_language_description" msgid="8176716678074126619">"Ζητήθηκε αλλαγή της γλώσσας συστήματος από άλλη συσκευή"</string> <string name="hdmi_cec_set_menu_language_accept" msgid="2513689457281009578">"Αλλαγή γλώσσας"</string> <string name="hdmi_cec_set_menu_language_decline" msgid="7650721096558646011">"Διατήρ. τρέχουσας γλώσσας"</string> <string name="share_wifi_button_text" msgid="1285273973812029240">"Μοιραστείτε το Wi‑Fi"</string> - <string name="wifi_debugging_title" msgid="7300007687492186076">"Να επιτρέπεται ασύρματος εντοπ. σφαλ. στο δίκτυο;"</string> + <string name="wifi_debugging_title" msgid="7300007687492186076">"Να επιτρέπεται ασύρματη αποσφαλμάτωση στο δίκτυο;"</string> <string name="wifi_debugging_message" msgid="5461204211731802995">"Όνομα δικτύου (SSID)\n<xliff:g id="SSID_0">%1$s</xliff:g>\n\nΔιεύθυνση Wi‑Fi (BSSID)\n<xliff:g id="BSSID_1">%2$s</xliff:g>"</string> <string name="wifi_debugging_always" msgid="2968383799517975155">"Να επιτρέπεται πάντα σε αυτό το δίκτυο"</string> <string name="wifi_debugging_allow" msgid="4573224609684957886">"Να επιτρέπεται"</string> - <string name="wifi_debugging_secondary_user_title" msgid="2493201475880517725">"Ο ασύρματος εντοπισμός σφαλμάτων δεν επιτρέπεται"</string> - <string name="wifi_debugging_secondary_user_message" msgid="9085779370142222881">"Ο χρήστης που είναι συνδεδεμένος αυτήν τη στιγμή στη συγκεκριμένη συσκευή δεν μπορεί να ενεργοποιήσει τον ασύρματο εντοπισμό σφαλμάτων. Για να χρησιμοποιήσετε αυτήν τη λειτουργία, κάντε εναλλαγή σε έναν χρήστη με δικαιώματα διαχειριστή."</string> + <string name="wifi_debugging_secondary_user_title" msgid="2493201475880517725">"Η ασύρματη αποσφαλμάτωση δεν επιτρέπεται"</string> + <string name="wifi_debugging_secondary_user_message" msgid="9085779370142222881">"Ο χρήστης που είναι συνδεδεμένος αυτήν τη στιγμή στη συγκεκριμένη συσκευή δεν μπορεί να ενεργοποιήσει την ασύρματη αποσφαλμάτωση. Για να χρησιμοποιήσετε αυτήν τη λειτουργία, κάντε εναλλαγή σε έναν χρήστη με δικαιώματα διαχειριστή."</string> <string name="usb_contaminant_title" msgid="894052515034594113">"Η θύρα USB απενεργοποιήθηκε"</string> <string name="usb_contaminant_message" msgid="7730476585174719805">"Για την προστασία της συσκευής σας από υγρασία ή ακαθαρσίες, η θύρα USB έχει απενεργοποιηθεί και δεν θα εντοπίζει τυχόν αξεσουάρ.\n\nΘα ειδοποιηθείτε όταν θα μπορείτε να χρησιμοποιήσετε ξανά τη θύρα USB."</string> <string name="usb_port_enabled" msgid="531823867664717018">"Η θύρα USB ενεργοποιήθηκε για τον εντοπισμό φορτιστών και αξεσουάρ"</string> @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Κάντε κλικ για σύζευξη νέας συσκευής"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Δεν ήταν δυνατή η ενημέρωση της προεπιλογής"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Προεπιλογή"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Προεπιλεγμένο μικρόφωνο για κλήσεις"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Μικρόφωνο του βοηθήματος ακοής"</item> + <item msgid="8501466270452446450">"Μικρόφωνο αυτού του τηλεφώνου"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Έχει επιλεγεί"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Ήχοι περιβάλλοντος"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Αριστερά"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"Όταν μοιράζεστε μια εφαρμογή, οτιδήποτε εμφανίζεται ή αναπαράγεται σε αυτή την εφαρμογή, είναι ορατό στην εφαρμογή <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>. Επομένως, να είστε προσεκτικοί με τους κωδικούς πρόσβασης, τα στοιχεία πληρωμής, τα μηνύματα, τις φωτογραφίες, τον ήχο και το βίντεο."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"Κοινή χρήση οθόνης"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"Η εφαρμογή <xliff:g id="APP_NAME">%1$s</xliff:g> απενεργοποίησε αυτή την επιλογή"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"Δεν υποστηρίζεται από την εφαρμογή"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"Επιλογή εφαρμογής για κοινή χρήση"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"Να γίνει μετάδοση της οθόνης σας;"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"Μετάδοση μίας εφαρμογής"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Ενεργό - Βάσει προσώπου"</string> <string name="inline_done_button" msgid="6043094985588909584">"Τέλος"</string> <string name="inline_ok_button" msgid="603075490581280343">"Εφαρμογή"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Απενεργοποίηση"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Σίγαση"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Προεπιλογή"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Αυτόματο"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"Δεξιό εικονίδιο"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Κρατήστε και σύρετε για την προσθήκη πλακιδίων"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Κρατήστε και σύρετε για αναδιάταξη των πλακιδίων"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Σύρετε εδώ για κατάργηση"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Χρειάζεστε τουλάχιστον <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> πλακίδια"</string> <string name="qs_edit" msgid="5583565172803472437">"Επεξεργασία"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"Άλλο"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"εναλλαγή μεγέθους για το πλακάκι"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"κατάργηση πλακιδίου"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"προσθήκη πλακιδίου στην τελευταία θέση"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Μετακίνηση πλακιδίου"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Προσθήκη πλακιδίου στην επιθυμητή θέση"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Άγνωστο"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Επαναφορά σε όλα τα πλακάκια;"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Σε όλα τα πλακάκια Γρήγορων ρυθμίσεων θα γίνει επαναφορά στις αρχικές ρυθμίσεις της συσκευής"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-en-rAU/strings.xml b/packages/SystemUI/res/values-en-rAU/strings.xml index 7b8f98237c7a..ea332cbc27c5 100644 --- a/packages/SystemUI/res/values-en-rAU/strings.xml +++ b/packages/SystemUI/res/values-en-rAU/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Click to pair new device"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Couldn\'t update preset"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Preset"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Default microphone for calls"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Hearing aid microphone"</item> + <item msgid="8501466270452446450">"This phone\'s microphone"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Selected"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Surroundings"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Left"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"On – Face-based"</string> <string name="inline_done_button" msgid="6043094985588909584">"Done"</string> <string name="inline_ok_button" msgid="603075490581280343">"Apply"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Turn off"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Silent"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Default"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automatic"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Right icon"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Hold and drag to add tiles"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Hold and drag to rearrange tiles"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Drag here to remove"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"You need at least <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> tiles"</string> <string name="qs_edit" msgid="5583565172803472437">"Edit"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Other"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"toggle the tile\'s size"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"remove tile"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"add tile to the last position"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Move tile"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Add tile to desired position"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Unknown"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Reset all tiles?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"All Quick Settings tiles will reset to the device\'s original settings"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-en-rCA/strings.xml b/packages/SystemUI/res/values-en-rCA/strings.xml index 15056c6e9a08..b6dd88a83936 100644 --- a/packages/SystemUI/res/values-en-rCA/strings.xml +++ b/packages/SystemUI/res/values-en-rCA/strings.xml @@ -982,11 +982,11 @@ <string name="right_icon" msgid="1103955040645237425">"Right icon"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Hold and drag to add tiles"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Hold and drag to rearrange tiles"</string> + <string name="tap_to_position_tile" msgid="6282815817773342757">"Tap to position tile"</string> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Drag here to remove"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"You need at least <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> tiles"</string> <string name="qs_edit" msgid="5583565172803472437">"Edit"</string> - <!-- no translation found for qs_edit_tiles (2105215324060865035) --> - <skip /> + <string name="qs_edit_tiles" msgid="2105215324060865035">"Edit tiles"</string> <string name="tuner_time" msgid="2450785840990529997">"Time"</string> <string-array name="clock_options"> <item msgid="3986445361435142273">"Show hours, minutes, and seconds"</item> @@ -1002,6 +1002,8 @@ <string name="other" msgid="429768510980739978">"Other"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"toggle the tile\'s size"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"remove tile"</string> + <string name="accessibility_qs_edit_toggle_placement_mode" msgid="3870429389210569610">"toggle placement mode"</string> + <string name="accessibility_qs_edit_toggle_selection" msgid="2201248304072372239">"toggle selection"</string> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"add tile to the last position"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Move tile"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Add tile to desired position"</string> @@ -1577,5 +1579,6 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Unknown"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Reset all tiles?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"All Quick Settings tiles will reset to the device’s original settings"</string> + <string name="demote_explain_text" msgid="1600426458580544250">"<xliff:g id="APPLICATION">%1$s</xliff:g> will no longer show Live Updates here. You can change this any time in Settings."</string> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-en-rGB/strings.xml b/packages/SystemUI/res/values-en-rGB/strings.xml index 7b8f98237c7a..ea332cbc27c5 100644 --- a/packages/SystemUI/res/values-en-rGB/strings.xml +++ b/packages/SystemUI/res/values-en-rGB/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Click to pair new device"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Couldn\'t update preset"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Preset"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Default microphone for calls"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Hearing aid microphone"</item> + <item msgid="8501466270452446450">"This phone\'s microphone"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Selected"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Surroundings"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Left"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"On – Face-based"</string> <string name="inline_done_button" msgid="6043094985588909584">"Done"</string> <string name="inline_ok_button" msgid="603075490581280343">"Apply"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Turn off"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Silent"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Default"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automatic"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Right icon"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Hold and drag to add tiles"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Hold and drag to rearrange tiles"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Drag here to remove"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"You need at least <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> tiles"</string> <string name="qs_edit" msgid="5583565172803472437">"Edit"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Other"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"toggle the tile\'s size"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"remove tile"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"add tile to the last position"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Move tile"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Add tile to desired position"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Unknown"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Reset all tiles?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"All Quick Settings tiles will reset to the device\'s original settings"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-en-rIN/strings.xml b/packages/SystemUI/res/values-en-rIN/strings.xml index 7b8f98237c7a..ea332cbc27c5 100644 --- a/packages/SystemUI/res/values-en-rIN/strings.xml +++ b/packages/SystemUI/res/values-en-rIN/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Click to pair new device"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Couldn\'t update preset"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Preset"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Default microphone for calls"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Hearing aid microphone"</item> + <item msgid="8501466270452446450">"This phone\'s microphone"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Selected"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Surroundings"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Left"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"On – Face-based"</string> <string name="inline_done_button" msgid="6043094985588909584">"Done"</string> <string name="inline_ok_button" msgid="603075490581280343">"Apply"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Turn off"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Silent"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Default"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automatic"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Right icon"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Hold and drag to add tiles"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Hold and drag to rearrange tiles"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Drag here to remove"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"You need at least <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> tiles"</string> <string name="qs_edit" msgid="5583565172803472437">"Edit"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Other"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"toggle the tile\'s size"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"remove tile"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"add tile to the last position"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Move tile"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Add tile to desired position"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Unknown"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Reset all tiles?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"All Quick Settings tiles will reset to the device\'s original settings"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-es-rUS/strings.xml b/packages/SystemUI/res/values-es-rUS/strings.xml index 1b15b1b4fe40..e9e58e12c286 100644 --- a/packages/SystemUI/res/values-es-rUS/strings.xml +++ b/packages/SystemUI/res/values-es-rUS/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Haz clic para vincular un dispositivo nuevo"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"No se pudo actualizar el ajuste predeterminado"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Ajuste predeterminado"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Micrófono predeterminado para llamadas"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Micrófono de audífonos"</item> + <item msgid="8501466270452446450">"Micrófono de este teléfono"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Seleccionado"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Sonido envolvente"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Izquierda"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Activa - En función del rostro"</string> <string name="inline_done_button" msgid="6043094985588909584">"Listo"</string> <string name="inline_ok_button" msgid="603075490581280343">"Aplicar"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Desactivar"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Silenciada"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Predeterminada"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automática"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Ícono derecho"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Mantén presionada una tarjeta y arrástrala para agregarla"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Mantén presionada una tarjeta y arrástrala para reubicarla"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Arrastra aquí para quitar"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Necesitas al menos <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> tarjetas"</string> <string name="qs_edit" msgid="5583565172803472437">"Editar"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Otros"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"alternar el tamaño del mosaico"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"quitar tarjeta"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"agregar tarjeta a la última posición"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Mover la tarjeta"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Agregar tarjeta a la posición deseada"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Desconocido"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"¿Quieres restablecer todas las tarjetas?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Se restablecerán todas las tarjeta de Configuración rápida a la configuración original del dispositivo"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-es-rUS/tiles_states_strings.xml b/packages/SystemUI/res/values-es-rUS/tiles_states_strings.xml index 3310d2bc0fb0..2eb8435ce803 100644 --- a/packages/SystemUI/res/values-es-rUS/tiles_states_strings.xml +++ b/packages/SystemUI/res/values-es-rUS/tiles_states_strings.xml @@ -56,9 +56,11 @@ <item msgid="5376619709702103243">"Desactivado"</item> <item msgid="4875147066469902392">"Activado"</item> </string-array> - <!-- no translation found for tile_states_modes_dnd:0 (6509540227356524582) --> - <!-- no translation found for tile_states_modes_dnd:1 (8589336868985358191) --> - <!-- no translation found for tile_states_modes_dnd:2 (726072717827778234) --> + <string-array name="tile_states_modes_dnd"> + <item msgid="6509540227356524582">"No disponible"</item> + <item msgid="8589336868985358191">"Desactivado"</item> + <item msgid="726072717827778234">"Activado"</item> + </string-array> <string-array name="tile_states_flashlight"> <item msgid="3465257127433353857">"No disponible"</item> <item msgid="5044688398303285224">"Desactivada"</item> diff --git a/packages/SystemUI/res/values-es/strings.xml b/packages/SystemUI/res/values-es/strings.xml index 2311b48b6570..011b9cce3cf0 100644 --- a/packages/SystemUI/res/values-es/strings.xml +++ b/packages/SystemUI/res/values-es/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Haz clic para emparejar un nuevo dispositivo"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"No se ha podido actualizar el preajuste"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Preajuste"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Micrófono predeterminado para llamadas"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Micrófono del audífono"</item> + <item msgid="8501466270452446450">"El micrófono de este teléfono"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Seleccionado"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Alrededores"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Izquierda"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Activado: basado en caras"</string> <string name="inline_done_button" msgid="6043094985588909584">"Hecho"</string> <string name="inline_ok_button" msgid="603075490581280343">"Aplicar"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Desactivar"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Silencio"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Predeterminado"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automática"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Icono a la derecha"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Mantén pulsado un recuadro y arrástralo para añadirlo"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Mantén pulsado un recuadro y arrástralo para reubicarlo"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Arrastra aquí para quitar una función"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Necesitas al menos <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> recuadros"</string> <string name="qs_edit" msgid="5583565172803472437">"Editar"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Otros"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"mostrar el tamaño del recuadro"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"quitar recuadro"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"añadir el recuadro a la última posición"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Mover recuadro"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Añadir recuadro a la posición deseada"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Desconocido"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"¿Borrar todos los recuadros?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Todos los recuadros de ajustes rápidos se restablecerán a los ajustes originales del dispositivo"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-es/tiles_states_strings.xml b/packages/SystemUI/res/values-es/tiles_states_strings.xml index 546190fc7702..57b140db744e 100644 --- a/packages/SystemUI/res/values-es/tiles_states_strings.xml +++ b/packages/SystemUI/res/values-es/tiles_states_strings.xml @@ -56,9 +56,11 @@ <item msgid="5376619709702103243">"Desactivado"</item> <item msgid="4875147066469902392">"Activado"</item> </string-array> - <!-- no translation found for tile_states_modes_dnd:0 (6509540227356524582) --> - <!-- no translation found for tile_states_modes_dnd:1 (8589336868985358191) --> - <!-- no translation found for tile_states_modes_dnd:2 (726072717827778234) --> + <string-array name="tile_states_modes_dnd"> + <item msgid="6509540227356524582">"No disponible"</item> + <item msgid="8589336868985358191">"Desactivado"</item> + <item msgid="726072717827778234">"Activado"</item> + </string-array> <string-array name="tile_states_flashlight"> <item msgid="3465257127433353857">"No disponible"</item> <item msgid="5044688398303285224">"Desactivado"</item> diff --git a/packages/SystemUI/res/values-et/strings.xml b/packages/SystemUI/res/values-et/strings.xml index 9d221715da7e..2412cfbef1f9 100644 --- a/packages/SystemUI/res/values-et/strings.xml +++ b/packages/SystemUI/res/values-et/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Uue seadme sidumiseks klõpsake"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Eelseadistust ei saanud värskendada"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Eelseadistus"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Kõnede vaikemikrofon"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Kuuldeaparaadi mikrofon"</item> + <item msgid="8501466270452446450">"Selle telefoni mikrofon"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Valitud"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Ümbritsevad helid"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Vasakule"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Sees – näopõhine"</string> <string name="inline_done_button" msgid="6043094985588909584">"Valmis"</string> <string name="inline_ok_button" msgid="603075490581280343">"Rakenda"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Lülita välja"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Hääletu"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Vaikeseade"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automaatne"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Parem ikoon"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Paanide lisamiseks hoidke all ja lohistage"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Paanide ümberpaigutamiseks hoidke neid all ja lohistage"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Lohistage eemaldamiseks siia"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Teil on vaja vähemalt <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> paani"</string> <string name="qs_edit" msgid="5583565172803472437">"Muutmine"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Muu"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"muutke paani suurust"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"paani eemaldamiseks"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"lisage paan viimasesse asukohta"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Teisalda paan"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Lisage paan soovitud asukohta"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Teadmata"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Kas lähtestada kõik paanid?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Kõik kiirseadete paanid lähtestatakse seadme algseadetele"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-eu/strings.xml b/packages/SystemUI/res/values-eu/strings.xml index 443632bb543a..1a960ec47e3c 100644 --- a/packages/SystemUI/res/values-eu/strings.xml +++ b/packages/SystemUI/res/values-eu/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Egin klik beste gailu bat parekatzeko"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Ezin izan da eguneratu aurrezarpena"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Aurrezarpena"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Deiak egiteko mikrofono lehenetsia"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Audifonoaren mikrofonoa"</item> + <item msgid="8501466270452446450">"Telefono honen mikrofonoa"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Hautatuta"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Ingurunea"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Ezkerrekoa"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Aktibatuta: aurpegian oinarrituta"</string> <string name="inline_done_button" msgid="6043094985588909584">"Eginda"</string> <string name="inline_ok_button" msgid="603075490581280343">"Aplikatu"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Desaktibatu"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Isila"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Lehenetsia"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automatikoa"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Eskuineko ikonoa"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Lauzak gehitzeko, eduki itzazu sakatuta, eta arrastatu"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Lauzak antolatzeko, eduki itzazu sakatuta, eta arrastatu"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Kentzeko, arrastatu hona"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"<xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> lauza behar dituzu gutxienez"</string> <string name="qs_edit" msgid="5583565172803472437">"Editatu"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Beste bat"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"aldatu lauzaren tamaina"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"kendu lauza"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"gehitu lauza azken posizioan"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Mugitu lauza"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Gehitu lauza nahi duzun posizioan"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Ezezagunak"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Lauza guztiak berrezarri nahi dituzu?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Gailuaren jatorrizko ezarpenak berrezarriko dira ezarpen bizkorren lauza guztietan"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-eu/tiles_states_strings.xml b/packages/SystemUI/res/values-eu/tiles_states_strings.xml index 923d84f23025..b37154e2d1e8 100644 --- a/packages/SystemUI/res/values-eu/tiles_states_strings.xml +++ b/packages/SystemUI/res/values-eu/tiles_states_strings.xml @@ -56,9 +56,11 @@ <item msgid="5376619709702103243">"Desaktibatuta"</item> <item msgid="4875147066469902392">"Aktibatuta"</item> </string-array> - <!-- no translation found for tile_states_modes_dnd:0 (6509540227356524582) --> - <!-- no translation found for tile_states_modes_dnd:1 (8589336868985358191) --> - <!-- no translation found for tile_states_modes_dnd:2 (726072717827778234) --> + <string-array name="tile_states_modes_dnd"> + <item msgid="6509540227356524582">"Ez dago erabilgarri"</item> + <item msgid="8589336868985358191">"Desaktibatuta"</item> + <item msgid="726072717827778234">"Aktibatuta"</item> + </string-array> <string-array name="tile_states_flashlight"> <item msgid="3465257127433353857">"Ez dago erabilgarri"</item> <item msgid="5044688398303285224">"Desaktibatuta"</item> diff --git a/packages/SystemUI/res/values-fa/strings.xml b/packages/SystemUI/res/values-fa/strings.xml index 58f35d1d8511..3b214ba71022 100644 --- a/packages/SystemUI/res/values-fa/strings.xml +++ b/packages/SystemUI/res/values-fa/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"برای جفت کردن دستگاه جدید، کلیک کنید"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"پیشتنظیم بهروزرسانی نشد"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"پیشتنظیم"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"میکروفون پیشفرض برای تماسها"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"میکروفون سمعک"</item> + <item msgid="8501466270452446450">"میکروفون این تلفن"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"انتخابشده"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"پیرامون"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"چپ"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"وقتی برنامهای را همرسانی میکنید، هر چیزی که در آن برنامه نمایش داده شود یا پخش شود برای <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> قابلمشاهده خواهد بود. درنتیجه مراقب چیزهایی مثل گذرواژهها، جزئیات پرداخت، پیامها، عکسها، و صدا و تصویر باشید."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"همرسانی صفحهنمایش"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"<xliff:g id="APP_NAME">%1$s</xliff:g>این گزینه را غیرفعال کرده است"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"توسط برنامه پشتیبانی نمیشود"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"برنامهای را برای همرسانی انتخاب کنید"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"محتوای صفحهنمایش شما پخش شود؟"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"پخش کردن محتوای یک برنامه"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"روشن - براساس چهره"</string> <string name="inline_done_button" msgid="6043094985588909584">"تمام"</string> <string name="inline_ok_button" msgid="603075490581280343">"اعمال"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"خاموش کردن"</string> <string name="notification_silence_title" msgid="8608090968400832335">"بیصدا"</string> <string name="notification_alert_title" msgid="3656229781017543655">"پیشفرض"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"خودکار"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"نماد راست"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"برای افزودن کاشی، نگه دارید و بکشید"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"برای تغییر دادن ترتیب کاشیها، آنها را نگه دارید و بکشید"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"برای حذف، به اینجا بکشید"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"حداقل به <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> کاشی نیاز دارید"</string> <string name="qs_edit" msgid="5583565172803472437">"ویرایش"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"موارد دیگر"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"تغییر اندازه کاشی"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"برداشتن کاشی"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"افزودن کاشی به آخرین جایگاه"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"انتقال کاشی"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"افزودن کاشی به جایگاه دلخواه"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"نامشخص"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"همه کاشیها بازنشانی شود؟"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"همه کاشیهای «تنظیمات فوری» به تنظیمات اصلی دستگاه بازنشانی خواهد شد"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>، <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-fi/strings.xml b/packages/SystemUI/res/values-fi/strings.xml index 1b767db0ca57..383830342480 100644 --- a/packages/SystemUI/res/values-fi/strings.xml +++ b/packages/SystemUI/res/values-fi/strings.xml @@ -421,10 +421,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Muodosta uusi laitepari klikkaamalla"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Esiasetusta ei voitu muuttaa"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Esiasetus"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Puhelujen oletusmikrofoni"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Kuulolaitteen mikrofoni"</item> + <item msgid="8501466270452446450">"Tämän puhelimen mikrofoni"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Valittu"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Ympäristö"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Vasen"</string> @@ -802,8 +803,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Päällä – kasvojen perusteella"</string> <string name="inline_done_button" msgid="6043094985588909584">"Valmis"</string> <string name="inline_ok_button" msgid="603075490581280343">"Käytä"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Laita pois päältä"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Äänetön"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Oletus"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automaattinen"</string> @@ -985,6 +985,8 @@ <string name="right_icon" msgid="1103955040645237425">"Oikea kuvake"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Lisää osioita koskettamalla pitkään ja vetämällä"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Järjestele koskettamalla pitkään ja vetämällä"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Poista vetämällä tähän."</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"<xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> kiekkoa on vähimmäismäärä"</string> <string name="qs_edit" msgid="5583565172803472437">"Muokkaa"</string> @@ -1005,6 +1007,10 @@ <string name="other" msgid="429768510980739978">"Muu"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"ruudun koko päälle/pois"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"poista kiekko"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"lisää laatta viimeiseen kohtaan"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Siirrä kiekkoa"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Lisää laatta haluttuun kohtaan"</string> @@ -1580,5 +1586,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Tuntematon"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Nollataanko kaikki laatat?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Kaikki pika-asetuslaatat palautetaan laitteen alkuperäisiin asetuksiin"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-fr-rCA/strings.xml b/packages/SystemUI/res/values-fr-rCA/strings.xml index 2a315e453cae..a6395b84cb0b 100644 --- a/packages/SystemUI/res/values-fr-rCA/strings.xml +++ b/packages/SystemUI/res/values-fr-rCA/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Cliquez ici pour associer un nouvel appareil"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Impossible de mettre à jour le préréglage"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Préréglage"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Microphone par défaut pour les appels"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Microphone pour prothèse auditive"</item> + <item msgid="8501466270452446450">"Le microphone de ce téléphone"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Sélectionné"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Environnement"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Gauche"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"Lorsque vous partagez une appli, tout ce qui s\'y affiche ou s\'y joue est visible par <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>. Par conséquent, soyez prudent avec les mots de passe, les détails du mode de paiement, les messages, les photos et les contenus audio et vidéo."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"Partager l\'écran"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"<xliff:g id="APP_NAME">%1$s</xliff:g> a désactivé cette option"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"Non pris en charge par l\'appli"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"Choisir l\'appli à partager"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"Diffuser votre écran?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"Diffuser une appli"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Activé : en fonction du visage"</string> <string name="inline_done_button" msgid="6043094985588909584">"Terminé"</string> <string name="inline_ok_button" msgid="603075490581280343">"Appliquer"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Désactiver"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Mode silencieux"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Par défaut"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automatique"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"Icône droite"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Sélectionnez et faites glisser les tuiles pour les ajouter"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Faites glisser les tuiles pour les réorganiser"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Faites glisser les tuiles ici pour les retirer"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Vous avez besoin d\'au moins <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> tuiles"</string> <string name="qs_edit" msgid="5583565172803472437">"Modifier"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"Autre"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"activer ou désactiver la taille de la tuile"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"retirer la tuile"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"Ajouter une tuile à la dernière position"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Déplacer la tuile"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Ajouter une tuile à la position désirée"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Inconnu"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Réinitialiser toutes les tuiles?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Toutes les tuiles des paramètres rapides seront réinitialisées aux paramètres par défaut de l\'appareil."</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-fr/strings.xml b/packages/SystemUI/res/values-fr/strings.xml index d7e9d446bec1..97181d6b338d 100644 --- a/packages/SystemUI/res/values-fr/strings.xml +++ b/packages/SystemUI/res/values-fr/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Cliquer pour associer un nouvel appareil"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Impossible de mettre à jour les préréglages"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Préréglage"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Micro par défaut pour les appels"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Micro de l\'appareil auditif"</item> + <item msgid="8501466270452446450">"Micro de ce téléphone"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Sélectionné"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Sons environnants"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Gauche"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Active - En fonction du visage"</string> <string name="inline_done_button" msgid="6043094985588909584">"OK"</string> <string name="inline_ok_button" msgid="603075490581280343">"Appliquer"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Désactiver"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Silencieux"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Par défaut"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automatique"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Icône droite"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Faites glisser les blocs pour les ajouter"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Faites glisser les blocs pour les réorganiser"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Faites glisser les blocs ici pour les supprimer"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Au minimum <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> tuiles sont nécessaires"</string> <string name="qs_edit" msgid="5583565172803472437">"Modifier"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Autre"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"activer/désactiver la taille du bloc"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"supprimer le bloc"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"ajouter le bloc à la dernière position"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Déplacer le bloc"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Ajouter le bloc à la position souhaitée"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Inconnu"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Réinitialiser tous les blocs ?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Tous les blocs \"Réglages rapides\" seront réinitialisés aux paramètres d\'origine de l\'appareil"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-gl/strings.xml b/packages/SystemUI/res/values-gl/strings.xml index f4a4ceb526dc..42f943f7f50b 100644 --- a/packages/SystemUI/res/values-gl/strings.xml +++ b/packages/SystemUI/res/values-gl/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Fai clic para vincular un novo dispositivo"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Non se puido actualizar a configuración predeterminada"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Configuración predeterminada"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Micrófono predefinido para as chamadas"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"O micrófono do audiófono"</item> + <item msgid="8501466270452446450">"O micrófono deste teléfono"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Elemento seleccionado"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Ambiente"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Esquerdo"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Activada: baseada na cara"</string> <string name="inline_done_button" msgid="6043094985588909584">"Feito"</string> <string name="inline_ok_button" msgid="603075490581280343">"Aplicar"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Desactivar"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Silenciadas"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Configuración predeterminada"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automática"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Icona á dereita"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Mantén premido e arrastra para engadir atallos"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Para reorganizar os atallos, mantenos premidos e arrástraos"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Arrastra o elemento ata aquí para quitalo"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Como mínimo ten que haber <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> mosaicos"</string> <string name="qs_edit" msgid="5583565172803472437">"Editar"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Outros"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"cambiar o tamaño do atallo"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"quitar atallo"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"engadir o atallo á última posición"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Mover atallo"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Engadir o atallo á última posición"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Categoría descoñecida"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Queres restablecer todos os atallos?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Restablecerase a configuración orixinal do dispositivo para todos os atallos de Configuración rápida"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-gu/strings.xml b/packages/SystemUI/res/values-gu/strings.xml index 2a8fe315647e..17b598198c7d 100644 --- a/packages/SystemUI/res/values-gu/strings.xml +++ b/packages/SystemUI/res/values-gu/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"નવા ડિવાઇસ સાથે જોડાણ કરવા માટે ક્લિક કરો"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"પ્રીસેટ અપડેટ કરી શક્યા નથી"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"પ્રીસેટ"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"કૉલ માટે ડિફૉલ્ટ માઇક્રોફોન"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"સાંભળવામાં મદદ આપતો માઇક્રોફોન"</item> + <item msgid="8501466270452446450">"આ ફોનનો માઇક્રોફોન"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"પસંદ કરી છે"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"આસપાસના અવાજો"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"ડાબે"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"ચાલુ છે - ચહેરા આધારિત રોટેશન"</string> <string name="inline_done_button" msgid="6043094985588909584">"થઈ ગયું"</string> <string name="inline_ok_button" msgid="603075490581280343">"લાગુ કરો"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"બંધ કરો"</string> <string name="notification_silence_title" msgid="8608090968400832335">"સાઇલન્ટ"</string> <string name="notification_alert_title" msgid="3656229781017543655">"ડિફૉલ્ટ"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"ઑટોમૅટિક રીતે"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"જમણું આઇકન"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"ટાઇલ ઉમેરવા માટે તેના પર આંગળી દબાવીને ખેંચો"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"ટાઇલને ફરીથી ગોઠવવા માટે આંગળી દબાવીને ખેંચો"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"દૂર કરવા માટે અહીં ખેંચો"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"તમને ઓછામાં ઓછી <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> ટાઇલની જરૂર છે"</string> <string name="qs_edit" msgid="5583565172803472437">"ફેરફાર કરો"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"અન્ય"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"ટાઇલના કદને ટૉગલ કરો"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"ટાઇલ કાઢી નાખો"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"છેલ્લા સ્થાનમાં ટાઇલ ઉમેરો"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"ટાઇલ ખસેડો"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"ઇચ્છિત સ્થાનમાં ટાઇલ ઉમેરો"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"અજાણ"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"તમામ ટાઇલ રીસેટ કરીએ?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"તમામ ઝડપી સેટિંગ ટાઇલને ડિવાઇસના ઑરિજિનલ સેટિંગ પર રીસેટ કરવામાં આવશે"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-hi/strings.xml b/packages/SystemUI/res/values-hi/strings.xml index 922794daa24b..0ce73c98f40c 100644 --- a/packages/SystemUI/res/values-hi/strings.xml +++ b/packages/SystemUI/res/values-hi/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"नया डिवाइस जोड़ने के लिए क्लिक करें"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"प्रीसेट अपडेट नहीं किया जा सका"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"प्रीसेट"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"कॉल के लिए डिफ़ॉल्ट माइक्रोफ़ोन"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"कान की मशीन के लिए माइक्रोफ़ोन"</item> + <item msgid="8501466270452446450">"इस फ़ोन का माइक्रोफ़ोन"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"चुना गया"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"आस-पास का वॉल्यूम"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"बाईं ओर के वॉल्यूम के लिए"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"चालू है - चेहरे की गतिविधि के हिसाब से कैमरे को घुमाने की सुविधा"</string> <string name="inline_done_button" msgid="6043094985588909584">"हो गया"</string> <string name="inline_ok_button" msgid="603075490581280343">"लागू करें"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"बंद करें"</string> <string name="notification_silence_title" msgid="8608090968400832335">"बिना आवाज़ के सूचनाएं दिखाएं"</string> <string name="notification_alert_title" msgid="3656229781017543655">"डिफ़ॉल्ट"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"अपने-आप"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"दायां आइकॉन"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"टाइल जोड़ने के लिए दबाकर खींचे और छोड़ें"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"टाइल का क्रम फिर से बदलने के लिए उन्हें दबाकर रखें और खींचें"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"हटाने के लिए यहां खींचें और छोड़ें"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"आपके पास कम से कम <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> टाइलें होनी चाहिए"</string> <string name="qs_edit" msgid="5583565172803472437">"बदलाव करें"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"अन्य"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"टाइल के साइज़ को टॉगल करने के लिए दो बार टैप करें"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"टाइल हटाएं"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"आखिरी जगह पर टाइल जोड़ें"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"टाइल को किसी और पोज़िशन पर ले जाएं"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"अपनी पसंदीदा जगह पर टाइल जोड़ें"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"कोई जानकारी नहीं है"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"क्या सभी टाइल रीसेट करनी हैं?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"क्विक सेटिंग टाइल, डिवाइस की ओरिजनल सेटिंग पर रीसेट हो जाएंगी"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-hr/strings.xml b/packages/SystemUI/res/values-hr/strings.xml index 3213d59dadf9..3b2d127010af 100644 --- a/packages/SystemUI/res/values-hr/strings.xml +++ b/packages/SystemUI/res/values-hr/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Kliknite da biste uparili novi uređaj"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Ažuriranje unaprijed definiranih postavki nije uspjelo"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Unaprijed definirana postavka"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Zadani mikrofon za pozive"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Mikrofon slušnog pomagala"</item> + <item msgid="8501466270452446450">"Mikrofon ovog telefona"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Odabrano"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Okruženje"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Lijevo"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Uključeno – na temelju lica"</string> <string name="inline_done_button" msgid="6043094985588909584">"Gotovo"</string> <string name="inline_ok_button" msgid="603075490581280343">"Primijeni"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Isključi"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Bešumno"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Zadano"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automatski"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Desna ikona"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Zadržite i povucite da biste dodali pločice"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Zadržite i povucite da biste premjestili pločice"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Povucite ovdje za uklanjanje"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Potrebno je barem <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> pločica"</string> <string name="qs_edit" msgid="5583565172803472437">"Uređivanje"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Ostalo"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"promjenu veličine pločice"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"uklanjanje kartice"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"dodavanje kartice na posljednji položaj"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Premještanje kartice"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Dodajte karticu na željeni položaj"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Nepoznato"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Želite li poništiti sve pločice?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Sve pločice Brze postavke vratit će se na izvorne postavke uređaja"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-hu/strings.xml b/packages/SystemUI/res/values-hu/strings.xml index 4327594260fc..b4042be66d9c 100644 --- a/packages/SystemUI/res/values-hu/strings.xml +++ b/packages/SystemUI/res/values-hu/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Kattintson új eszköz párosításához"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Nem sikerült frissíteni a beállításkészletet"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Beállításkészlet"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Alapértelmezett mikrofon a hívásokhoz"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Hallókészülék mikrofonja"</item> + <item msgid="8501466270452446450">"A telefon mikrofonja"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Kiválasztva"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Környezet"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Bal"</string> @@ -982,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Jobb oldali ikon"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Tartsa lenyomva, és húzza a mozaikok hozzáadásához"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Tartsa lenyomva, és húzza a mozaikok átrendezéséhez"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Húzza ide az eltávolításhoz"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Legalább <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> kártya szükséges"</string> <string name="qs_edit" msgid="5583565172803472437">"Szerkesztés"</string> @@ -1002,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Egyéb"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"a mozaik méretének módosítása"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"mozaik eltávolításához"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"mozaik hozzáadása az utolsó pozícióhoz"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Mozaik áthelyezése"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Mozaik hozzáadása a kívánt pozícióhoz"</string> @@ -1577,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Ismeretlen"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Visszaállítja az összes mozaikot?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Az összes Gyorsbeállítások mozaik visszaáll az eszköz eredeti beállításaira"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-hy/strings.xml b/packages/SystemUI/res/values-hy/strings.xml index eaff82423bd8..d606213161b8 100644 --- a/packages/SystemUI/res/values-hy/strings.xml +++ b/packages/SystemUI/res/values-hy/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Սեղմեք՝ նոր սարք զուգակցելու համար"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Չհաջողվեց թարմացնել կարգավորումների հավաքածուն"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Կարգավորումների հավաքածու"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Կանխադրված խոսափող զանգերի համար"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Լսողական սարքի խոսափող"</item> + <item msgid="8501466270452446450">"Այս հեռախոսի խոսափողը"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Ընտրված է"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Շրջակայք"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Ձախ"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Միաց․ – Դիմաճանաչման հիման վրա"</string> <string name="inline_done_button" msgid="6043094985588909584">"Փակել"</string> <string name="inline_ok_button" msgid="603075490581280343">"Կիրառել"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Անջատել"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Անձայն"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Կանխադրված"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Ավտոմատ"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Աջ պատկերակ"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Պահեք և քաշեք՝ սալիկներ ավելացնելու համար"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Պահեք և քաշեք՝ սալիկները վերադասավորելու համար"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Քաշեք այստեղ՝ հեռացնելու համար"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Հարկավոր է առնվազն <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> սալիկ"</string> <string name="qs_edit" msgid="5583565172803472437">"Փոփոխել"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Այլ"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"փոխեք սալիկի չափը"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"հեռացնել սալիկը"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"ավելացնել սալիկը վերջին դիրքում"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Տեղափոխել սալիկը"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Ավելացնել սալիկը նախընտրած դիրքում"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Անհայտ"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Զրոյացնե՞լ բոլոր սալիկները"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Արագ կարգավորումների բոլոր սալիկները կզրոյացվեն սարքի սկզբնական կարգավորումների համաձայն։"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-in/strings.xml b/packages/SystemUI/res/values-in/strings.xml index 763f76754b74..a117599c08cc 100644 --- a/packages/SystemUI/res/values-in/strings.xml +++ b/packages/SystemUI/res/values-in/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Klik untuk menyambungkan perangkat baru"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Tidak dapat memperbarui preset"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Preset"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Mikrofon default untuk panggilan"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Mikrofon alat bantu dengar"</item> + <item msgid="8501466270452446450">"Mikrofon ponsel ini"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Dipilih"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Suara sekitar"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Kiri"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Aktif - Berbasis deteksi wajah"</string> <string name="inline_done_button" msgid="6043094985588909584">"Selesai"</string> <string name="inline_ok_button" msgid="603075490581280343">"Terapkan"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Nonaktifkan"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Senyap"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Default"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Otomatis"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Ikon kanan"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Tahan dan tarik untuk menambahkan kartu"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Tahan dan tarik untuk menata ulang kartu"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Tarik ke sini untuk menghapus"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Anda membutuhkan setidaknya <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> kartu"</string> <string name="qs_edit" msgid="5583565172803472437">"Edit"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Lainnya"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"mengubah ukuran kartu"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"menghapus kartu"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"menambahkan kartu ke posisi terakhir"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Pindahkan kartu"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Tambahkan kartu ke posisi yang diinginkan"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Tidak diketahui"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Reset semua kartu?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Semua kartu Setelan Cepat akan direset ke setelan asli perangkat"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-is/strings.xml b/packages/SystemUI/res/values-is/strings.xml index 970a9130135a..bd8c7d1894f6 100644 --- a/packages/SystemUI/res/values-is/strings.xml +++ b/packages/SystemUI/res/values-is/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Smelltu til að para nýtt tæki"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Tókst ekki að uppfæra forstillingu"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Forstilling"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Sjálfgefinn hljóðnemi fyrir símtöl"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Hljóðnemi heyrnartækis"</item> + <item msgid="8501466270452446450">"Hljóðnemi þessa síma"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Valið"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Umhverfi"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Vinstri"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Kveikt – út frá andliti"</string> <string name="inline_done_button" msgid="6043094985588909584">"Lokið"</string> <string name="inline_ok_button" msgid="603075490581280343">"Nota"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Slökkva"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Hljóðlaust"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Sjálfgefið"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Sjálfvirk"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Tákn til hægri"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Haltu inni og dragðu til að bæta við flísum"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Haltu og dragðu til að endurraða flísum"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Dragðu hingað til að fjarlægja"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Flísarnar mega ekki vera færri en <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g>"</string> <string name="qs_edit" msgid="5583565172803472437">"Breyta"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Annað"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"breyta stærð reitsins"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"fjarlægja flís"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"bæta reit við síðustu stöðu"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Færa flís"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Bæta reit við óskaða stöðu"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Óþekkt"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Endurstilla alla reiti?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Allir flýtistillingareitir munu endurstillast á upprunalegar stillingar tækisins"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-it/strings.xml b/packages/SystemUI/res/values-it/strings.xml index 7323037cb00b..7d170a9a1980 100644 --- a/packages/SystemUI/res/values-it/strings.xml +++ b/packages/SystemUI/res/values-it/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Fai clic per accoppiare un nuovo dispositivo"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Impossibile aggiornare preset"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Preset"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Microfono predefinito per le chiamate"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Microfono dell\'apparecchio acustico"</item> + <item msgid="8501466270452446450">"Microfono di questo smartphone"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Selezionato"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Audio ambientale"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Sinistra"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"Quando condividi un\'app, tutto ciò che viene mostrato o riprodotto al suo interno è visibile a <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>. Presta quindi attenzione a password, dati di pagamento, messaggi, foto, audio e video."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"Condividi schermo"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"<xliff:g id="APP_NAME">%1$s</xliff:g> ha disattivato questa opzione"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"Non supportata dall\'app"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"Scegli l\'app da condividere"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"Trasmettere lo schermo?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"Trasmetti un\'app"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"On - Rotazione basata sul viso"</string> <string name="inline_done_button" msgid="6043094985588909584">"Fine"</string> <string name="inline_ok_button" msgid="603075490581280343">"Applica"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Disattiva"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Modalità silenziosa"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Modalità predefinita"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automatico"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"Icona destra"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Tieni premuto e trascina per aggiungere riquadri"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Tieni premuto e trascina per riordinare i riquadri"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Trascina qui per rimuovere"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Occorrono almeno <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> schede"</string> <string name="qs_edit" msgid="5583565172803472437">"Modifica"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"Altro"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"attivare/disattivare le dimensioni del riquadro"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"rimuovere il riquadro"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"aggiungere il riquadro all\'ultima posizione"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Sposta riquadro"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Aggiungi il riquadro alla posizione desiderata"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Sconosciuti"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Reimpostare tutti i riquadri?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Tutti i riquadri Impostazioni rapide verranno reimpostati sulle impostazioni originali del dispositivo"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-iw/strings.xml b/packages/SystemUI/res/values-iw/strings.xml index 3ee24a3d5a75..cf6d80acdeae 100644 --- a/packages/SystemUI/res/values-iw/strings.xml +++ b/packages/SystemUI/res/values-iw/strings.xml @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"פועל – מבוסס על זיהוי פנים"</string> <string name="inline_done_button" msgid="6043094985588909584">"סיום"</string> <string name="inline_ok_button" msgid="603075490581280343">"אישור"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"השבתה"</string> <string name="notification_silence_title" msgid="8608090968400832335">"שקט"</string> <string name="notification_alert_title" msgid="3656229781017543655">"ברירת מחדל"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"באופן אוטומטי"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"סמל ימני"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"יש ללחוץ ולגרור כדי להוסיף לחצנים"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"יש ללחוץ ולגרור כדי לסדר מחדש את כרטיסי המידע"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"אפשר לגרור לכאן כדי להסיר"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"יש צורך ב-<xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> אריחים לפחות"</string> <string name="qs_edit" msgid="5583565172803472437">"עריכה"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"אחר"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"שינוי גודל הלחצן"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"הסרת הלחצן"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"הוספת הלחצן במיקום האחרון"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"העברת הלחצן"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"הוספת הלחצן במיקום הרצוי"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"לא ידוע"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"לאפס את כל הלחצנים?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"כל הלחצנים ב\'הגדרות מהירות\' יאופסו להגדרות המקוריות של המכשיר"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-iw/tiles_states_strings.xml b/packages/SystemUI/res/values-iw/tiles_states_strings.xml index 42aa531186f6..4ff8fe4e6f80 100644 --- a/packages/SystemUI/res/values-iw/tiles_states_strings.xml +++ b/packages/SystemUI/res/values-iw/tiles_states_strings.xml @@ -56,9 +56,11 @@ <item msgid="5376619709702103243">"כבוי"</item> <item msgid="4875147066469902392">"פועל"</item> </string-array> - <!-- no translation found for tile_states_modes_dnd:0 (6509540227356524582) --> - <!-- no translation found for tile_states_modes_dnd:1 (8589336868985358191) --> - <!-- no translation found for tile_states_modes_dnd:2 (726072717827778234) --> + <string-array name="tile_states_modes_dnd"> + <item msgid="6509540227356524582">"לא זמין"</item> + <item msgid="8589336868985358191">"מושבת"</item> + <item msgid="726072717827778234">"מופעל"</item> + </string-array> <string-array name="tile_states_flashlight"> <item msgid="3465257127433353857">"לא זמין"</item> <item msgid="5044688398303285224">"כבוי"</item> diff --git a/packages/SystemUI/res/values-ja/strings.xml b/packages/SystemUI/res/values-ja/strings.xml index 1e85479ca0eb..ca4dfc88b6ba 100644 --- a/packages/SystemUI/res/values-ja/strings.xml +++ b/packages/SystemUI/res/values-ja/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"クリックすると、新しいデバイスをペア設定できます"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"プリセットを更新できませんでした"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"プリセット"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"通話に使うデフォルトのマイク"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"補聴器のマイク"</item> + <item msgid="8501466270452446450">"このスマートフォンのマイク"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"選択中"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"周囲の音"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"左"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"アプリを共有すると、そのアプリで表示または再生される内容が <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> にすべて公開されます。パスワード、お支払い情報、メッセージ、写真、音声、動画などの情報にご注意ください。"</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"画面を共有"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"<xliff:g id="APP_NAME">%1$s</xliff:g> がこのオプションを無効にしています"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"このアプリではサポートされていません"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"共有するアプリを選択"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"画面をキャストしますか?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"1 つのアプリをキャスト"</string> @@ -982,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"右アイコン"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"タイルを追加するには長押ししてドラッグ"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"タイルを並べ替えるには長押ししてドラッグ"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"削除するにはここにドラッグ"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"タイルは <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> 個以上必要です"</string> <string name="qs_edit" msgid="5583565172803472437">"編集"</string> @@ -1002,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"その他"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"タイルのサイズを切り替えます"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"タイルを削除"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"タイルを最後の位置に追加する"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"タイルを移動"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"タイルを目的の位置に追加する"</string> @@ -1577,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"不明"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"すべてのタイルをリセットしますか?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"すべてのクイック設定タイルがデバイスの元の設定にリセットされます"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>、<xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-ka/strings.xml b/packages/SystemUI/res/values-ka/strings.xml index 0a25f83d1185..42b9a3c7e605 100644 --- a/packages/SystemUI/res/values-ka/strings.xml +++ b/packages/SystemUI/res/values-ka/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"დააწკაპუნეთ ახალი მოწყობილობის დასაწყვილებლად"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"წინასწარ დაყენებული პარამეტრების განახლება ვერ მოხერხდა"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"წინასწარ დაყენებული"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"ნაგულისხმევი მიკროფონი ზარებისთვის"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"სმენის მოწყობილობის მიკროფონი"</item> + <item msgid="8501466270452446450">"ამ ტელეფონის მიკროფონი"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"არჩეულია"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"გარემოცვა"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"მარცხენა"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"აპის გაზიარებისას <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ხედავს ყველაფერს, რაც ჩანს ან უკრავს ამ აპში. ამიტომ სიფრთხილე გამოიჩინეთ ისეთ ინფორმაციასთან, როგორიცაა პაროლები, გადახდის დეტალები, შეტყობინებები, ფოტოები, აუდიო და ვიდეო."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"ეკრანის გაზიარება"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"<xliff:g id="APP_NAME">%1$s</xliff:g>-მა გათიშა ეს ვარიანტი"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"არ არის მხარდაჭერილი აპის მიერ"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"გაზიარებისთვის აპის არჩევა"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"გსურთ თქვენი ეკრანის ტრანსლირება?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"ერთი აპის ტრანსლირება"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"ჩართული — სახის მიხედვით"</string> <string name="inline_done_button" msgid="6043094985588909584">"მზადაა"</string> <string name="inline_ok_button" msgid="603075490581280343">"მისადაგება"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"გამორთვა"</string> <string name="notification_silence_title" msgid="8608090968400832335">"ჩუმი"</string> <string name="notification_alert_title" msgid="3656229781017543655">"ნაგულისხმევი"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"ავტომატური"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"მარჯვენა ხატულა"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"ჩავლებით გადაიტანეთ ბლოკების დასამატებლად"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"ფილების გადაწყობა შეგიძლიათ მათი ჩავლებით გადატანით"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"ამოსაშლელად, ჩავლებით გადმოიტანეთ აქ"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"თქვენ გჭირდებათ მოზაიკის <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> ფილა მაინც"</string> <string name="qs_edit" msgid="5583565172803472437">"რედაქტირება"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"სხვა"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"მოზაიკის ფილის ზომის გადასართავად"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"მოზაიკის ფილის წაშლა"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"მოზაიკის ფილის ბოლო პოზიციაზე დამატება"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"მოზაიკის გადატანა"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"მოზაიკის ფილის სასურველ პოზიციაზე დამატება"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"უცნობი"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"გსურთ ყველა ფილის გადაყენება?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"სწრაფი პარამეტრების ყველა ფილა გადაყენდება მოწყობილობის ორიგინალ პარამეტრებზე"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-kk/strings.xml b/packages/SystemUI/res/values-kk/strings.xml index c7e8d81f4e45..42d2874a10c7 100644 --- a/packages/SystemUI/res/values-kk/strings.xml +++ b/packages/SystemUI/res/values-kk/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Жаңа құрылғыны жұптау үшін басыңыз."</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Параметрлер жинағын жаңарту мүмкін болмады."</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Параметрлер жинағы"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Қоңырауларға арналған әдепкі микрофон"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Есту аппаратының микрофоны"</item> + <item msgid="8501466270452446450">"Осы телефонның микрофоны"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Таңдалды"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Айнала"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Сол жақ"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Қосулы – бет негізінде"</string> <string name="inline_done_button" msgid="6043094985588909584">"Дайын"</string> <string name="inline_ok_button" msgid="603075490581280343">"Қолдану"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Өшіру"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Үнсіз"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Әдепкі"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Автоматты"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Оң жақ белгіше"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Қажетті элементтерді сүйреп әкеліп қойыңыз"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Элементтердің ретін өзгерту үшін оларды басып тұрып сүйреңіз"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Керексіздерін осы жерге сүйреңіз"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Кемінде <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> бөлшек қажет."</string> <string name="qs_edit" msgid="5583565172803472437">"Өзгерту"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Басқа"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"бөлшек өлшемін ауыстыру"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"бөлшекті өшіру"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"соңғы позицияға бөлшек қосу"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Бөлшекті жылжыту"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Қалаған позицияға бөлшек қосу"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Белгісіз"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Барлық бөлшекті бастапқы күйге қайтару керек пе?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Барлық \"Жылдам параметрлер\" бөлшегі құрылғының бастапқы параметрлеріне қайтарылады."</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-km/strings.xml b/packages/SystemUI/res/values-km/strings.xml index 349e34a2c8cd..e85b81d62ff1 100644 --- a/packages/SystemUI/res/values-km/strings.xml +++ b/packages/SystemUI/res/values-km/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"ចុច ដើម្បីផ្គូផ្គងឧបករណ៍ថ្មី"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"មិនអាចប្ដូរការកំណត់ជាមុនបានទេ"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"កំណត់ជាមុន"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"មីក្រូហ្វូនលំនាំដើមសម្រាប់ការហៅទូរសព្ទ"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"មីក្រូហ្វូនដែលមានឧបករណ៍ជំនួយការស្ដាប់"</item> + <item msgid="8501466270452446450">"មីក្រូហ្វូនរបស់ទូរសព្ទនេះ"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"បានជ្រើសរើស"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"មជ្ឈដ្ឋានជុំវិញ"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"ឆ្វេង"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"នៅពេលអ្នកបង្ហាញកម្មវិធីណាមួយ <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> មើលឃើញអ្វីគ្រប់យ៉ាងដែលបង្ហាញ ឬចាក់ក្នុងកម្មវិធីនោះ។ ដូច្នេះ សូមប្រុងប្រយ័ត្នចំពោះអ្វីៗដូចជា ពាក្យសម្ងាត់ ព័ត៌មានលម្អិតអំពីការទូទាត់ប្រាក់ សារ រូបថត ព្រមទាំងសំឡេង និងវីដេអូ។"</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"បង្ហាញអេក្រង់"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"<xliff:g id="APP_NAME">%1$s</xliff:g> បានបិទជម្រើសនេះ"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"មិនអាចប្រើបានតាមកម្មវិធីនេះទេ"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"ជ្រើសរើសកម្មវិធីដើម្បីចែករំលែក"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"បញ្ជូនអេក្រង់របស់អ្នកឬ?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"បញ្ជូនកម្មវិធីមួយ"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"បើក - ផ្អែកលើមុខ"</string> <string name="inline_done_button" msgid="6043094985588909584">"រួចរាល់"</string> <string name="inline_ok_button" msgid="603075490581280343">"ប្រើ"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"បិទ"</string> <string name="notification_silence_title" msgid="8608090968400832335">"ស្ងាត់"</string> <string name="notification_alert_title" msgid="3656229781017543655">"លំនាំដើម"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"ស្វ័យប្រវត្តិ"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"រូបតំណាងខាងស្ដាំ"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"ចុចឱ្យជាប់ រួចអូសដើម្បីបញ្ចូលប្រអប់"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"ចុចឱ្យជាប់ រួចអូសដើម្បីរៀបចំប្រអប់ឡើងវិញ"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"អូសទីនេះដើម្បីយកចេញ"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"អ្នកត្រូវការប្រអប់យ៉ាងតិច <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g>"</string> <string name="qs_edit" msgid="5583565172803472437">"កែ"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"ផ្សេងៗ"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"បិទ/បើកទំហំរបស់ប្រអប់"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"ដកប្រអប់ចេញ"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"បញ្ចូលប្រអប់ទៅទីតាំងចុងក្រោយ"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"ផ្លាស់ទីប្រអប់"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"បញ្ចូលប្រអប់ទៅទីតាំងដែលចង់បាន"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"មិនស្គាល់"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"កំណត់ប្រអប់ទាំងអស់ឡើងវិញឬ?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"ប្រអប់ការកំណត់រហ័សទាំងអស់នឹងកំណត់ឡើងវិញទៅការកំណត់ដើមរបស់ឧបករណ៍"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-kn/strings.xml b/packages/SystemUI/res/values-kn/strings.xml index 0fa4c2476151..3bddcbd9f906 100644 --- a/packages/SystemUI/res/values-kn/strings.xml +++ b/packages/SystemUI/res/values-kn/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"ಹೊಸ ಸಾಧನವನ್ನು ಜೋಡಿಸಲು ಕ್ಲಿಕ್ ಮಾಡಿ"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"ಪ್ರಿಸೆಟ್ ಅನ್ನು ಅಪ್ಡೇಟ್ ಮಾಡಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"ಪ್ರಿಸೆಟ್"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"ಕರೆಗಳಿಗಾಗಿ ಡೀಫಾಲ್ಟ್ ಮೈಕ್ರೊಫೋನ್"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"ಶ್ರವಣ ಸಾಧನದ ಮೈಕ್ರೊಫೋನ್"</item> + <item msgid="8501466270452446450">"ಈ ಫೋನ್ನ ಮೈಕ್ರೊಫೋನ್"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"ಆಯ್ಕೆಮಾಡಲಾಗಿದೆ"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"ಆ್ಯಂಬಿಯೆಂಟ್ ವಾಲ್ಯೂಮ್"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"ಎಡ"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"ನೀವು ಆ್ಯಪ್ ಅನ್ನು ಹಂಚಿಕೊಳ್ಳುತ್ತಿರುವಾಗ, ಆ ಆ್ಯಪ್ನಲ್ಲಿ ತೋರಿಸಿರುವ ಅಥವಾ ಪ್ಲೇ ಮಾಡಿದ ಏನಾದರೂ <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ಗೆ ಗೋಚರಿಸುತ್ತದೆ. ಆದ್ದರಿಂದ ಪಾಸ್ವರ್ಡ್ಗಳು, ಪಾವತಿ ವಿವರಗಳು, ಸಂದೇಶಗಳು, ಫೋಟೋಗಳು ಮತ್ತು ಆಡಿಯೋ ಮತ್ತು ವೀಡಿಯೊದಂತಹ ವಿಷಯಗಳ ಬಗ್ಗೆ ಜಾಗರೂಕರಾಗಿರಿ."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"ಸ್ಕ್ರೀನ್ ಹಂಚಿಕೊಳ್ಳಿ"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"<xliff:g id="APP_NAME">%1$s</xliff:g> ಈ ಆಯ್ಕೆಯನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಿದೆ"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"ಆ್ಯಪ್ ಬೆಂಬಲಿಸುವುದಿಲ್ಲ"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"ಹಂಚಿಕೊಳ್ಳಲು ಆ್ಯಪ್ ಅನ್ನು ಆಯ್ಕೆಮಾಡಿ"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"ನಿಮ್ಮ ಸ್ಕ್ರೀನ್ ಅನ್ನು ಬಿತ್ತರಿಸಬೇಕೇ?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"ಒಂದು ಆ್ಯಪ್ ಅನ್ನು ಬಿತ್ತರಿಸಿ"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"ಆನ್ ಆಗಿದೆ - ಮುಖ-ಆಧಾರಿತ"</string> <string name="inline_done_button" msgid="6043094985588909584">"ಪೂರ್ಣಗೊಂಡಿದೆ"</string> <string name="inline_ok_button" msgid="603075490581280343">"ಅನ್ವಯಿಸಿ"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"ಆಫ್ ಮಾಡಿ"</string> <string name="notification_silence_title" msgid="8608090968400832335">"ನಿಶ್ಶಬ್ದ"</string> <string name="notification_alert_title" msgid="3656229781017543655">"ಡೀಫಾಲ್ಟ್"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"ಸ್ವಯಂಚಾಲಿತ"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"ಬಲ ಐಕಾನ್"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"ಟೈಲ್ಗಳನ್ನು ಸೇರಿಸಲು ಹೋಲ್ಡ್ ಮಾಡಿ ಮತ್ತು ಡ್ರ್ಯಾಗ್ ಮಾಡಿ"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"ಟೈಲ್ಗಳನ್ನು ಮರುಹೊಂದಿಸಲು ಹೋಲ್ಡ್ ಮಾಡಿ ಮತ್ತು ಡ್ರ್ಯಾಗ್ ಮಾಡಿ"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"ತೆಗೆದುಹಾಕಲು ಇಲ್ಲಿ ಡ್ರ್ಯಾಗ್ ಮಾಡಿ"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"ನಿಮಗೆ ಕನಿಷ್ಠ <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> ಟೈಲ್ಗಳ ಅಗತ್ಯವಿದೆ"</string> <string name="qs_edit" msgid="5583565172803472437">"ಎಡಿಟ್"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"ಇತರ"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"ಟೈಲ್ ನ ಗಾತ್ರವನ್ನು ಟಾಗಲ್ ಮಾಡಿ"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"ಟೈಲ್ ಅನ್ನು ತೆಗೆದುಹಾಕಿ"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"ಕೊನೆಯ ಸ್ಥಾನಕ್ಕೆ ಟೈಲ್ ಅನ್ನು ಸೇರಿಸಿ"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"ಟೈಲ್ ಸರಿಸಿ"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"ಬಯಸಿದ ಸ್ಥಾನಕ್ಕೆ ಟೈಲ್ ಅನ್ನು ಸೇರಿಸಿ"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"ಅಪರಿಚಿತ"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"ಎಲ್ಲಾ ಟೈಲ್ಗಳನ್ನು ರೀಸೆಟ್ ಮಾಡಬೇಕೆ?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"ಎಲ್ಲಾ ತ್ವರಿತ ಸೆಟ್ಟಿಂಗ್ಗಳ ಟೈಲ್ಗಳನ್ನು ಸಾಧನದ ಮೂಲ ಸೆಟ್ಟಿಂಗ್ಗಳಿಗೆ ರೀಸೆಟ್ ಮಾಡಲಾಗುತ್ತದೆ"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-ko/strings.xml b/packages/SystemUI/res/values-ko/strings.xml index 2b9fc89af8b4..970096ea9e5f 100644 --- a/packages/SystemUI/res/values-ko/strings.xml +++ b/packages/SystemUI/res/values-ko/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"새 기기와 페어링하려면 클릭하세요"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"사전 설정을 업데이트할 수 없음"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"미리 설정"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"통화용 기본 마이크"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"보청기 마이크"</item> + <item msgid="8501466270452446450">"이 휴대전화의 마이크"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"선택됨"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"주변 소리"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"왼쪽"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"켜짐 - 얼굴 기준"</string> <string name="inline_done_button" msgid="6043094985588909584">"완료"</string> <string name="inline_ok_button" msgid="603075490581280343">"적용"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"사용 중지"</string> <string name="notification_silence_title" msgid="8608090968400832335">"무음"</string> <string name="notification_alert_title" msgid="3656229781017543655">"기본값"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"자동"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"오른쪽 아이콘"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"길게 터치하고 드래그하여 타일 추가"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"길게 터치하고 드래그하여 타일 재정렬"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"여기로 드래그하여 삭제"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"<xliff:g id="MIN_NUM_TILES">%1$d</xliff:g>개 이상의 타일이 필요합니다."</string> <string name="qs_edit" msgid="5583565172803472437">"수정"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"기타"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"타일 크기 전환"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"타일 삭제"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"마지막 위치에 타일 추가"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"타일 이동"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"원하는 위치에 타일 추가"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"알 수 없음"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"모든 타일을 재설정하시겠습니까?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"모든 빠른 설정 타일이 기기의 원래 설정으로 재설정됩니다."</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-ky/strings.xml b/packages/SystemUI/res/values-ky/strings.xml index b1f6f484ed59..46c67e72b93a 100644 --- a/packages/SystemUI/res/values-ky/strings.xml +++ b/packages/SystemUI/res/values-ky/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Жаңы түзмөк кошуу үчүн басыңыз"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Алдын ала коюлган параметрлер жаңыртылган жок"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Алдын ала коюлган параметрлер"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Чалуулар үчүн демейки микрофон"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Угуу аппаратынын микрофону"</item> + <item msgid="8501466270452446450">"Ушул телефондун микрофону"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Тандалды"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Айланадагы үндөр"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Сол"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"Колдонмону бөлүшкөндө, андагы нерселер <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> колдонмосуна көрүнөт. Андыктан сырсөздөр, төлөмдүн чоо-жайы, билдирүүлөр, сүрөттөр, аудио жана видеолор менен этият болуңуз."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"Экранды бөлүшүү"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"<xliff:g id="APP_NAME">%1$s</xliff:g> бул параметрди өчүрүп койду"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"Колдонмодо иштебейт"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"Бөлүшүү үчүн колдонмо тандоо"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"Экранды башка түзмөккө чыгарасызбы?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"Бир колдонмону чыгаруу"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Күйүк – Жүздүн негизинде"</string> <string name="inline_done_button" msgid="6043094985588909584">"Бүттү"</string> <string name="inline_ok_button" msgid="603075490581280343">"Колдонуу"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Өчүрүү"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Үнсүз"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Демейки"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Автоматтык"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"¨Оңго¨ сүрөтчөсү"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Керектүү элементтерди сүйрөп келиңиз"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Иретин өзгөртүү үчүн кармап туруп, сүйрөңүз"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Өчүрүү үчүн бул жерге сүйрөңүз"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Сизге жок дегенде <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> мозаика керек"</string> <string name="qs_edit" msgid="5583565172803472437">"Түзөтүү"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"Башка"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"плитканын өлчөмүн которуштуруу"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"ыкчам баскычты өчүрүү"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"аягына карта кошуу"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Ыкчам баскычты жылдыруу"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Керектүү жерге карта кошуу"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Белгисиз"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Бардык параметрлерди кайра коесузбу?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Бардык ыкчам параметрлер түзмөктүн баштапкы маанилерине кайтарылат"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-lo/strings.xml b/packages/SystemUI/res/values-lo/strings.xml index c2a4e8fad79b..961cab32d18f 100644 --- a/packages/SystemUI/res/values-lo/strings.xml +++ b/packages/SystemUI/res/values-lo/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"ຄລິກເພື່ອຈັບຄູ່ອຸປະກອນໃໝ່"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"ບໍ່ສາມາດອັບເດດການຕັ້ງຄ່າລ່ວງໜ້າໄດ້"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"ຄ່າທີ່ກຳນົດລ່ວງໜ້າ"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"ໄມໂຄຣໂຟນເລີ່ມຕົ້ນສຳລັບການໂທ"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"ໄມໂຄຣໂຟນເຄື່ອງຊ່ວຍຟັງ"</item> + <item msgid="8501466270452446450">"ໄມໂຄຣໂຟນຂອງໂທລະສັບເຄື່ອງນີ້"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"ເລືອກແລ້ວ"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"ສຽງແວດລ້ອມ"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"ຊ້າຍ"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"ເມື່ອທ່ານແບ່ງປັນແອັບຂອງທ່ານ, ຄົນອື່ນຈະເບິ່ງເຫັນທຸກຢ່າງທີ່ສະແດງ ຫຼື ຫຼິ້ນໃນແອັບໃນ <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>. ດັ່ງນັ້ນ, ໃຫ້ລະມັດລະວັງສິ່ງຕ່າງໆ ເຊັ່ນ: ລະຫັດຜ່ານ, ລາຍລະອຽດການຈ່າຍເງິນ, ຂໍ້ຄວາມ, ຮູບພາບ, ພ້ອມທັງສຽງ ແລະ ວິດີໂອ."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"ແບ່ງປັນໜ້າຈໍ"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"<xliff:g id="APP_NAME">%1$s</xliff:g> ປິດການນຳໃຊ້ຕົວເລືອກນີ້ແລ້ວ"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"ບໍ່ຮອງຮັບໂດຍແອັບ"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"ເລືອກແອັບທີ່ຈະແບ່ງປັນ"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"ສົ່ງສັນຍານໜ້າຈໍຂອງທ່ານບໍ?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"ສົ່ງສັນຍານແອັບ 1 ລາຍການ"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"ເປີດ - ອ້າງອີງໃບໜ້າ"</string> <string name="inline_done_button" msgid="6043094985588909584">"ແລ້ວໆ"</string> <string name="inline_ok_button" msgid="603075490581280343">"ນຳໃຊ້"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"ປິດ"</string> <string name="notification_silence_title" msgid="8608090968400832335">"ປິດສຽງ"</string> <string name="notification_alert_title" msgid="3656229781017543655">"ຄ່າເລີ່ມຕົ້ນ"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"ອັດຕະໂນມັດ"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"ໄອຄອນຂວາ"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"ກົດຄ້າງໄວ້ແລ້ວລາກເພື່ອເພີ່ມຊ່ອງ"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"ກົດຄ້າງໄວ້ແລ້ວລາກເພື່ອຈັດຮຽງໃໝ່"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"ລາກມາບ່ອນນີ້ເພື່ອລຶບອອກ"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"ທ່ານຍຕ້ອງໃຊ້ຢ່າງໜ້ອຍ <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> ຊ່ອງ"</string> <string name="qs_edit" msgid="5583565172803472437">"ແກ້ໄຂ"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"ອື່ນໆ"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"ສະຫຼັບຂະໜາດຂອງແຜ່ນ"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"ລຶບແຜ່ນອອກ"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"ເພີ່ມແຜ່ນໃສ່ຕຳແໜ່ງສຸດທ້າຍ"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"ຍ້າຍແຜ່ນ"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"ເພີ່ມແຜ່ນໃສ່ຕຳແໜ່ງທີ່ຕ້ອງການ"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"ບໍ່ຮູ້ຈັກ"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"ຣີເຊັດແຜ່ນທັງໝົດບໍ?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"ແຜ່ນການຕັ້ງຄ່າດ່ວນທັງໝົດຈະຣີເຊັດເປັນການຕັ້ງຄ່າແບບເກົ່າຂອງອຸປະກອນ"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-lt/strings.xml b/packages/SystemUI/res/values-lt/strings.xml index 48ab51b46e57..46ef5cf78e61 100644 --- a/packages/SystemUI/res/values-lt/strings.xml +++ b/packages/SystemUI/res/values-lt/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Spustelėkite, kad susietumėte naują įrenginį"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Išankstinių nustatymų atnaujinti nepavyko"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Išankstiniai nustatymai"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Numatytasis mikrofonas skambučiams"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Klausos aparato mikrofonas"</item> + <item msgid="8501466270452446450">"Šio telefono mikrofonas"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Pasirinkta"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Aplinka"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Kairė"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Įjungta – pagal veidą"</string> <string name="inline_done_button" msgid="6043094985588909584">"Atlikta"</string> <string name="inline_ok_button" msgid="603075490581280343">"Taikyti"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Išjungti"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Tylūs"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Numatytasis"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automatinis"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Piktograma dešinėje"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Jei norite pridėti išklotinių, laikykite nuspaudę ir vilkite"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Norėdami pertvarkyti išklot., laikykite nuspaudę ir vilkite"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Vilkite čia, jei norite pašalinti"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Turi būti bent <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> išklotinės elem."</string> <string name="qs_edit" msgid="5583565172803472437">"Redaguoti"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Kita"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"perjungti išklotinės elemento dydį"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"pašalintumėte išklotinės elementą"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"pridėti išklotinės elementą paskutinėje pozicijoje"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Perkelti išklotinės elementą"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Pridėti išklotinės elementą norimoje pozicijoje"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Nežinoma"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Iš naujo nustatyti visus išklotines elementus?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Visi sparčiųjų nustatymų išklotinės elementai bus iš naujo nustatyti į pradinius įrenginio nustatymus"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-lv/strings.xml b/packages/SystemUI/res/values-lv/strings.xml index e5c407f30b6c..5c052d92f220 100644 --- a/packages/SystemUI/res/values-lv/strings.xml +++ b/packages/SystemUI/res/values-lv/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Noklikšķiniet, lai savienotu pārī jaunu ierīci"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Nevarēja atjaunināt pirmsiestatījumu"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Pirmsiestatījums"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Noklusējuma mikrofons zvaniem"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Dzirdes aparāta mikrofons"</item> + <item msgid="8501466270452446450">"Šī tālruņa mikrofons"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Atlasīts"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Apkārtnes skaņas"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Pa kreisi"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Ieslēgta — ar sejas noteikšanu"</string> <string name="inline_done_button" msgid="6043094985588909584">"Gatavs"</string> <string name="inline_ok_button" msgid="603075490581280343">"Lietot"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Izslēgt"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Klusums"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Noklusējums"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automātiski"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Ikona labajā pusē"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Lai pievienotu elementus, turiet un velciet tos"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Lai pārkārtotu elementus, turiet un velciet tos"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Lai noņemtu vienumus, velciet tos šeit."</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Nepieciešami vismaz <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> elementi"</string> <string name="qs_edit" msgid="5583565172803472437">"Rediģēt"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Citi"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"pārslēgt elementa lielumu"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"noņemt elementu"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"pievienotu elementu pēdējā pozīcijā"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Pārvietot elementu"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Pievienot elementu vēlamajā pozīcijā"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Nezināma"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Vai atiestatīt visus elementus?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Visiem ātro iestatījumu elementiem tiks atiestatīti sākotnējie iestatījumi"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-mk/strings.xml b/packages/SystemUI/res/values-mk/strings.xml index e546fb584ed3..b8ed62f46a15 100644 --- a/packages/SystemUI/res/values-mk/strings.xml +++ b/packages/SystemUI/res/values-mk/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Кликнете за да спарите нов уред"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Не можеше да се ажурира зададената вредност"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Зададени вредности"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Стандарден микрофон за повици"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Микрофон на слушно помагало"</item> + <item msgid="8501466270452446450">"Микрофонот на овој телефон"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Избрано"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Опкружување"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Лево"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"Додека споделувате апликација, сѐ што се прикажува или пушта на таа апликација е видливо за <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>. Затоа, бидете внимателни со работи како лозинки, детали за плаќање, пораки, фотографии и аудио и видео."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"Сподели екран"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"<xliff:g id="APP_NAME">%1$s</xliff:g> ја оневозможи опцијава"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"Не е поддржано од апликацијата"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"Изберете апликација за споделување"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"Да се емитува вашиот екран?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"Емитувајте една апликација"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Вклучено - според лице"</string> <string name="inline_done_button" msgid="6043094985588909584">"Готово"</string> <string name="inline_ok_button" msgid="603075490581280343">"Примени"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Исклучи"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Безгласно"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Стандардно"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Автоматски"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"Десна икона"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Задржете и влечете за да додадете плочки"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Задржете и влечете за да ги преуредите плочките"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Повлечете тука за да се отстрани"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Потребни ви се најмалку <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> плочки"</string> <string name="qs_edit" msgid="5583565172803472437">"Измени"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"Друго"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"вклучување/исклучување на големината на плочката"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"отстранување на плочката"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"додајте плочка на последната позиција"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Преместување на плочката"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Додајте плочка на саканата позиција"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Непознато"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Да се ресетираат сите плочки?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Сите плочки на „Брзи поставки“ ќе се ресетираат на првичните поставки на уредот"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-ml/strings.xml b/packages/SystemUI/res/values-ml/strings.xml index 17b1c3d9d49c..4b21fa28f697 100644 --- a/packages/SystemUI/res/values-ml/strings.xml +++ b/packages/SystemUI/res/values-ml/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"പുതിയ ഉപകരണം ജോടിയാക്കാൻ ക്ലിക്ക് ചെയ്യുക"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"പ്രീസെറ്റ് അപ്ഡേറ്റ് ചെയ്യാനായില്ല"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"പ്രീസെറ്റ്"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"കോളുകൾക്കുള്ള ഡിഫോൾട്ട് മൈക്രോഫോൺ"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"ശ്രവണ സഹായി മൈക്രോഫോൺ"</item> + <item msgid="8501466270452446450">"ഈ ഫോണിന്റെ മൈക്രോഫോൺ"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"തിരഞ്ഞെടുത്തു"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"സറൗണ്ടിംഗ്സ്"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"ഇടത്"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"നിങ്ങളുടെ ആപ്പ് പങ്കിടുമ്പോൾ, ആ ആപ്പിൽ കാണിക്കുന്നതോ പ്ലേ ചെയ്യുന്നതോ ആയ എല്ലാ കാര്യങ്ങളും <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> എന്നതിന് ദൃശ്യമാകും. അതിനാൽ പാസ്വേഡുകൾ, പേയ്മെന്റ് വിശദാംശങ്ങൾ, സന്ദേശങ്ങൾ, ഫോട്ടോകൾ, ഓഡിയോ, വീഡിയോ എന്നിവ പോലുള്ള കാര്യങ്ങളിൽ ശ്രദ്ധ പുലർത്തുക."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"സ്ക്രീൻ പങ്കിടുക"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"<xliff:g id="APP_NAME">%1$s</xliff:g> ഈ ഓപ്ഷൻ പ്രവർത്തനരഹിതമാക്കി"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"ആപ്പ് പിന്തുണയ്ക്കുന്നില്ല"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"പങ്കിടാൻ ആപ്പ് തിരഞ്ഞെടുക്കുക"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"നിങ്ങളുടെ സ്ക്രീൻ കാസ്റ്റ് ചെയ്യണോ?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"ഒരു ആപ്പ് കാസ്റ്റ് ചെയ്യുക"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"ഓണാണ് - ഫേസ് ബേസ്ഡ്"</string> <string name="inline_done_button" msgid="6043094985588909584">"പൂർത്തിയായി"</string> <string name="inline_ok_button" msgid="603075490581280343">"ബാധകമാക്കുക"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"ഓഫാക്കുക"</string> <string name="notification_silence_title" msgid="8608090968400832335">"നിശബ്ദം"</string> <string name="notification_alert_title" msgid="3656229781017543655">"ഡിഫോൾട്ട്"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"സ്വയമേവ"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"വലതുവശത്തെ ചിഹ്നം"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"ടൈലുകൾ ചേർക്കാൻ അമർത്തിപ്പിടിച്ച് വലിച്ചിടുക"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"ടൈലുകൾ പുനഃക്രമീകരിക്കാൻ അമർത്തിപ്പിടിച്ച് വലിച്ചിടുക"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"നീക്കംചെയ്യുന്നതിന് ഇവിടെ വലിച്ചിടുക"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"നിങ്ങൾക്ക് ചുരുങ്ങിയത് <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> ടൈലുകളെങ്കിലും വേണം"</string> <string name="qs_edit" msgid="5583565172803472437">"എഡിറ്റ് ചെയ്യുക"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"മറ്റുള്ളവ"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"ടൈലിന്റെ വലുപ്പം മാറ്റുക"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"ടൈൽ നീക്കം ചെയ്യുക"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"അവസാന ഭാഗത്ത് ടൈൽ ചേർക്കുക"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"ടൈൽ നീക്കുക"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"ഇഷ്ടമുള്ള ഭാഗത്ത് ടൈൽ ചേർക്കുക"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"അജ്ഞാതം"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"എല്ലാ ടൈലുകളും റീസെറ്റ് ചെയ്യണോ?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"എല്ലാ ദ്രുത ക്രമീകരണ ടൈലുകളും ഉപകരണത്തിന്റെ ഒറിജിനൽ ക്രമീകരണത്തിലേക്ക് റീസെറ്റ് ചെയ്യും"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-mn/strings.xml b/packages/SystemUI/res/values-mn/strings.xml index ba7baf4ae5b0..abdf33125e6c 100644 --- a/packages/SystemUI/res/values-mn/strings.xml +++ b/packages/SystemUI/res/values-mn/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Шинэ төхөөрөмж хослуулахын тулд товшино уу"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Урьдчилсан тохируулгыг шинэчилж чадсангүй"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Урьдчилсан тохируулга"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Дуудлагад ашиглах өгөгдмөл микрофон"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Сонсголын төхөөрөмжийн микрофон"</item> + <item msgid="8501466270452446450">"Энэ утасны микрофон"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Сонгосон"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Орчин тойрон"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Зүүн"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Асаалттай - Царайнд суурилсан"</string> <string name="inline_done_button" msgid="6043094985588909584">"Болсон"</string> <string name="inline_ok_button" msgid="603075490581280343">"Ашиглах"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Унтраах"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Чимээгүй"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Өгөгдмөл"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Автомат"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Баруун дүрс тэмдэг"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Хавтан нэмэхийн тулд дараад чирэх"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Хавтангуудыг дахин цэгцлэхийн тулд дараад чирнэ үү"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Устгахын тулд энд зөөнө үү"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Танд хамгийн багадаа <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> хавтан шаардлагатай"</string> <string name="qs_edit" msgid="5583565172803472437">"Засах"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Бусад"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"хавтангийн хэмжээг асаах/унтраах"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"хавтанг хасна уу"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"хавтанг сүүлийн байрлалд нэмэх"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Хавтанг зөөх"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Хавтанг хүссэн байрлалд нэмэх"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Тодорхойгүй"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Бүх хавтанг шинэчлэх үү?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Шуурхай тохиргооны бүх хавтан төхөөрөмжийн эх тохиргоо руу шинэчлэгдэнэ"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-mr/strings.xml b/packages/SystemUI/res/values-mr/strings.xml index d6d8e1cfa70d..bc80b33b312c 100644 --- a/packages/SystemUI/res/values-mr/strings.xml +++ b/packages/SystemUI/res/values-mr/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"नवीन डिव्हाइस पेअर करण्यासाठी क्लिक करा"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"प्रीसेट अपडेट करता आले नाही"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"प्रीसेट"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"कॉलसाठी डीफॉल्ट मायक्रोफोन"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"श्रवणयंत्र मायक्रोफोन"</item> + <item msgid="8501466270452446450">"या फोनचा मायक्रोफोन"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"निवडला आहे"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"जवळपासचे"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"डावे"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"तुम्ही अॅप शेअर करता, तेव्हा त्या अॅपमध्ये दाखवल्या किंवा प्ले होणाऱ्या कोणत्याही गोष्टी <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> साठी दृश्यमान असतात. त्यामुळे पासवर्ड, पेमेंट तपशील, मेसेज, फोटो आणि ऑडिओ व व्हिडिओ यांसारख्या गोष्टींबाबत सावधगिरी बाळगा."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"स्क्रीन शेअर करा"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"<xliff:g id="APP_NAME">%1$s</xliff:g> हा पर्याय बंद केला आहे"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"ॲपद्वारे सपोर्ट नाही"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"शेअर करण्यासाठी अॅप निवडा"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"तुमची स्क्रीन कास्ट करायची आहे का?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"एक अॅप कास्ट करा"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"सुरू - चेहऱ्यावर आधारित"</string> <string name="inline_done_button" msgid="6043094985588909584">"पूर्ण झाले"</string> <string name="inline_ok_button" msgid="603075490581280343">"लागू करा"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"बंद करा"</string> <string name="notification_silence_title" msgid="8608090968400832335">"सायलंट"</string> <string name="notification_alert_title" msgid="3656229781017543655">"डीफॉल्ट"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"ऑटोमॅटिक"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"उजवे आयकन"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"टाइल जोडण्यासाठी धरून ठेवून ड्रॅग करा"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"टाइलची पुनर्रचना करण्यासाठी धरून ठेवून ड्रॅग करा"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"काढण्यासाठी येथे ड्रॅग करा"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"तुम्हाला किमान <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> टाइलची गरज आहे"</string> <string name="qs_edit" msgid="5583565172803472437">"संपादित करा"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"अन्य"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"टाइलचा आकार टॉगल करा"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"टाइल काढून टाका"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"टाइल शेवटच्या स्थानावर जोडा"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"टाइल हलवा"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"हव्या असलेल्या स्थानावर टाइल जोडा"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"अज्ञात"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"सर्व टाइल रीसेट करायच्या?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"सर्व क्विक सेटिंग्ज टाइल डिव्हाइसच्या मूळ सेटिंग्जवर रीसेट केल्या जातील"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-ms/strings.xml b/packages/SystemUI/res/values-ms/strings.xml index 2aa5068557d6..14c7197aa9e4 100644 --- a/packages/SystemUI/res/values-ms/strings.xml +++ b/packages/SystemUI/res/values-ms/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Klik untuk menggandingkan peranti baharu"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Tidak dapat mengemaskinikan pratetapan"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Pratetapan"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Mikrofon lalai untuk panggilan"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Mikrofon alat bantu pendengaran"</item> + <item msgid="8501466270452446450">"Mikrofon telefon ini"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Dipilih"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Persekitaran"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Kiri"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"Apabila anda berkongsi apl, apa-apa sahaja kandungan yang dipaparkan atau dimainkan dalam apl tersebut boleh dilihat oleh <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>. Oleh hal yang demikian, berhati-hati dengan perkara seperti kata laluan, butiran pembayaran, mesej, foto dan audio serta video."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"Kongsi skrin"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"<xliff:g id="APP_NAME">%1$s</xliff:g> telah melumpuhkan pilihan ini"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"Tidak disokong oleh apl"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"Pilih apl untuk dikongsi"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"Hantar skrin anda?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"Hantar satu apl"</string> @@ -982,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"Ikon kanan"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Tahan dan seret untuk menambahkan jubin"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Tahan dan seret untuk mengatur semula jubin"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Seret ke sini untuk mengalih keluar"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Anda memerlukan sekurang-kurangnya <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> jubin"</string> <string name="qs_edit" msgid="5583565172803472437">"Edit"</string> @@ -1002,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"Lain-lain"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"togol saiz jubin"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"alih keluar jubin"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"tambahkan jubin pada kedudukan terakhir"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Alihkan jubin"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Tambahkan jubin pada kedudukan yang dikehendaki"</string> @@ -1577,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Tidak diketahui"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Tetapkan semula semua jubin?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Semua jubin Tetapan Pantas akan ditetapkan semula kepada tetapan asal peranti"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-my/strings.xml b/packages/SystemUI/res/values-my/strings.xml index 70f8259df0a7..9849dd9d6219 100644 --- a/packages/SystemUI/res/values-my/strings.xml +++ b/packages/SystemUI/res/values-my/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"စက်အသစ် တွဲချိတ်ရန် နှိပ်ပါ"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"အသင့်သုံးကို အပ်ဒိတ်လုပ်၍မရပါ"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"ကြိုတင်သတ်မှတ်ချက်"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"ခေါ်ဆိုမှုများအတွက် မူရင်းမိုက်ခရိုဖုန်း"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"နားကြားကိရိယာ မိုက်ခရိုဖုန်း"</item> + <item msgid="8501466270452446450">"ဤဖုန်း၏ မိုက်ခရိုဖုန်း"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"ရွေးထားသည်"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"ဝန်းကျင်အသံ"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"ဘယ်"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"ဖွင့် - မျက်နှာအခြေခံ"</string> <string name="inline_done_button" msgid="6043094985588909584">"ပြီးပြီ"</string> <string name="inline_ok_button" msgid="603075490581280343">"အသုံးပြုရန်"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"ပိတ်ရန်"</string> <string name="notification_silence_title" msgid="8608090968400832335">"အသံပိတ်ရန်"</string> <string name="notification_alert_title" msgid="3656229781017543655">"မူလ"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"အလိုအလျောက်"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"လက်ယာသင်္ကေတ"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"အကွက်များထည့်ရန် ဖိဆွဲပါ"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"အကွက်ငယ်များ ပြန်စီစဉ်ရန် ဖိပြီးဆွဲပါ"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"ဖယ်ရှားရန် ဤနေရာသို့ဖိဆွဲပါ"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"အနည်းဆုံး <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> ကွက် ရှိရမည်"</string> <string name="qs_edit" msgid="5583565172803472437">"တည်းဖြတ်ရန်"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"အခြား"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"အကွက်ငယ်၏ အရွယ်အစားကို ပြောင်းရန်"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"အကွက်ငယ်ကို ဖယ်ရှားရန်"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"နောက်ဆုံးနေရာတွင် အကွက်ငယ် ထည့်ရန်"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"အကွက်ငယ်ကို ရွှေ့ရန်"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"ဆန္ဒရှိရာ နေရာတွင် အကွက်ငယ် ထည့်ရန်"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"အမျိုးအမည်မသိ"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"အကွက်ငယ်အားလုံးကို ပြင်ဆင်သတ်မှတ်မလား။"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"အမြန်ဆက်တင်များ အကွက်ငယ်အားလုံးကို စက်ပစ္စည်း၏ မူရင်းဆက်တင်များသို့ ပြင်ဆင်သတ်မှတ်ပါမည်"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>၊ <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-nb/strings.xml b/packages/SystemUI/res/values-nb/strings.xml index 82aeb65157d3..62c018bf68d9 100644 --- a/packages/SystemUI/res/values-nb/strings.xml +++ b/packages/SystemUI/res/values-nb/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Klikk for å koble til en ny enhet"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Kunne ikke oppdatere forhåndsinnstillingen"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Forhåndsinnstilling"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Standardmikrofonen for anrop"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Mikrofonen i høreapparatet"</item> + <item msgid="8501466270452446450">"Mikrofonen i denne telefonen"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Valgt"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Omgivelser"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Venstre"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"På – ansiktsbasert"</string> <string name="inline_done_button" msgid="6043094985588909584">"Ferdig"</string> <string name="inline_ok_button" msgid="603075490581280343">"Bruk"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Slå av"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Lydløs"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Standard"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automatisk"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Høyre-ikon"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Hold og dra for å legge til brikker"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Hold og dra for å flytte på rutene"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Dra hit for å fjerne"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Du trenger minst <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> infobrikker"</string> <string name="qs_edit" msgid="5583565172803472437">"Endre"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Annet"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"bytt størrelse på brikken"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"fjerne infobrikken"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"legge til en brikke på den siste posisjonen"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Flytt infobrikken"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Legg til brikken på ønsket posisjon"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Ukjent"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Vil du tilbakestille alle brikkene?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Alle brikker for hurtiginnstillinger tilbakestilles til enhetens opprinnelige innstillinger"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-ne/strings.xml b/packages/SystemUI/res/values-ne/strings.xml index 78164a250f42..c10163883409 100644 --- a/packages/SystemUI/res/values-ne/strings.xml +++ b/packages/SystemUI/res/values-ne/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"नयाँ डिभाइसमा कनेक्ट गर्न क्लिक गर्नुहोस्"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"प्रिसेट अपडेट गर्न सकिएन"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"पूर्वनिर्धारित"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"कलका लागि प्रयोग गरिने डिफल्ट माइक्रोफोन"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"श्रवण यन्त्रको माइक्रोफोन"</item> + <item msgid="8501466270452446450">"यो फोनको माइक्रोफोन"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"चयन गरिएको छ"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"वरपरका आवाज"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"बायाँ"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"अन छ - अनुहारमा आधारित"</string> <string name="inline_done_button" msgid="6043094985588909584">"सम्पन्न भयो"</string> <string name="inline_ok_button" msgid="603075490581280343">"लागू गर्नुहोस्"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"अफ गर्नुहोस्"</string> <string name="notification_silence_title" msgid="8608090968400832335">"साइलेन्ट"</string> <string name="notification_alert_title" msgid="3656229781017543655">"डिफल्ट"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"स्वचालित"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"दायाँतिरको आइकन"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"टाइलहरू थप्न होल्ड गरी ड्र्याग गर्नुहोस्"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"टाइलहरू पुनः क्रमबद्ध गर्न होल्ड गरी ड्र्याग गर्नुहोस्"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"हटाउनका लागि यहाँ तान्नुहोस्"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"तपाईंलाई कम्तीमा <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> वटा टाइल चाहिन्छ"</string> <string name="qs_edit" msgid="5583565172803472437">"सम्पादन गर्नुहोस्"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"अन्य"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"टाइलको आकार टगल गर्नुहोस्"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"टाइल हटाउनुहोस्"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"अन्तिम स्थानमा टाइल हाल्नुहोस्"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"टाइल सार्नुहोस्"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"आफूले चाहेको स्थानमा टाइल हाल्नुहोस्"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"अज्ञात"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"सबै टाइलहरू रिसेट गर्ने हो?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"द्रुत सेटिङका सबै टाइलहरू रिसेट गरी डिभाइसका मूल सेटिङ लागू गरिने छन्"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml index 85182a02faaf..3dd01996afb8 100644 --- a/packages/SystemUI/res/values-night/colors.xml +++ b/packages/SystemUI/res/values-night/colors.xml @@ -76,6 +76,16 @@ <color name="media_dialog_seekbar_progress">@color/material_dynamic_secondary40</color> <color name="media_dialog_button_background">@color/material_dynamic_primary70</color> <color name="media_dialog_solid_button_text">@color/material_dynamic_secondary20</color> + <color name="media_dialog_primary">@android:color/system_primary_dark</color> + <color name="media_dialog_on_primary">@android:color/system_on_primary_dark</color> + <color name="media_dialog_secondary">@android:color/system_secondary_dark</color> + <color name="media_dialog_secondary_container">@android:color/system_secondary_container_dark</color> + <color name="media_dialog_surface_container">@android:color/system_surface_container_dark</color> + <color name="media_dialog_surface_container_high">@android:color/system_surface_container_high_dark</color> + <color name="media_dialog_on_surface">@android:color/system_on_surface_dark</color> + <color name="media_dialog_on_surface_variant">@android:color/system_on_surface_variant_dark</color> + <color name="media_dialog_outline">@android:color/system_outline_dark</color> + <color name="media_dialog_outline_variant">@android:color/system_outline_variant_dark</color> <!-- Biometric dialog colors --> <color name="biometric_dialog_gray">#ffcccccc</color> diff --git a/packages/SystemUI/res/values-nl/strings.xml b/packages/SystemUI/res/values-nl/strings.xml index fe6f34814279..82d29de64116 100644 --- a/packages/SystemUI/res/values-nl/strings.xml +++ b/packages/SystemUI/res/values-nl/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Klik om nieuw apparaat te koppelen"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Kan voorinstelling niet updaten"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Voorinstelling"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Standaardmicrofoon voor gesprekken"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Microfoon voor hoortoestel"</item> + <item msgid="8501466270452446450">"Microfoon van deze telefoon"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Geselecteerd"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Omgevingsgeluid"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Links"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Aan: op basis van gezicht"</string> <string name="inline_done_button" msgid="6043094985588909584">"Klaar"</string> <string name="inline_ok_button" msgid="603075490581280343">"Toepassen"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Uitzetten"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Stil"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Standaard"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automatisch"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Icoon rechts"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Houd een tegel ingedrukt en sleep om die toe te voegen"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Houd een tegel ingedrukt en sleep om die te verplaatsen"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Sleep hier naartoe om te verwijderen"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Je hebt minimaal <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> tegels nodig"</string> <string name="qs_edit" msgid="5583565172803472437">"Bewerken"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Overig"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"het formaat van de tegel schakelen"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"tegel verwijderen"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"tegel toevoegen op de laatste positie"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Tegel verplaatsen"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Tegel toevoegen op gewenste positie"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Onbekend"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Alle tegels resetten?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Alle tegels voor Snelle instellingen worden teruggezet naar de oorspronkelijke instellingen van het apparaat"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-or/strings.xml b/packages/SystemUI/res/values-or/strings.xml index fa16f28f15a8..a93508f2a3f3 100644 --- a/packages/SystemUI/res/values-or/strings.xml +++ b/packages/SystemUI/res/values-or/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"ନୂଆ ଡିଭାଇସ ପେୟାର କରିବାକୁ କ୍ଲିକ କରନ୍ତୁ"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"ପ୍ରିସେଟକୁ ଅପଡେଟ କରାଯାଇପାରିଲା ନାହିଁ"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"ପ୍ରିସେଟ"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"କଲ ପାଇଁ ଡିଫଲ୍ଟ ମାଇକ୍ରୋଫୋନ"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"ଶ୍ରବଣ ଯନ୍ତ୍ରର ମାଇକ୍ରୋଫୋନ"</item> + <item msgid="8501466270452446450">"ଏହି ଫୋନର ମାଇକ୍ରୋଫୋନ"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"ଚୟନ କରାଯାଇଛି"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"ପରିପାର୍ଶ୍ୱ"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"ବାମ"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"ଆପଣ ଏକ ଆପ ସେୟାର କରିବା ସମୟରେ, ସେହି ଆପରେ ଦେଖାଯାଉଥିବା କିମ୍ବା ପ୍ଲେ ହେଉଥିବା ସବୁକିଛି <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>କୁ ଦେଖାଯାଏ। ତେଣୁ ପାସୱାର୍ଡ, ପେମେଣ୍ଟ ବିବରଣୀ, ମେସେଜ, ଫଟୋ ଏବଂ ଅଡିଓ ଓ ଭିଡିଓ ପରି ବିଷୟଗୁଡ଼ିକ ପ୍ରତି ସତର୍କ ରୁହନ୍ତୁ।"</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"ସ୍କ୍ରିନ ସେୟାର କରନ୍ତୁ"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"<xliff:g id="APP_NAME">%1$s</xliff:g> ଏହି ବିକଳ୍ପକୁ ଅକ୍ଷମ କରିଛି"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"ଆପରେ ସପୋର୍ଟ କରୁନାହିଁ"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"ସେୟାର କରିବାକୁ ଆପ ବାଛନ୍ତୁ"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"ଆପଣଙ୍କ ସ୍କ୍ରିନକୁ କାଷ୍ଟ କରିବେ?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"ଗୋଟିଏ ଆପକୁ କାଷ୍ଟ କରନ୍ତୁ"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"ଚାଲୁ ଅଛି - ଫେସ-ଆଧାରିତ"</string> <string name="inline_done_button" msgid="6043094985588909584">"ହୋଇଗଲା"</string> <string name="inline_ok_button" msgid="603075490581280343">"ଲାଗୁ କରନ୍ତୁ"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"ବନ୍ଦ କରନ୍ତୁ"</string> <string name="notification_silence_title" msgid="8608090968400832335">"ନୀରବ"</string> <string name="notification_alert_title" msgid="3656229781017543655">"ଡିଫଲ୍ଟ"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"ସ୍ୱଚାଳିତ"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"ଡାହାଣ ଆଇକନ୍"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"ଟାଇଲ୍ ଯୋଗ କରିବା ପାଇଁ ଦାବିଧରି ଡ୍ରାଗ କରନ୍ତୁ"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"ଟାଇଲ ପୁଣି ସଜାଇବାକୁ ଦାବିଧରି ଟାଣନ୍ତୁ"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"ବାହାର କରିବାକୁ ଏଠାକୁ ଡ୍ରାଗ୍ କରନ୍ତୁ"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"ଆପଣଙ୍କର ଅତିକମ୍ରେ <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g>ଟି ଟାଇଲ୍ ଆବଶ୍ୟକ"</string> <string name="qs_edit" msgid="5583565172803472437">"ଏଡିଟ କରନ୍ତୁ"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"ଅନ୍ୟ"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"ଟାଇଲର ସାଇଜକୁ ଟୋଗଲ କରନ୍ତୁ"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"ଟାଇଲ୍ କାଢ଼ି ଦିଅନ୍ତୁ"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"ଶେଷ ପୋଜିସନରେ ଟାଇଲ ଯୋଗ କରିବା"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"ଟାଇଲ୍ ମୁଭ୍ କରନ୍ତୁ"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"ଇଚ୍ଛିତ ପୋଜିସନରେ ଟାଇଲ ଯୋଗ କରନ୍ତୁ"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"ଅଜଣା"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"ସମସ୍ତ ଟାଇଲକୁ ରିସେଟ କରିବେ?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"ସମସ୍ତ କୁଇକ ସେଟିଂସ ଟାଇଲ ଡିଭାଇସର ମୂଳ ସେଟିଂସରେ ରିସେଟ ହୋଇଯିବ"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-or/tiles_states_strings.xml b/packages/SystemUI/res/values-or/tiles_states_strings.xml index 37d3c95629f5..ac4d3b35f96c 100644 --- a/packages/SystemUI/res/values-or/tiles_states_strings.xml +++ b/packages/SystemUI/res/values-or/tiles_states_strings.xml @@ -56,9 +56,11 @@ <item msgid="5376619709702103243">"ବନ୍ଦ ଅଛି"</item> <item msgid="4875147066469902392">"ଚାଲୁ ଅଛି"</item> </string-array> - <!-- no translation found for tile_states_modes_dnd:0 (6509540227356524582) --> - <!-- no translation found for tile_states_modes_dnd:1 (8589336868985358191) --> - <!-- no translation found for tile_states_modes_dnd:2 (726072717827778234) --> + <string-array name="tile_states_modes_dnd"> + <item msgid="6509540227356524582">"ଅନୁପଲବ୍ଧ"</item> + <item msgid="8589336868985358191">"ବନ୍ଦ ଅଛି"</item> + <item msgid="726072717827778234">"ଚାଲୁ ଅଛି"</item> + </string-array> <string-array name="tile_states_flashlight"> <item msgid="3465257127433353857">"ଉପଲବ୍ଧ ନାହିଁ"</item> <item msgid="5044688398303285224">"ବନ୍ଦ ଅଛି"</item> diff --git a/packages/SystemUI/res/values-pa/strings.xml b/packages/SystemUI/res/values-pa/strings.xml index 98d56e34a6b0..0e2373093dfc 100644 --- a/packages/SystemUI/res/values-pa/strings.xml +++ b/packages/SystemUI/res/values-pa/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"\'ਨਵਾਂ ਡੀਵਾਈਸ ਜੋੜਾਬੱਧ ਕਰੋ\' \'ਤੇ ਕਲਿੱਕ ਕਰੋ"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"ਪ੍ਰੀਸੈੱਟ ਨੂੰ ਅੱਪਡੇਟ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"ਪ੍ਰੀਸੈੱਟ"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"ਕਾਲਾਂ ਲਈ ਪੂਰਵ-ਨਿਰਧਾਰਿਤ ਮਾਈਕ੍ਰੋਫ਼ੋਨ"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"ਸੁਣਨ ਦੇ ਸਾਧਨ ਦਾ ਮਾਈਕ੍ਰੋਫ਼ੋਨ"</item> + <item msgid="8501466270452446450">"ਇਸ ਫ਼ੋਨ ਦਾ ਮਾਈਕ੍ਰੋਫ਼ੋਨ"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"ਚੁਣਿਆ ਗਿਆ"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"ਆਲੇ-ਦੁਆਲੇ"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"ਖੱਬੇ"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"ਕਿਸੇ ਐਪ ਨੂੰ ਸਾਂਝਾ ਕਰਨ ਦੌਰਾਨ, ਉਸ ਐਪ \'ਤੇ ਦਿਖ ਰਹੀ ਜਾਂ ਚਲਾਈ ਗਈ ਹਰੇਕ ਚੀਜ਼ <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ਨੂੰ ਦਿਖਣਯੋਗ ਹੁੰਦੀ ਹੈ। ਇਸ ਲਈ ਪਾਸਵਰਡਾਂ, ਭੁਗਤਾਨ ਵੇਰਵਿਆਂ, ਸੁਨੇਹਿਆਂ, ਫ਼ੋਟੋਆਂ ਅਤੇ ਆਡੀਓ ਅਤੇ ਵੀਡੀਓ ਵਰਗੀਆਂ ਚੀਜ਼ਾਂ ਵਾਸਤੇ ਸਾਵਧਾਨ ਰਹੋ।"</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"ਸਕ੍ਰੀਨ ਸਾਂਝੀ ਕਰੋ"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"<xliff:g id="APP_NAME">%1$s</xliff:g> ਨੇ ਇਸ ਵਿਕਲਪ ਨੂੰ ਬੰਦ ਕਰ ਦਿੱਤਾ ਹੈ"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"ਐਪ ਵੱਲੋਂ ਸਮਰਥਿਤ ਨਹੀਂ"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"ਸਾਂਝਾ ਕਰਨ ਲਈ ਐਪ ਚੁਣੋ"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"ਕੀ ਸਕ੍ਰੀਨ ਨੂੰ ਕਾਸਟ ਕਰਨਾ ਹੈ?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"ਇੱਕ ਐਪ ਨੂੰ ਕਾਸਟ ਕਰੋ"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"ਚਾਲੂ ਹੈ - ਚਿਹਰਾ-ਆਧਾਰਿਤ"</string> <string name="inline_done_button" msgid="6043094985588909584">"ਹੋ ਗਿਆ"</string> <string name="inline_ok_button" msgid="603075490581280343">"ਲਾਗੂ ਕਰੋ"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"ਬੰਦ ਕਰੋ"</string> <string name="notification_silence_title" msgid="8608090968400832335">"ਸ਼ਾਂਤ"</string> <string name="notification_alert_title" msgid="3656229781017543655">"ਪੂਰਵ-ਨਿਰਧਾਰਿਤ"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"ਸਵੈਚਲਿਤ"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"ਸੱਜਾ ਪ੍ਰਤੀਕ"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"ਟਾਇਲਾਂ ਸ਼ਾਮਲ ਕਰਨ ਲਈ ਫੜ੍ਹ ਕੇ ਘਸੀਟੋ"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"ਟਾਇਲਾਂ ਨੂੰ ਮੁੜ-ਵਿਵਸਥਿਤ ਕਰਨ ਲਈ ਫੜ੍ਹ ਕੇ ਘਸੀਟੋ"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"ਹਟਾਉਣ ਲਈ ਇੱਥੇ ਘਸੀਟੋ"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"ਤੁਹਾਨੂੰ ਘੱਟੋ-ਘੱਟ <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> ਟਾਇਲਾਂ ਦੀ ਲੋੜ ਪਵੇਗੀ"</string> <string name="qs_edit" msgid="5583565172803472437">"ਸੰਪਾਦਨ ਕਰੋ"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"ਹੋਰ"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"ਟਾਇਲ ਦੇ ਆਕਾਰ ਨੂੰ ਟੌਗਲ ਕਰੋ"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"ਟਾਇਲ ਹਟਾਓ"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"ਪਿਛਲੀ ਸਥਿਤੀ \'ਤੇ ਟਾਇਲ ਸ਼ਾਮਲ ਕਰੋ"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"ਟਾਇਲ ਨੂੰ ਲਿਜਾਓ"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"ਮਨਪਸੰਦ ਸਥਿਤੀ \'ਤੇ ਟਾਇਲ ਸ਼ਾਮਲ ਕਰੋ"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"ਅਗਿਆਤ"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"ਕੀ ਸਾਰੀਆਂ ਟਾਇਲਾਂ ਨੂੰ ਰੀਸੈੱਟ ਕਰਨਾ ਹੈ?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"ਸਾਰੀਆਂ ਤਤਕਾਲ ਸੈਟਿੰਗਾਂ ਟਾਇਲਾਂ ਡੀਵਾਈਸ ਦੀਆਂ ਮੂਲ ਸੈਟਿੰਗਾਂ \'ਤੇ ਰੀਸੈੱਟ ਹੋ ਜਾਣਗੀਆਂ"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-pl/strings.xml b/packages/SystemUI/res/values-pl/strings.xml index cdf30a992f49..ed9641716cd7 100644 --- a/packages/SystemUI/res/values-pl/strings.xml +++ b/packages/SystemUI/res/values-pl/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Kliknij, aby sparować nowe urządzenie"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Nie udało się zaktualizować gotowego ustawienia"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Gotowe ustawienie"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Domyślny mikrofon do połączeń"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Mikrofon aparatu słuchowego"</item> + <item msgid="8501466270452446450">"Mikrofon tego telefonu"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Wybrano"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Otoczenie"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Po lewej"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Włączono – na podstawie twarzy"</string> <string name="inline_done_button" msgid="6043094985588909584">"Gotowe"</string> <string name="inline_ok_button" msgid="603075490581280343">"Zastosuj"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Wyłącz"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Ciche"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Domyślne"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automatycznie"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Prawa ikona"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Aby dodać kafelki, przytrzymaj je i przeciągnij"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Aby przestawić kafelki, przytrzymaj je i przeciągnij"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Przeciągnij tutaj, by usunąć"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Minimalna liczba kafelków to <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g>"</string> <string name="qs_edit" msgid="5583565172803472437">"Edytuj"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Inne"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"przełącz rozmiar kafelka"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"usunąć kartę"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"dodaj kafelek do ostatniej pozycji"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Przenieś kartę"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Dodaj kafelek do wybranej pozycji"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Nieznane"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Zresetować wszystkie kafelki?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Wszystkie kafelki Szybkich ustawień zostaną zresetowane do oryginalnych ustawień urządzenia"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-pl/tiles_states_strings.xml b/packages/SystemUI/res/values-pl/tiles_states_strings.xml index abc1867f0660..ebd89d2dc7b9 100644 --- a/packages/SystemUI/res/values-pl/tiles_states_strings.xml +++ b/packages/SystemUI/res/values-pl/tiles_states_strings.xml @@ -56,9 +56,11 @@ <item msgid="5376619709702103243">"Wyłączone"</item> <item msgid="4875147066469902392">"Włączone"</item> </string-array> - <!-- no translation found for tile_states_modes_dnd:0 (6509540227356524582) --> - <!-- no translation found for tile_states_modes_dnd:1 (8589336868985358191) --> - <!-- no translation found for tile_states_modes_dnd:2 (726072717827778234) --> + <string-array name="tile_states_modes_dnd"> + <item msgid="6509540227356524582">"Niedostępny"</item> + <item msgid="8589336868985358191">"Wyłączony"</item> + <item msgid="726072717827778234">"Włączony"</item> + </string-array> <string-array name="tile_states_flashlight"> <item msgid="3465257127433353857">"Niedostępny"</item> <item msgid="5044688398303285224">"Wyłączona"</item> diff --git a/packages/SystemUI/res/values-pt-rBR/strings.xml b/packages/SystemUI/res/values-pt-rBR/strings.xml index d9bbaf064da1..ab06ef85e7e4 100644 --- a/packages/SystemUI/res/values-pt-rBR/strings.xml +++ b/packages/SystemUI/res/values-pt-rBR/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Clique para parear o novo dispositivo"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Não foi possível atualizar a predefinição"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Predefinição"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Microfone padrão para ligações"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Microfone para aparelhos auditivos"</item> + <item msgid="8501466270452446450">"O microfone deste smartphone"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Selecionado"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Som ambiente"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Lado esquerdo"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Ativada (reconhecimento facial)"</string> <string name="inline_done_button" msgid="6043094985588909584">"Concluído"</string> <string name="inline_ok_button" msgid="603075490581280343">"Aplicar"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Desativar"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Silenciosas"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Padrão"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automática"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Ícone à direita"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Mantenha pressionado e arraste para adicionar blocos"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Para reorganizar, toque no bloco sem soltar e arraste"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Arraste aqui para remover"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"É preciso haver pelo menos <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> blocos"</string> <string name="qs_edit" msgid="5583565172803472437">"Editar"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Outros"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"alternar o tamanho do bloco"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"remover o bloco"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"adicionar o bloco à última posição"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Mover bloco"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Adicionar o bloco à posição desejada"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Desconhecidos"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Redefinir todos os blocos?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Todos os blocos \"Configurações rápidas\" serão redefinidos para as configurações originais do dispositivo"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-pt-rBR/tiles_states_strings.xml b/packages/SystemUI/res/values-pt-rBR/tiles_states_strings.xml index 30e45feb3c13..28ea47f31f5c 100644 --- a/packages/SystemUI/res/values-pt-rBR/tiles_states_strings.xml +++ b/packages/SystemUI/res/values-pt-rBR/tiles_states_strings.xml @@ -56,9 +56,11 @@ <item msgid="5376619709702103243">"Desativado"</item> <item msgid="4875147066469902392">"Ativado"</item> </string-array> - <!-- no translation found for tile_states_modes_dnd:0 (6509540227356524582) --> - <!-- no translation found for tile_states_modes_dnd:1 (8589336868985358191) --> - <!-- no translation found for tile_states_modes_dnd:2 (726072717827778234) --> + <string-array name="tile_states_modes_dnd"> + <item msgid="6509540227356524582">"Indisponível"</item> + <item msgid="8589336868985358191">"Desativado"</item> + <item msgid="726072717827778234">"Ativado"</item> + </string-array> <string-array name="tile_states_flashlight"> <item msgid="3465257127433353857">"Indisponível"</item> <item msgid="5044688398303285224">"Apagada"</item> diff --git a/packages/SystemUI/res/values-pt-rPT/strings.xml b/packages/SystemUI/res/values-pt-rPT/strings.xml index 913f5b3be8bd..94dbc4428ae0 100644 --- a/packages/SystemUI/res/values-pt-rPT/strings.xml +++ b/packages/SystemUI/res/values-pt-rPT/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Clique para sincronizar um novo dispositivo"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Não foi possível atualizar a predefinição"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Predefinição"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Microfone predefinido para chamadas"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Microfone do aparelho auditivo"</item> + <item msgid="8501466270452446450">"Microfone deste telemóvel"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Selecionado"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Ambiente"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Esquerda"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"Quando está a partilhar uma app, tudo o que é mostrado ou reproduzido nessa app é visível para a app <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>. Por isso, tenha cuidado com, por exemplo, palavras-passe, detalhes de pagamento, mensagens, fotos, áudio e vídeo."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"Partilhar ecrã"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"A app <xliff:g id="APP_NAME">%1$s</xliff:g> desativou esta opção"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"Não compatível com a app"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"Escolha uma app para partilhar"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"Transmitir o ecrã?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"Transmitir uma app"</string> @@ -982,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"Ícone direito"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Toque sem soltar e arraste para reorganizar os mosaicos"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Toque sem soltar e arraste para reorganizar os mosaicos"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Arrastar para aqui para remover"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Necessita de, pelo menos, <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> cartões"</string> <string name="qs_edit" msgid="5583565172803472437">"Editar"</string> @@ -1002,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"Outro"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"ativar/desativar o tamanho do mosaico"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"remover o cartão"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"adicionar o mosaico à última posição"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Mover cartão"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Adicionar mosaico à posição pretendida"</string> @@ -1577,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Desconhecido"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Repor todos os mosaicos?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Todos os mosaicos de Definições rápidas vão ser repostos para as definições originais do dispositivo"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-pt/strings.xml b/packages/SystemUI/res/values-pt/strings.xml index d9bbaf064da1..ab06ef85e7e4 100644 --- a/packages/SystemUI/res/values-pt/strings.xml +++ b/packages/SystemUI/res/values-pt/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Clique para parear o novo dispositivo"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Não foi possível atualizar a predefinição"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Predefinição"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Microfone padrão para ligações"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Microfone para aparelhos auditivos"</item> + <item msgid="8501466270452446450">"O microfone deste smartphone"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Selecionado"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Som ambiente"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Lado esquerdo"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Ativada (reconhecimento facial)"</string> <string name="inline_done_button" msgid="6043094985588909584">"Concluído"</string> <string name="inline_ok_button" msgid="603075490581280343">"Aplicar"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Desativar"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Silenciosas"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Padrão"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automática"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Ícone à direita"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Mantenha pressionado e arraste para adicionar blocos"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Para reorganizar, toque no bloco sem soltar e arraste"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Arraste aqui para remover"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"É preciso haver pelo menos <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> blocos"</string> <string name="qs_edit" msgid="5583565172803472437">"Editar"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Outros"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"alternar o tamanho do bloco"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"remover o bloco"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"adicionar o bloco à última posição"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Mover bloco"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Adicionar o bloco à posição desejada"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Desconhecidos"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Redefinir todos os blocos?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Todos os blocos \"Configurações rápidas\" serão redefinidos para as configurações originais do dispositivo"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-pt/tiles_states_strings.xml b/packages/SystemUI/res/values-pt/tiles_states_strings.xml index 30e45feb3c13..28ea47f31f5c 100644 --- a/packages/SystemUI/res/values-pt/tiles_states_strings.xml +++ b/packages/SystemUI/res/values-pt/tiles_states_strings.xml @@ -56,9 +56,11 @@ <item msgid="5376619709702103243">"Desativado"</item> <item msgid="4875147066469902392">"Ativado"</item> </string-array> - <!-- no translation found for tile_states_modes_dnd:0 (6509540227356524582) --> - <!-- no translation found for tile_states_modes_dnd:1 (8589336868985358191) --> - <!-- no translation found for tile_states_modes_dnd:2 (726072717827778234) --> + <string-array name="tile_states_modes_dnd"> + <item msgid="6509540227356524582">"Indisponível"</item> + <item msgid="8589336868985358191">"Desativado"</item> + <item msgid="726072717827778234">"Ativado"</item> + </string-array> <string-array name="tile_states_flashlight"> <item msgid="3465257127433353857">"Indisponível"</item> <item msgid="5044688398303285224">"Apagada"</item> diff --git a/packages/SystemUI/res/values-ro/strings.xml b/packages/SystemUI/res/values-ro/strings.xml index 906ee09405c3..0d16f3549144 100644 --- a/packages/SystemUI/res/values-ro/strings.xml +++ b/packages/SystemUI/res/values-ro/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Dă clic pentru a asocia un nou dispozitiv"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Nu s-a putut actualiza presetarea"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Presetare"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Microfon prestabilit pentru apeluri"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Microfonul aparatului auditiv"</item> + <item msgid="8501466270452446450">"Microfonul acestui telefon"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Selectat"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Împrejurimi"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Stânga"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Activată – În funcție de chip"</string> <string name="inline_done_button" msgid="6043094985588909584">"Gata"</string> <string name="inline_ok_button" msgid="603075490581280343">"Aplică"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Dezactivează"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Silențios"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Prestabilite"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automat"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Pictograma din dreapta"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Ține apăsat și trage pentru a adăuga carduri"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Ține apăsat și trage pentru a rearanja cardurile"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Trage aici pentru a elimina"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Ai nevoie de cel puțin <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> carduri"</string> <string name="qs_edit" msgid="5583565172803472437">"Editează"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Altele"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"comută dimensiunea cardului"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"elimină cardul"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"adaugă cardul în ultima poziție"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Mută cardul"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Adaugă cardul în poziția dorită"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Necunoscută"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Resetezi toate cardurile?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Toate cardurile Setări rapide se vor reseta la setările inițiale ale dispozitivului"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-ru/strings.xml b/packages/SystemUI/res/values-ru/strings.xml index ee9d755088ed..442b16e16b0b 100644 --- a/packages/SystemUI/res/values-ru/strings.xml +++ b/packages/SystemUI/res/values-ru/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Нажмите, чтобы подключить новое устройство"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Не удалось обновить набор настроек."</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Набор настроек"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Микрофон по умолчанию для вызовов"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Микрофон слухового аппарата"</item> + <item msgid="8501466270452446450">"Встроенный микрофон"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Выбрано"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Окружающие звуки"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Левый"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"При показе приложения все, что в нем происходит, будет видно в приложении \"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>\". Поэтому будьте осторожны с паролями, сведениями о способах оплаты, сообщениями, фотографиями, аудио- и видеозаписями."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"Показать экран"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"Приложение \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" отключило эту возможность"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"Приложение не поддерживает эту функцию"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"Выбор приложения для демонстрации"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"Начать трансляцию экрана?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"Транслировать одно приложение"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Включить (на основе распознавания лиц)"</string> <string name="inline_done_button" msgid="6043094985588909584">"Готово"</string> <string name="inline_ok_button" msgid="603075490581280343">"Применить"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Отключить"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Без звука"</string> <string name="notification_alert_title" msgid="3656229781017543655">"По умолчанию"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Автоматически"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"Значок \"Вправо\""</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Чтобы добавить элементы, перетащите их."</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Чтобы изменить порядок элементов, перетащите их."</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Чтобы удалить, перетащите сюда"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Должно остаться не менее <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> элементов"</string> <string name="qs_edit" msgid="5583565172803472437">"Изменить"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"Другое"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"изменить размер параметра"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"удалить панель"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"добавить параметр в конец"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Переместить панель"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Выбрать, куда добавить параметр"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Неизвестно"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Сбросить все параметры?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Для всех параметров быстрых настроек будут восстановлены значения по умолчанию."</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-si/strings.xml b/packages/SystemUI/res/values-si/strings.xml index 583d8842b189..074ef9386ecc 100644 --- a/packages/SystemUI/res/values-si/strings.xml +++ b/packages/SystemUI/res/values-si/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"නව උපාංගය යුගල කිරීමට ක්ලික් කරන්න"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"පෙර සැකසීම යාවත්කාලීන කළ නොහැකි විය"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"පෙරසැකසුම"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"ඇමතුම් සඳහා පෙරනිමි මයික්රෆෝනය"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"ශ්රවණාධාර මයික්රෆෝනය"</item> + <item msgid="8501466270452446450">"මෙම දුරකථනයේ මයික්රෆෝනය"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"තෝරන ලදි"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"වටපිටාව"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"වම"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"ඔබ යෙදුමක් බෙදා ගන්නා විට, එම යෙදුමේ පෙන්වන හෝ වාදනය කරන ඕනෑම දෙයක් <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> වෙත දෘශ්යමාන වේ. ඒ නිසා මුරපද, ගෙවීම් විස්තර, පණිවිඩ, ඡායාරූප, සහ ශ්රව්ය සහ දෘශ්ය වැනි දේවල් පිළිබඳ ප්රවේශම් වන්න."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"තිරය බෙදා ගන්න"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"<xliff:g id="APP_NAME">%1$s</xliff:g> මෙම විකල්පය අබල කර ඇත"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"යෙදුම මඟින් සහාය නොදක්වයි"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"බෙදා ගැනීමට යෙදුම තෝරන්න"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"ඔබේ තිරය විකාශය කරන්න ද?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"එක් යෙදුමක් විකාශය කරන්න"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"ක්රියාත්මකයි - මුහුණ-පදනම්ව"</string> <string name="inline_done_button" msgid="6043094985588909584">"නිමයි"</string> <string name="inline_ok_button" msgid="603075490581280343">"යොදන්න"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"ක්රියාවිරහිත කරන්න"</string> <string name="notification_silence_title" msgid="8608090968400832335">"නිහඬ"</string> <string name="notification_alert_title" msgid="3656229781017543655">"පෙරනිමි"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"ස්වයංක්රිය"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"දකුණු නිරූපකය"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"ටයිල් එක් කිරීමට අල්ලාගෙන සිට අදින්න"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"ටයිල් නැවත සකස් කිරීමට අල්ලාගෙන සිට අදින්න"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"ඉවත් කිරීමට මෙතැනට අදින්න"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"ඔබ අවම වශයෙන් ටයිල් <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> ක් අවශ්ය වෙයි"</string> <string name="qs_edit" msgid="5583565172803472437">"සංස්කරණය"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"වෙනත්"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"ටයිල් එකේ ප්රමාණය මාරු කරන්න"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"ටයිල් ඉවත් කරන්න"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"ටයිල් එක අවසාන ස්ථානයට එක් කරන්න"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"ටයිල් ගෙන යන්න"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"අපේක්ෂිත ස්ථානයට ටයිල් එක එක් කරන්න"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"නොදනී"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"සියලු ටයිල් නැවත සකසන්න ද?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"සියලු ඉක්මන් සැකසීම් ටයිල් උපාංගයේ මුල් සැකසීම් වෙත නැවත සකසනු ඇත"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-si/tiles_states_strings.xml b/packages/SystemUI/res/values-si/tiles_states_strings.xml index 7eeaefd4e546..1b5cf748026a 100644 --- a/packages/SystemUI/res/values-si/tiles_states_strings.xml +++ b/packages/SystemUI/res/values-si/tiles_states_strings.xml @@ -56,9 +56,11 @@ <item msgid="5376619709702103243">"අක්රියයි"</item> <item msgid="4875147066469902392">"සක්රියයි"</item> </string-array> - <!-- no translation found for tile_states_modes_dnd:0 (6509540227356524582) --> - <!-- no translation found for tile_states_modes_dnd:1 (8589336868985358191) --> - <!-- no translation found for tile_states_modes_dnd:2 (726072717827778234) --> + <string-array name="tile_states_modes_dnd"> + <item msgid="6509540227356524582">"නොමැත"</item> + <item msgid="8589336868985358191">"ක්රියාවිරහිතයි"</item> + <item msgid="726072717827778234">"ක්රියාත්මකයි"</item> + </string-array> <string-array name="tile_states_flashlight"> <item msgid="3465257127433353857">"නොමැත"</item> <item msgid="5044688398303285224">"අක්රියයි"</item> diff --git a/packages/SystemUI/res/values-sk/strings.xml b/packages/SystemUI/res/values-sk/strings.xml index 206e10c6245d..ae828110a5c3 100644 --- a/packages/SystemUI/res/values-sk/strings.xml +++ b/packages/SystemUI/res/values-sk/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Kliknutím spárujete nové zariadenie"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Predvoľbu sa nepodarilo aktualizovať"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Predvoľba"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Predvolený mikrofón pre hovory"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Mikrofón načúvadla"</item> + <item msgid="8501466270452446450">"Mikrofón tohto telefónu"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Vybrané"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Okolie"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Vľavo"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Zapnuté – podľa tváre"</string> <string name="inline_done_button" msgid="6043094985588909584">"Hotovo"</string> <string name="inline_ok_button" msgid="603075490581280343">"Použiť"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Vypnúť"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Tiché"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Predvolené"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automaticky"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Pravá ikona"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Pridržaním a presunutím pridáte karty"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Karty môžete usporiadať pridržaním a presunutím"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Presunutím sem odstránite"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Minimálny počet vyžadovaných dlaždíc: <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g>"</string> <string name="qs_edit" msgid="5583565172803472437">"Upraviť"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Ďalšie"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"prepnúť veľkosť karty"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"odstrániť kartu"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"pridáte kartu na poslednú pozíciu"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Presunúť kartu"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Pridať kartu na požadovanú pozíciu"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Neznáme"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Chcete resetovať všetky karty?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Všetky karty rýchlych nastavení sa resetujú na pôvodné nastavenia zariadenia"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-sl/strings.xml b/packages/SystemUI/res/values-sl/strings.xml index 1c1ff03c3dcf..c06b387dde72 100644 --- a/packages/SystemUI/res/values-sl/strings.xml +++ b/packages/SystemUI/res/values-sl/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Kliknite za seznanitev nove naprave"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Prednastavljenih vrednosti ni bilo mogoče posodobiti"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Prednastavljeno"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Privzeti mikrofon za klice"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Mikrofon slušnega aparata"</item> + <item msgid="8501466270452446450">"Mikrofon tega telefona"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Izbrano"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Okolica"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Levo"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"Pri deljenju aplikacije je aplikaciji <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> vidno vse, kar je prikazano ali predvajano v tej aplikaciji. Zato bodite previdni z gesli, podatki za plačilo, sporočili, fotografijami ter z zvokom in videom."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"Deli zaslon"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"Aplikacija <xliff:g id="APP_NAME">%1$s</xliff:g> je onemogočila to možnost"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"Aplikacija ne podpira funkcije"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"Izbira aplikacije za deljenje"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"Želite predvajati vsebino zaslona?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"Predvajanje ene aplikacije"</string> @@ -982,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"Desna ikona"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Držite in povlecite, da dodate ploščice."</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Držite in povlecite, da prerazporedite ploščice."</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Če želite odstraniti, povlecite sem"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Imeti morate vsaj toliko ploščic: <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g>"</string> <string name="qs_edit" msgid="5583565172803472437">"Uredi"</string> @@ -1002,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"Drugo"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"preklop velikosti ploščice"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"odstranitev ploščice"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"dodajanje ploščice na zadnji položaj"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Premik ploščice"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Dodaj ploščico na želeno mesto"</string> @@ -1577,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Neznano"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Želite ponastaviti vse ploščice?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Vse ploščice v hitrih nastavitvah bodo ponastavljene na prvotne nastavitve naprave."</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-sq/strings.xml b/packages/SystemUI/res/values-sq/strings.xml index c68e7a81a02c..88f8695ed90f 100644 --- a/packages/SystemUI/res/values-sq/strings.xml +++ b/packages/SystemUI/res/values-sq/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Kliko për të çiftuar një pajisje të re"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Paravendosja nuk mund të përditësohej"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Paravendosja"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Mikrofoni i parazgjedhur për telefonatat"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Mikrofoni i aparatit të dëgjimit"</item> + <item msgid="8501466270452446450">"Mikrofoni i këtij telefoni"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Zgjedhur"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Ambienti rrethues"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Majtas"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Aktiv - Në bazë të fytyrës"</string> <string name="inline_done_button" msgid="6043094985588909584">"U krye"</string> <string name="inline_ok_button" msgid="603075490581280343">"Zbato"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Çaktivizo"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Në heshtje"</string> <string name="notification_alert_title" msgid="3656229781017543655">"E parazgjedhur"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automatike"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Ikona djathtas"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Mbaje të shtypur dhe zvarrit për të shtuar pllakëza"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Mbaje të shtypur dhe zvarrit për të risistemuar pllakëzat"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Zvarrit këtu për ta hequr"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Të duhen të paktën <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> pllakëza"</string> <string name="qs_edit" msgid="5583565172803472437">"Redakto"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Të tjera"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"ndrysho madhësinë e pllakëzës"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"hiq pllakëzën"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"shtuar pllakëzën në pozicionin e fundit"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Zhvendos pllakëzën"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Shto pllakëzën në pozicionin e dëshiruar"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Nuk njihet"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Të rivendosen të gjitha pllakëzat?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Të gjitha pllakëzat e \"Cilësimeve të shpejta\" do të rivendosen te cilësimet origjinale të pajisjes"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-sr/strings.xml b/packages/SystemUI/res/values-sr/strings.xml index 46d83528114f..6561de1da71b 100644 --- a/packages/SystemUI/res/values-sr/strings.xml +++ b/packages/SystemUI/res/values-sr/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Кликните да бисте упарили нов уређај"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Ажурирање задатих подешавања није успело"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Унапред одређена подешавања"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Подразумевани микрофон за позиве"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Микрофон слушног апарата"</item> + <item msgid="8501466270452446450">"Микрофон овог телефона"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Изабрано"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Окружење"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Лево"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"Када делите апликацију, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> види сав садржај који се приказује или пушта у њој. Зато пазите на лозинке, информације о плаћању, поруке, слике, аудио и видео садржај."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"Дели екран"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"Апликација <xliff:g id="APP_NAME">%1$s</xliff:g> је онемогућила ову опцију"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"Апликација то не подржава"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"Одаберите апликацију за дељење"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"Желите да пребаците екран?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"Пребаци једну апликацију"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Укључено – на основу лица"</string> <string name="inline_done_button" msgid="6043094985588909584">"Готово"</string> <string name="inline_ok_button" msgid="603075490581280343">"Примени"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Искључи"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Нечујно"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Подразумевано"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Аутоматска"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"Десна икона"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Задржите и превуците да бисте додали плочице"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Задржите и превуците да бисте променили распоред плочица"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Превуците овде да бисте уклонили"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Минималан број плочица је <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g>"</string> <string name="qs_edit" msgid="5583565172803472437">"Измени"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"Друго"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"укључивање или искључивање величине плочице"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"уклонили плочицу"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"додали плочицу на последњу позицију"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Преместите плочицу"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Додајте плочицу на жељену позицију"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Непознато"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Желите да ресетујете све плочице?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Све плочице Брзих подешавања ће се ресетовати на првобитна подешавања уређаја"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-sv/strings.xml b/packages/SystemUI/res/values-sv/strings.xml index ed0daf9da04b..6b442082e6e5 100644 --- a/packages/SystemUI/res/values-sv/strings.xml +++ b/packages/SystemUI/res/values-sv/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Klicka för att parkoppla en ny enhet"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Det gick inte att uppdatera förinställningen"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Förinställning"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Standardmikrofon för samtal"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Hörapparatens mikrofon"</item> + <item msgid="8501466270452446450">"Telefonens mikrofon"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Markerad"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Omgivningsläge"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Vänster"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"På – ansiktsbaserad"</string> <string name="inline_done_button" msgid="6043094985588909584">"Klart"</string> <string name="inline_ok_button" msgid="603075490581280343">"Tillämpa"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Inaktivera"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Tyst"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Standard"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Automatiskt"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Höger ikon"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Lägg till rutor genom att trycka och dra"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Ordna om rutor genom att trycka och dra"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Ta bort genom att dra här"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Minst <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> rutor måste finnas kvar"</string> <string name="qs_edit" msgid="5583565172803472437">"Redigera"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Annat"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"växla rutstorlek"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"ta bort ruta"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"lägg till en ruta på den sista platsen"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Flytta ruta"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Lägg till en ruta på önskad plats"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Okänt"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Vill du återställa alla rutor?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Alla Snabbinställningsrutor återställs till enhetens ursprungliga inställningar"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-sw/strings.xml b/packages/SystemUI/res/values-sw/strings.xml index da390fb459ac..9e0e67bb2491 100644 --- a/packages/SystemUI/res/values-sw/strings.xml +++ b/packages/SystemUI/res/values-sw/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Bofya ili uunganishe kifaa kipya"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Imeshindwa kusasisha mipangilio iliyowekwa mapema"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Mipangilio iliyowekwa mapema"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Maikrofoni chaguomsingi ya kupiga simu"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Maikrofoni ya visaidizi vya kusikia"</item> + <item msgid="8501466270452446450">"Maikrofoni ya simu hii"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Umechagua"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Mazingira"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Kushoto"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Imewashwa - Inayolenga nyuso"</string> <string name="inline_done_button" msgid="6043094985588909584">"Nimemaliza"</string> <string name="inline_ok_button" msgid="603075490581280343">"Tumia"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Zima"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Kimya"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Chaguomsingi"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Otomatiki"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Aikoni ya kulia"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Shikilia na uburute ili uongeze vigae"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Shikilia na uburute ili upange vigae upya"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Buruta hapa ili uondoe"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Unahitaji angalau vigae <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g>"</string> <string name="qs_edit" msgid="5583565172803472437">"Badilisha"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Nyingine"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"ubadilishe ukubwa wa kigae"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"ondoa kigae"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"weka kigae kwenye nafasi ya mwisho"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Hamisha kigae"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Weka kigae kwenye nafasi unayopenda"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Visivyojulikana"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Ungependa kubadilisha vigae vyote?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Vigae vyote vya Mipangilio ya Haraka vitabadilishwa kuwa katika mipangilio halisi ya kifaa"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-ta/strings.xml b/packages/SystemUI/res/values-ta/strings.xml index 0c7aa40868cb..66baf1feed4f 100644 --- a/packages/SystemUI/res/values-ta/strings.xml +++ b/packages/SystemUI/res/values-ta/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"புதிய சாதனத்தை இணைக்க கிளிக் செய்யலாம்"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"முன்னமைவைப் புதுப்பிக்க முடியவில்லை"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"முன்னமைவு"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"அழைப்புகளுக்கான இயல்பு மைக்ரோஃபோன்"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"செவித்துணைக் கருவி மைக்ரோஃபோன்"</item> + <item msgid="8501466270452446450">"இந்த மொபைலின் மைக்ரோஃபோன்"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"தேர்ந்தெடுக்கப்பட்டது"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"சுற்றுப்புறங்கள்"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"இடது"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"ஆன் - முகம் அடிப்படையிலானது"</string> <string name="inline_done_button" msgid="6043094985588909584">"முடிந்தது"</string> <string name="inline_ok_button" msgid="603075490581280343">"பயன்படுத்து"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"ஆஃப் செய்"</string> <string name="notification_silence_title" msgid="8608090968400832335">"சைலன்ட்"</string> <string name="notification_alert_title" msgid="3656229781017543655">"இயல்புநிலை"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"தானியங்கு"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"வலப்புற ஐகான்"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"கட்டங்களைச் சேர்க்க, அவற்றைப் பிடித்து இழுக்கவும்"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"கட்டங்களை மறுவரிசைப்படுத்த அவற்றைப் பிடித்து இழுக்கவும்"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"அகற்ற, இங்கே இழுக்கவும்"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"குறைந்தது <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> கட்டங்கள் தேவை"</string> <string name="qs_edit" msgid="5583565172803472437">"மாற்று"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"மற்றவை"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"கட்டத்தின் அளவை நிலைமாற்றும்"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"கட்டத்தை அகற்றும்"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"கடைசி இடத்தில் கட்டத்தைச் சேர்க்கலாம்"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"கட்டத்தை நகர்த்து"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"விரும்பிய இடத்தில் கட்டத்தைச் சேர்க்கலாம்"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"தெரியவில்லை"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"அனைத்துக் கட்டங்களையும் மீட்டமைக்கவா?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"விரைவு அமைப்புகளின் கட்டங்கள் அனைத்தும் சாதனத்தின் அசல் அமைப்புகளுக்கு மீட்டமைக்கப்படும்"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-te/strings.xml b/packages/SystemUI/res/values-te/strings.xml index 972c64ab7f26..d1bc7e53afb5 100644 --- a/packages/SystemUI/res/values-te/strings.xml +++ b/packages/SystemUI/res/values-te/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"కొత్త పరికరాన్ని పెయిర్ చేయడానికి క్లిక్ చేయండి"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"ప్రీసెట్ను అప్డేట్ చేయడం సాధ్యపడలేదు"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"ప్రీసెట్"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"కాల్స్ అందుకునే ఆటోమేటిక్ మైక్రోఫోన్"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"వినికిడి పరికర మైక్రోఫోన్"</item> + <item msgid="8501466270452446450">"ఈ ఫోన్ మైక్రోఫోన్"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"ఎంచుకోబడింది"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"పరిసరాలు"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"ఎడమ వైపునకు"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"మీరు యాప్ను షేర్ చేసేటప్పుడు, సంబంధిత యాప్లో కనిపించేవి లేదా ప్లే అయ్యేవన్నీ <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>కు కనిపిస్తాయి. కాబట్టి పాస్వర్డ్లు, పేమెంట్ వివరాలు, మెసేజ్లు, ఫోటోలు, ఆడియో, ఇంకా వీడియో వంటి విషయాల్లో జాగ్రత్త వహించండి."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"స్క్రీన్ను షేర్ చేయండి"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"<xliff:g id="APP_NAME">%1$s</xliff:g> ఈ ఆప్షన్ను డిజేబుల్ చేసింది"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"యాప్ ద్వారా సపోర్ట్ చేయబడలేదు"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"షేర్ చేయడానికి యాప్ను ఎంచుకోండి"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"మీ స్క్రీన్ను ప్రసారం చేయాలా?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"ఒక యాప్ను ప్రసారం చేయండి"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"\'ముఖం ఆధారం\'ను - ఆన్ చేయండి"</string> <string name="inline_done_button" msgid="6043094985588909584">"పూర్తయింది"</string> <string name="inline_ok_button" msgid="603075490581280343">"అప్లయి చేయి"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"ఆఫ్ చేయండి"</string> <string name="notification_silence_title" msgid="8608090968400832335">"నిశ్శబ్దం"</string> <string name="notification_alert_title" msgid="3656229781017543655">"ఆటోమేటిక్ సెట్టింగ్"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"ఆటోమేటిక్"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"కుడివైపు ఉన్న చిహ్నం"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"టైల్స్ను జోడించడానికి పట్టుకుని, లాగండి"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"టైల్స్ను వేరే క్రమంలో అమర్చడానికి వాటిని పట్టుకుని, లాగండి"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"తీసివేయడానికి ఇక్కడికి లాగండి"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"మీ వద్ద కనీసం <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> టైల్లు ఉండాలి"</string> <string name="qs_edit" msgid="5583565172803472437">"ఎడిట్ చేయండి"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"ఇతరం"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"టైల్ సైజ్ను టోగుల్ చేయండి"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"టైల్ను తీసివేయండి"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"చివరి పొజిషన్కు టైల్ను జోడించండి"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"టైల్ను తరలించండి"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"కావలసిన పొజిషన్కు టైల్ను జోడించండి"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"తెలియదు"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"టైల్స్ అన్ని రీసెట్ చేయాలా?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"అన్ని క్విక్ సెట్టింగ్ల టైల్స్, పరికరం తాలూకు ఒరిజినల్ సెట్టింగ్లకు రీసెట్ చేయబడతాయి"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-th/strings.xml b/packages/SystemUI/res/values-th/strings.xml index a4f14473e004..920207bfa122 100644 --- a/packages/SystemUI/res/values-th/strings.xml +++ b/packages/SystemUI/res/values-th/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"คลิกเพื่อจับคู่อุปกรณ์ใหม่"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"ไม่สามารถอัปเดตค่าที่กำหนดล่วงหน้า"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"ค่าที่กำหนดล่วงหน้า"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"ไมโครโฟนเริ่มต้นสำหรับการโทร"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"ไมโครโฟนเครื่องช่วยฟัง"</item> + <item msgid="8501466270452446450">"ไมโครโฟนของโทรศัพท์เครื่องนี้"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"เลือกแล้ว"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"เสียงแวดล้อม"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"ซ้าย"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"เมื่อกำลังแชร์แอป <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> จะมองเห็นทุกสิ่งที่แสดงหรือเล่นอยู่ในแอปดังกล่าว ดังนั้นโปรดระวังสิ่งต่างๆ อย่างเช่นรหัสผ่าน รายละเอียดการชำระเงิน ข้อความ รูปภาพ รวมถึงเสียงและวิดีโอ"</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"แชร์หน้าจอ"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"<xliff:g id="APP_NAME">%1$s</xliff:g> ปิดใช้ตัวเลือกนี้"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"แอปไม่รองรับ"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"เลือกแอปที่จะแชร์"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"แคสต์หน้าจอของคุณไหม"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"แคสต์แอปเดียว"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"เปิด - ตามใบหน้า"</string> <string name="inline_done_button" msgid="6043094985588909584">"เสร็จสิ้น"</string> <string name="inline_ok_button" msgid="603075490581280343">"ใช้"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"ปิด"</string> <string name="notification_silence_title" msgid="8608090968400832335">"ปิดเสียง"</string> <string name="notification_alert_title" msgid="3656229781017543655">"ค่าเริ่มต้น"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"อัตโนมัติ"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"ไอคอนทางขวา"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"กดค้างแล้วลากเพื่อเพิ่มการ์ด"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"กดค้างแล้วลากเพื่อจัดเรียงการ์ดใหม่"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"ลากมาที่นี่เพื่อนำออก"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"คุณต้องมีการ์ดอย่างน้อย <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> รายการ"</string> <string name="qs_edit" msgid="5583565172803472437">"แก้ไข"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"อื่นๆ"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"สลับขนาดของการ์ด"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"นำชิ้นส่วนออก"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"เพิ่มการ์ดไปยังตำแหน่งสุดท้าย"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"ย้ายชิ้นส่วน"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"เพิ่มการ์ดไปยังตำแหน่งที่ต้องการ"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"ไม่ทราบ"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"รีเซ็ตการ์ดทั้งหมดใช่ไหม"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"การ์ดการตั้งค่าด่วนทั้งหมดจะรีเซ็ตเป็นการตั้งค่าเดิมของอุปกรณ์"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-th/tiles_states_strings.xml b/packages/SystemUI/res/values-th/tiles_states_strings.xml index b9690568d136..677888b16436 100644 --- a/packages/SystemUI/res/values-th/tiles_states_strings.xml +++ b/packages/SystemUI/res/values-th/tiles_states_strings.xml @@ -56,9 +56,11 @@ <item msgid="5376619709702103243">"ปิด"</item> <item msgid="4875147066469902392">"เปิด"</item> </string-array> - <!-- no translation found for tile_states_modes_dnd:0 (6509540227356524582) --> - <!-- no translation found for tile_states_modes_dnd:1 (8589336868985358191) --> - <!-- no translation found for tile_states_modes_dnd:2 (726072717827778234) --> + <string-array name="tile_states_modes_dnd"> + <item msgid="6509540227356524582">"ไม่พร้อมใช้งาน"</item> + <item msgid="8589336868985358191">"ปิด"</item> + <item msgid="726072717827778234">"เปิด"</item> + </string-array> <string-array name="tile_states_flashlight"> <item msgid="3465257127433353857">"ไม่พร้อมใช้งาน"</item> <item msgid="5044688398303285224">"ปิด"</item> diff --git a/packages/SystemUI/res/values-tl/strings.xml b/packages/SystemUI/res/values-tl/strings.xml index 1f251758db94..3625e3a97dba 100644 --- a/packages/SystemUI/res/values-tl/strings.xml +++ b/packages/SystemUI/res/values-tl/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"I-click para magpares ng bagong device"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Hindi ma-update ang preset"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Preset"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Default na mikropono para sa mga tawag"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Mikropono ng hearing aid"</item> + <item msgid="8501466270452446450">"Ang mikropono ng teleponong ito"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Napili"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Paligid"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Kaliwa"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"Kapag nagshe-share ka ng app, makikita ng <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ang kahit anong ipinapakita o pine-play sa app na iyon. Kaya mag-ingat sa mga bagay-bagay tulad ng mga password, detalye ng pagbabayad, mensahe, larawan, at audio at video."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"Ibahagi ang screen"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"Na-disable ng <xliff:g id="APP_NAME">%1$s</xliff:g> ang opsyong ito"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"Hindi sinusuportahan ng app"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"Pumili ng app na ishe-share"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"I-cast ang iyong screen?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"Mag-cast ng isang app"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Naka-on - Batay sa mukha"</string> <string name="inline_done_button" msgid="6043094985588909584">"Tapos na"</string> <string name="inline_ok_button" msgid="603075490581280343">"Ilapat"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"I-off"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Naka-silent"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Default"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Awtomatiko"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"Icon ng kanan"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Pindutin nang matagal at i-drag para magdagdag ng mga tile"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Pindutin nang matagal at i-drag para ayusin ulit ang tile"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"I-drag dito upang alisin"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Kailangan mo ng kahit <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> (na) tile"</string> <string name="qs_edit" msgid="5583565172803472437">"I-edit"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"Iba pa"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"i-toggle ang laki ng tile"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"alisin ang tile"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"magdagdag ng tile sa huling posisyon"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Ilipat ang tile"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Magdagdag ng tile sa gustong posisyon"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Hindi Alam"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"I-reset ang lahat ng tile?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Magre-reset sa mga orihinal na setting ng device ang lahat ng tile ng Mga Mabilisang Setting"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-tr/strings.xml b/packages/SystemUI/res/values-tr/strings.xml index 4748cc4dad54..f2d2ae889657 100644 --- a/packages/SystemUI/res/values-tr/strings.xml +++ b/packages/SystemUI/res/values-tr/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Yeni cihaz eşlemek için tıklayın"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Hazır ayar güncellenemedi"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Hazır Ayar"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Aramalar için varsayılan mikrofon"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"İşitme cihazı mikrofonu"</item> + <item msgid="8501466270452446450">"Bu telefonun mikrofonu"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Seçili"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Çevredeki sesler"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Sol"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Açık - Yüze göre"</string> <string name="inline_done_button" msgid="6043094985588909584">"Bitti"</string> <string name="inline_ok_button" msgid="603075490581280343">"Uygula"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Kapat"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Sessiz"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Varsayılan"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Otomatik"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Sağ simge"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Kutu eklemek için basılı tutup sürükleyin"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Kutuları yeniden düzenlemek için basılı tutun ve sürükleyin"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Kaldırmak için buraya sürükleyin"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"En az <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> kutu gerekiyor"</string> <string name="qs_edit" msgid="5583565172803472437">"Düzenle"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Diğer"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"kutu boyutunu değiştir"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"Kutuyu kaldırmak için"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"kutuyu son konuma ekleyin"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Kutuyu taşı"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"kutuyu istediğiniz konuma ekleyin"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Bilinmiyor"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Tüm ayar kutuları sıfırlansın mı?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Tüm Hızlı Ayarlar kutuları cihazın özgün ayarlarına sıfırlanır"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-uk/strings.xml b/packages/SystemUI/res/values-uk/strings.xml index d89467cf5e23..d9dc96b05152 100644 --- a/packages/SystemUI/res/values-uk/strings.xml +++ b/packages/SystemUI/res/values-uk/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Натисніть, щоб підключити новий пристрій"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Не вдалось оновити набір налаштувань"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Набір налаштувань"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Мікрофон за умовчанням для дзвінків"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Мікрофон слухового апарата"</item> + <item msgid="8501466270452446450">"Мікрофон цього телефона"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Вибрано"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Звуки оточення"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Ліворуч"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Увімкнути (за обличчям)"</string> <string name="inline_done_button" msgid="6043094985588909584">"Готово"</string> <string name="inline_ok_button" msgid="603075490581280343">"Застосувати"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Вимкнути"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Без звуку"</string> <string name="notification_alert_title" msgid="3656229781017543655">"За умовчанням"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Автоматично"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Значок праворуч"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Перетягніть потрібні елементи, щоб додати їх"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Щоб змінити порядок елементів, перетягуйте їх"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Перетягніть сюди, щоб видалити"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Мінімальна кількість фрагментів: <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g>"</string> <string name="qs_edit" msgid="5583565172803472437">"Редагувати"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Інше"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"змінити розмір плитки"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"вилучити опцію"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"додати панель на останню позицію"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Перемістити опцію"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"додати панель на потрібну позицію"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Невідомо"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Скинути всі панелі?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Усі панелі швидких налаштувань буде скинуто до стандартних налаштувань пристрою"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-uk/tiles_states_strings.xml b/packages/SystemUI/res/values-uk/tiles_states_strings.xml index a31e858074e0..f5b0f80d32f7 100644 --- a/packages/SystemUI/res/values-uk/tiles_states_strings.xml +++ b/packages/SystemUI/res/values-uk/tiles_states_strings.xml @@ -56,9 +56,11 @@ <item msgid="5376619709702103243">"Вимкнено"</item> <item msgid="4875147066469902392">"Увімкнено"</item> </string-array> - <!-- no translation found for tile_states_modes_dnd:0 (6509540227356524582) --> - <!-- no translation found for tile_states_modes_dnd:1 (8589336868985358191) --> - <!-- no translation found for tile_states_modes_dnd:2 (726072717827778234) --> + <string-array name="tile_states_modes_dnd"> + <item msgid="6509540227356524582">"Недоступно"</item> + <item msgid="8589336868985358191">"Вимкнено"</item> + <item msgid="726072717827778234">"Увімкнено"</item> + </string-array> <string-array name="tile_states_flashlight"> <item msgid="3465257127433353857">"Недоступно"</item> <item msgid="5044688398303285224">"Вимкнено"</item> diff --git a/packages/SystemUI/res/values-ur/strings.xml b/packages/SystemUI/res/values-ur/strings.xml index 51950e099640..0658c23e817e 100644 --- a/packages/SystemUI/res/values-ur/strings.xml +++ b/packages/SystemUI/res/values-ur/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"نئے آلے کا جوڑا بنانے کے لیے کلک کریں"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"پہلے سے ترتیب شدہ کو اپ ڈیٹ نہیں کیا جا سکا"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"پہلے سے ترتیب شدہ"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"کالز کے لیے ڈیفالٹ مائیکروفون"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"سماعتی آلہ مائیکروفون"</item> + <item msgid="8501466270452446450">"اس فون کا مائیکروفون"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"منتخب کردہ"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"اطراف"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"دائیں"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"آن - چہرے پر مبنی"</string> <string name="inline_done_button" msgid="6043094985588909584">"ہو گیا"</string> <string name="inline_ok_button" msgid="603075490581280343">"لاگو کریں"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"آف کریں"</string> <string name="notification_silence_title" msgid="8608090968400832335">"خاموش"</string> <string name="notification_alert_title" msgid="3656229781017543655">"ڈیفالٹ"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"خودکار"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"دائيں جانب کا آئيکن"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"ٹائلز شامل کرنے کے لئے پکڑ کر گھسیٹیں"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"ٹائلز کو دوبارہ ترتیب دینے کیلئے پکڑ کر گھسیٹیں"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"ہٹانے کیلئے یہاں گھسیٹیں؟"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"آپ کو کم از کم <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> ٹائلز کی ضرورت ہے"</string> <string name="qs_edit" msgid="5583565172803472437">"ترمیم کریں"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"دیگر"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"ٹائل کے سائز کو ٹوگل کریں"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"ٹائل ہٹائیں"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"ٹائل کو آخری پوزیشن پر شامل کریں"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"ٹائل منتقل کریں"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"ٹائل کو مطلوبہ پوزیشن پر شامل کریں"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"نامعلوم"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"سبھی ٹائلز ری سیٹ کریں؟"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"سبھی فوری ترتیبات کی ٹائلز آلہ کی اصل ترتیبات پر ری سیٹ ہو جائیں گی"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>، <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-uz/strings.xml b/packages/SystemUI/res/values-uz/strings.xml index 8d750dd8ad08..3521d16d1e20 100644 --- a/packages/SystemUI/res/values-uz/strings.xml +++ b/packages/SystemUI/res/values-uz/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Yangi qurilmani ulash uchun bosing"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Andoza yangilanmadi"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Andoza"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Chaqiruvlar uchun asosiy mikrofon"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Eshitish moslamasi mikrofoni"</item> + <item msgid="8501466270452446450">"Bu telefonning mikrofoni"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Tanlangan"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Atrof-muhit"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Chap"</string> @@ -586,8 +587,7 @@ <string name="media_projection_entry_app_permission_dialog_warning_single_app" msgid="7094417930857938876">"Ilovani namoyish qilayotganingizda oʻsha ilova ichida koʻrsatilayotgan yoki ijro qilinayotganlar <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>ga koʻrinadi. Shu sababli parollar, toʻlov tafsilotlari, xabarlar, suratlar, audio va video chiqmasligi uchun ehtiyot boʻling."</string> <string name="media_projection_entry_app_permission_dialog_continue_entire_screen" msgid="1850848182344377579">"Ekranni namoyish qilish"</string> <string name="media_projection_entry_app_permission_dialog_single_app_disabled" msgid="8999903044874669995">"<xliff:g id="APP_NAME">%1$s</xliff:g> bu sozlamani faolsizlantirgan"</string> - <!-- no translation found for media_projection_entry_app_permission_dialog_single_app_not_supported (4860247304058870233) --> - <skip /> + <string name="media_projection_entry_app_permission_dialog_single_app_not_supported" msgid="4860247304058870233">"Ilova tomonidan dastaklanmaydi"</string> <string name="media_projection_entry_share_app_selector_title" msgid="1419515119767501822">"Ulashiladigan ilovani tanlash"</string> <string name="media_projection_entry_cast_permission_dialog_title" msgid="752756942658159416">"Ekraningiz uzatilsinmi?"</string> <string name="media_projection_entry_cast_permission_dialog_option_text_single_app" msgid="6073353940838561981">"Bitta ilovani uzatish"</string> @@ -800,8 +800,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Yoqish - Yuz asosida"</string> <string name="inline_done_button" msgid="6043094985588909584">"Tayyor"</string> <string name="inline_ok_button" msgid="603075490581280343">"Tatbiq etish"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Faolsizlantirish"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Sokin"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Standart"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Avtomatik"</string> @@ -983,6 +982,8 @@ <string name="right_icon" msgid="1103955040645237425">"O‘ngga belgisi"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Keraklisini ushlab torting"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Qayta tartiblash uchun ushlab torting"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"O‘chirish uchun bu yerga torting"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Kamida <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> ta katakcha lozim"</string> <string name="qs_edit" msgid="5583565172803472437">"Tahrirlash"</string> @@ -1003,6 +1004,10 @@ <string name="other" msgid="429768510980739978">"Boshqa"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"katak oʻlchamini almashtirish"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"katakchani olib tashlash"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"kartochkani oxirgi oʻringa qoʻshish"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Katakchani boshqa joyga olish"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Kartochkani kerakli oʻringa qoʻshish"</string> @@ -1578,5 +1583,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Noaniq"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Barcha katakchalar asliga qaytarilsinmi?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Barcha Tezkor sozlamalar katakchalari qurilmaning asl sozlamalariga qaytariladi"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-vi/strings.xml b/packages/SystemUI/res/values-vi/strings.xml index 300dea9bb021..cf36d6908c15 100644 --- a/packages/SystemUI/res/values-vi/strings.xml +++ b/packages/SystemUI/res/values-vi/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Nhấp để ghép nối thiết bị mới"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Không cập nhật được giá trị đặt trước"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Chế độ đặt sẵn"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Micrô mặc định cho cuộc gọi"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Micrô trên thiết bị trợ thính"</item> + <item msgid="8501466270452446450">"Micrô của điện thoại này"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Đã chọn"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Âm lượng xung quanh"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Trái"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Đang bật – Dựa trên khuôn mặt"</string> <string name="inline_done_button" msgid="6043094985588909584">"Xong"</string> <string name="inline_ok_button" msgid="603075490581280343">"Áp dụng"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Tắt"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Im lặng"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Mặc định"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Tự động"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"Biểu tượng bên phải"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Giữ và kéo để thêm ô"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Giữ và kéo để sắp xếp lại các ô"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Kéo vào đây để xóa"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Bạn cần ít nhất <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> ô"</string> <string name="qs_edit" msgid="5583565172803472437">"Chỉnh sửa"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"Khác"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"bật hoặc tắt kích thước của ô"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"xóa ô"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"thêm ô vào vị trí cuối cùng"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Di chuyển ô"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Thêm ô vào vị trí mong muốn"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Không xác định"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Đặt lại mọi ô?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Mọi ô Cài đặt nhanh sẽ được đặt lại về chế độ cài đặt ban đầu của thiết bị"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-zh-rCN/strings.xml b/packages/SystemUI/res/values-zh-rCN/strings.xml index dacca692fb65..e469e38fb754 100644 --- a/packages/SystemUI/res/values-zh-rCN/strings.xml +++ b/packages/SystemUI/res/values-zh-rCN/strings.xml @@ -106,7 +106,7 @@ <string name="backlinks_include_link" msgid="4562093591148248158">"包括链接"</string> <string name="backlinks_duplicate_label_format" msgid="558445128952827926">"<xliff:g id="APPNAME">%1$s</xliff:g> <xliff:g id="FREQUENCYCOUNT">(%2$d)</xliff:g>"</string> <string name="backlinks_cross_profile_error" msgid="1355798585727802282">"无法添加来自其他个人资料的链接"</string> - <string name="screenrecord_title" msgid="4257171601439507792">"屏幕录制器"</string> + <string name="screenrecord_title" msgid="4257171601439507792">"屏幕录制"</string> <string name="screenrecord_background_processing_label" msgid="7244617554884238898">"正在处理屏幕录制视频"</string> <string name="screenrecord_channel_description" msgid="4147077128486138351">"持续显示屏幕录制会话通知"</string> <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"要录制屏幕吗?"</string> @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"点击即可与新设备配对"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"无法更新预设"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"预设"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"通话的默认麦克风"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"助听器麦克风"</item> + <item msgid="8501466270452446450">"此手机的麦克风"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"已选择"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"周围声音"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"左侧"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"已开启 - 基于人脸"</string> <string name="inline_done_button" msgid="6043094985588909584">"完成"</string> <string name="inline_ok_button" msgid="603075490581280343">"应用"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"关闭"</string> <string name="notification_silence_title" msgid="8608090968400832335">"静音"</string> <string name="notification_alert_title" msgid="3656229781017543655">"默认"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"自动"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"向右图标"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"按住并拖动即可添加功能块"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"按住并拖动即可重新排列功能块"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"拖动到此处即可移除"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"您至少需要 <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> 个卡片"</string> <string name="qs_edit" msgid="5583565172803472437">"编辑"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"其他"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"切换功能块大小"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"移除功能块"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"将功能块添加到最后一个位置"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"移动功能块"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"将功能块添加到所需位置"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"未知"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"要重置所有功能块吗?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"所有“快捷设置”功能块都将重置为设备的原始设置"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>,<xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-zh-rCN/tiles_states_strings.xml b/packages/SystemUI/res/values-zh-rCN/tiles_states_strings.xml index 5980c314d0d6..b0fd9be8b2fd 100644 --- a/packages/SystemUI/res/values-zh-rCN/tiles_states_strings.xml +++ b/packages/SystemUI/res/values-zh-rCN/tiles_states_strings.xml @@ -56,9 +56,11 @@ <item msgid="5376619709702103243">"已关闭"</item> <item msgid="4875147066469902392">"已开启"</item> </string-array> - <!-- no translation found for tile_states_modes_dnd:0 (6509540227356524582) --> - <!-- no translation found for tile_states_modes_dnd:1 (8589336868985358191) --> - <!-- no translation found for tile_states_modes_dnd:2 (726072717827778234) --> + <string-array name="tile_states_modes_dnd"> + <item msgid="6509540227356524582">"不可用"</item> + <item msgid="8589336868985358191">"关"</item> + <item msgid="726072717827778234">"开"</item> + </string-array> <string-array name="tile_states_flashlight"> <item msgid="3465257127433353857">"不可用"</item> <item msgid="5044688398303285224">"已关闭"</item> diff --git a/packages/SystemUI/res/values-zh-rHK/strings.xml b/packages/SystemUI/res/values-zh-rHK/strings.xml index afc33d9611b9..4a1291e07894 100644 --- a/packages/SystemUI/res/values-zh-rHK/strings.xml +++ b/packages/SystemUI/res/values-zh-rHK/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"㩒一下就可以配對新裝置"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"無法更新預設"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"預設"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"預設通話用麥克風"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"助聽器麥克風"</item> + <item msgid="8501466270452446450">"此手機的麥克風"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"揀咗"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"環境聲音"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"左"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"已開啟 - 根據面孔偵測"</string> <string name="inline_done_button" msgid="6043094985588909584">"完成"</string> <string name="inline_ok_button" msgid="603075490581280343">"套用"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"關閉"</string> <string name="notification_silence_title" msgid="8608090968400832335">"靜音"</string> <string name="notification_alert_title" msgid="3656229781017543655">"預設"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"自動"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"向右圖示"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"按住並拖曳即可新增圖塊"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"按住並拖曳即可重新排列圖塊"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"拖曳這裡即可移除"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"你需要有至少 <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> 個資訊方塊"</string> <string name="qs_edit" msgid="5583565172803472437">"編輯"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"其他"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"切換圖塊大小"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"移除圖塊"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"加圖塊去上一個位置"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"移動圖塊"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"加圖塊去目標位置"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"不明"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"要重設所有圖塊嗎?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"所有「快速設定」圖塊將重設為裝置的原始設定"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>,<xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-zh-rTW/strings.xml b/packages/SystemUI/res/values-zh-rTW/strings.xml index 3ea590a17188..89f2018cfed4 100644 --- a/packages/SystemUI/res/values-zh-rTW/strings.xml +++ b/packages/SystemUI/res/values-zh-rTW/strings.xml @@ -419,10 +419,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"按一下即可配對新裝置"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"無法更新預設設定"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"預設"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"預設通話麥克風"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"助聽器麥克風"</item> + <item msgid="8501466270452446450">"這支手機的麥克風"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"已選取"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"環境"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"左"</string> @@ -800,8 +801,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"已開啟 - 依臉部方向旋轉"</string> <string name="inline_done_button" msgid="6043094985588909584">"完成"</string> <string name="inline_ok_button" msgid="603075490581280343">"套用"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"關閉"</string> <string name="notification_silence_title" msgid="8608090968400832335">"靜音"</string> <string name="notification_alert_title" msgid="3656229781017543655">"預設"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"自動"</string> @@ -983,6 +983,8 @@ <string name="right_icon" msgid="1103955040645237425">"向右圖示"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"按住並拖曳即可新增設定方塊"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"按住並拖曳即可重新排列設定方塊"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"拖曳到這裡即可移除"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"你至少必須要有 <xliff:g id="MIN_NUM_TILES">%1$d</xliff:g> 個設定方塊"</string> <string name="qs_edit" msgid="5583565172803472437">"編輯"</string> @@ -1003,6 +1005,10 @@ <string name="other" msgid="429768510980739978">"其他"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"切換設定方塊大小"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"移除圖塊"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"將設定方塊新增到最後一個位置"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"移動圖塊"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"將設定方塊新增到所需位置"</string> @@ -1578,5 +1584,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"不明"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"要重設所有設定方塊嗎?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"所有快速設定方塊都會恢復裝置的原始設定"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>、<xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values-zu/strings.xml b/packages/SystemUI/res/values-zu/strings.xml index 767adbb25c25..3bef836a9195 100644 --- a/packages/SystemUI/res/values-zu/strings.xml +++ b/packages/SystemUI/res/values-zu/strings.xml @@ -421,10 +421,11 @@ <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Chofoza ukuze ubhangqe idivayisi entsha"</string> <string name="hearing_devices_presets_error" msgid="350363093458408536">"Ayikwazanga ukubuyekeza ukusetha ngaphambilini"</string> <string name="hearing_devices_preset_label" msgid="7878267405046232358">"Ukusetha ngaphambilini"</string> - <!-- no translation found for hearing_devices_input_routing_label (730396728151232306) --> - <skip /> - <!-- no translation found for hearing_device_input_routing_options:0 (4582190415045337003) --> - <!-- no translation found for hearing_device_input_routing_options:1 (8501466270452446450) --> + <string name="hearing_devices_input_routing_label" msgid="730396728151232306">"Imakrofoni ezenzekelayo yocingo"</string> + <string-array name="hearing_device_input_routing_options"> + <item msgid="4582190415045337003">"Imakrofoni yomshini wendlebe"</item> + <item msgid="8501466270452446450">"Imakrofoni yale foni"</item> + </string-array> <string name="hearing_devices_spinner_item_selected" msgid="3137083889662762383">"Okukhethiwe"</string> <string name="hearing_devices_ambient_label" msgid="629440938614895797">"Izindawo ezizungezile"</string> <string name="hearing_devices_ambient_control_left" msgid="3586965448230412600">"Kwesokunxele"</string> @@ -802,8 +803,7 @@ <string name="rotation_lock_camera_rotation_on" msgid="789434807790534274">"Vuliwe - Kususelwe kubuso"</string> <string name="inline_done_button" msgid="6043094985588909584">"Kwenziwe"</string> <string name="inline_ok_button" msgid="603075490581280343">"Faka"</string> - <!-- no translation found for inline_turn_off_notifications (2653064779176881329) --> - <skip /> + <string name="inline_turn_off_notifications" msgid="2653064779176881329">"Vala"</string> <string name="notification_silence_title" msgid="8608090968400832335">"Kuthulile"</string> <string name="notification_alert_title" msgid="3656229781017543655">"Okuzenzekelayo"</string> <string name="notification_automatic_title" msgid="3745465364578762652">"Okuzenzekelayo"</string> @@ -985,6 +985,8 @@ <string name="right_icon" msgid="1103955040645237425">"Isithonjana sangakwesokudla"</string> <string name="drag_to_add_tiles" msgid="8933270127508303672">"Bamba uphinde uhudule ukuze ungeze amathayela"</string> <string name="drag_to_rearrange_tiles" msgid="2143204300089638620">"Bamba uphinde uhudule ukuze uphinde ulungise amathayela"</string> + <!-- no translation found for tap_to_position_tile (6282815817773342757) --> + <skip /> <string name="drag_to_remove_tiles" msgid="4682194717573850385">"Hudulela lapha ukuze ususe"</string> <string name="drag_to_remove_disabled" msgid="933046987838658850">"Udinga okungenani amathayela angu-<xliff:g id="MIN_NUM_TILES">%1$d</xliff:g>"</string> <string name="qs_edit" msgid="5583565172803472437">"Hlela"</string> @@ -1005,6 +1007,10 @@ <string name="other" msgid="429768510980739978">"Okunye"</string> <string name="accessibility_qs_edit_toggle_tile_size_action" msgid="1485194410119733586">"guqula usayizi wethayela"</string> <string name="accessibility_qs_edit_remove_tile_action" msgid="775511891457193480">"susa ithayela"</string> + <!-- no translation found for accessibility_qs_edit_toggle_placement_mode (3870429389210569610) --> + <skip /> + <!-- no translation found for accessibility_qs_edit_toggle_selection (2201248304072372239) --> + <skip /> <string name="accessibility_qs_edit_tile_add_action" msgid="8311378984458545661">"faka ithayela endaweni yokugcina"</string> <string name="accessibility_qs_edit_tile_start_move" msgid="2009373939914517817">"Hambisa ithayela"</string> <string name="accessibility_qs_edit_tile_start_add" msgid="8141710006899065161">"Faka ithayela endaweni oyifunayo"</string> @@ -1580,5 +1586,7 @@ <string name="qs_edit_mode_category_unknown" msgid="509314252124053550">"Akwaziwa"</string> <string name="qs_edit_mode_reset_dialog_title" msgid="5344853290033761627">"Qala kabusha onke amathayela?"</string> <string name="qs_edit_mode_reset_dialog_content" msgid="7474773130622653653">"Ithayela Lamasethingi Asheshayo lizosetha kabusha libuyele kumasethingi okuqala edivayisi"</string> + <!-- no translation found for demote_explain_text (1600426458580544250) --> + <skip /> <string name="volume_slider_disabled_message_template" msgid="1305088816797803460">"<xliff:g id="STREAM_NAME">%1$s</xliff:g>, <xliff:g id="DISABLED_MESSAGE">%2$s</xliff:g>"</string> </resources> diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 7c6a1b1bf63d..cb656ca0a108 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -145,9 +145,10 @@ <color name="smart_reply_button_background">#ffffffff</color> <color name="smart_reply_button_stroke">@*android:color/accent_device_default</color> - <!-- Magic Action colors --> - <color name="magic_action_button_text_color">@androidprv:color/materialColorOnSurface</color> - <color name="magic_action_button_stroke_color">@androidprv:color/materialColorOnSurface</color> + <!-- Animated Action colors --> + <color name="animated_action_button_text_color">@androidprv:color/materialColorOnSurface</color> + <color name="animated_action_button_stroke_color">@androidprv:color/materialColorOnSurface</color> + <color name="animated_action_button_attribution_color">@androidprv:color/materialColorOnSurfaceVariant</color> <!-- Biometric dialog colors --> <color name="biometric_dialog_gray">#ff757575</color> @@ -209,6 +210,16 @@ <color name="media_dialog_seekbar_progress">@android:color/system_accent1_200</color> <color name="media_dialog_button_background">@color/material_dynamic_primary40</color> <color name="media_dialog_solid_button_text">@color/material_dynamic_neutral95</color> + <color name="media_dialog_primary">@android:color/system_primary_light</color> + <color name="media_dialog_on_primary">@android:color/system_on_primary_light</color> + <color name="media_dialog_secondary">@android:color/system_secondary_light</color> + <color name="media_dialog_secondary_container">@android:color/system_secondary_container_light</color> + <color name="media_dialog_surface_container">@android:color/system_surface_container_light</color> + <color name="media_dialog_surface_container_high">@android:color/system_surface_container_high_light</color> + <color name="media_dialog_on_surface">@android:color/system_on_surface_light</color> + <color name="media_dialog_on_surface_variant">@android:color/system_on_surface_variant_light</color> + <color name="media_dialog_outline">@android:color/system_outline_light</color> + <color name="media_dialog_outline_variant">@android:color/system_outline_variant_light</color> <!-- controls --> <color name="control_primary_text">#E6FFFFFF</color> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 55e94028b95e..f062bd1d4990 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -300,6 +300,9 @@ <!-- Side padding on the side of notifications --> <dimen name="notification_side_paddings">16dp</dimen> + <!-- Width of inline notification menu item buttons --> + <dimen name="notification_menu_item_width">112dp</dimen> + <!-- Starting translateY offset of the HUN appear and disappear animations. Indicates the amount by the view is positioned above the screen before the animation starts. --> <dimen name="heads_up_appear_y_above_screen">32dp</dimen> @@ -370,10 +373,12 @@ <dimen name="min_notification_layout_height">48dp</dimen> <!-- Size of the space to place a notification menu item --> - <dimen name="notification_menu_icon_size">64dp</dimen> + <dimen name="notification_menu_icon_size">120dp</dimen> <!-- The space around a notification menu item --> <dimen name="notification_menu_icon_padding">20dp</dimen> + <!-- The space around a notification menu button --> + <dimen name="notification_menu_button_padding">8dp</dimen> <!-- scroll view the size of 3 channel rows --> <dimen name="notification_blocker_channel_list_height">192dp</dimen> @@ -1160,16 +1165,16 @@ <dimen name="notification_2025_smart_reply_button_corner_radius">18dp</dimen> <dimen name="notification_2025_smart_reply_button_min_height">48dp</dimen> - <!-- Magic Action params. --> + <!-- Animated Action params. --> <!-- Corner radius = half of min_height to create rounded sides. --> - <dimen name="magic_action_button_corner_radius">16dp</dimen> - <dimen name="magic_action_button_icon_size">20dp</dimen> - <dimen name="magic_action_button_outline_stroke_width">1dp</dimen> - <dimen name="magic_action_button_padding_horizontal">12dp</dimen> - <dimen name="magic_action_button_inset_vertical">8dp</dimen> - <dimen name="magic_action_button_drawable_padding">8dp</dimen> - <dimen name="magic_action_button_touch_target_height">48dp</dimen> - <dimen name="magic_action_button_font_size">12sp</dimen> + <dimen name="animated_action_button_corner_radius">16dp</dimen> + <dimen name="animated_action_button_icon_size">20dp</dimen> + <dimen name="animated_action_button_outline_stroke_width">1dp</dimen> + <dimen name="animated_action_button_padding_horizontal">12dp</dimen> + <dimen name="animated_action_button_inset_vertical">8dp</dimen> + <dimen name="animated_action_button_drawable_padding">8dp</dimen> + <dimen name="animated_action_button_touch_target_height">48dp</dimen> + <dimen name="animated_action_button_font_size">12sp</dimen> <!-- A reasonable upper bound for the height of the smart reply button. The measuring code needs to start with a guess for the maximum size. Currently two-line smart reply buttons @@ -1562,8 +1567,20 @@ <dimen name="media_output_dialog_item_height">64dp</dimen> <dimen name="media_output_dialog_margin_horizontal">16dp</dimen> <dimen name="media_output_dialog_list_padding_top">8dp</dimen> + <dimen name="media_output_dialog_app_icon_size">16dp</dimen> + <dimen name="media_output_dialog_app_icon_bottom_margin">11dp</dimen> <dimen name="media_output_dialog_icon_left_radius">@dimen/media_output_dialog_active_background_radius</dimen> <dimen name="media_output_dialog_icon_right_radius">0dp</dimen> + <dimen name="media_output_dialog_corner_radius">20dp</dimen> + <dimen name="media_output_dialog_button_gap">8dp</dimen> + <dimen name="media_output_item_content_vertical_margin">8dp</dimen> + <dimen name="media_output_item_content_vertical_margin_active">4dp</dimen> + <dimen name="media_output_item_horizontal_gap">12dp</dimen> + <dimen name="media_output_item_icon_size">40dp</dimen> + <dimen name="media_output_item_icon_padding">8dp</dimen> + <dimen name="media_output_item_expand_icon_width">28dp</dimen> + <dimen name="media_output_item_expand_icon_height">20dp</dimen> + <item name="media_output_item_subtitle_alpha" format="float" type="dimen">0.8</item> <!-- Distance that the full shade transition takes in order to complete by tapping on a button like "expand". --> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 681bd53f1a40..bbf56936d560 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -592,6 +592,9 @@ <!-- Content description of the button to expand the group of devices. [CHAR LIMIT=NONE] --> <string name="accessibility_expand_group">Expand group.</string> + <!-- Content description of the button to collapse the group of devices. [CHAR LIMIT=NONE] --> + <string name="accessibility_collapse_group">Collapse group.</string> + <!-- Content description of the button to add a device to a group. [CHAR LIMIT=NONE] --> <string name="accessibility_add_device_to_group">Add device to group.</string> @@ -2124,6 +2127,9 @@ <!-- [CHAR LIMIT=30] Text shown in button used to prevent app from showing Live Updates, for this notification and all future ones --> <string name="notification_inline_disable_promotion">Don\'t show as pinned</string> + <!-- [CHAR LIMIT=30] Text shown in button used to prevent app from showing Live Updates, for this notification and all future ones --> + <string name="notification_inline_disable_promotion_button">Block Live Updates from this app</string> + <!-- Text accompanying the "Show live updates" switch explaining the purpose of the setting --> <string name="live_notifications_title">Showing Live Updates</string> @@ -2351,8 +2357,8 @@ <string name="group_system_access_all_apps_search">Open apps list</string> <!-- User visible title for the keyboard shortcut that accesses [system] settings. [CHAR LIMIT=70] --> <string name="group_system_access_system_settings">Open settings</string> - <!-- User visible title for the keyboard shortcut that accesses Assistant app. [CHAR LIMIT=70] --> - <string name="group_system_access_google_assistant">Open assistant</string> + <!-- User visible title for the keyboard shortcut that accesses the default digital assistant app. [CHAR LIMIT=70] --> + <string name="group_system_access_google_assistant">Open digital assistant</string> <!-- User visible title for the keyboard shortcut that locks screen. [CHAR LIMIT=70] --> <string name="group_system_lock_screen">Lock screen</string> <!-- User visible title for the keyboard shortcut that pulls up Notes app for quick memo. [CHAR LIMIT=70] --> @@ -3290,6 +3296,8 @@ <string name="media_output_dialog_connect_failed">Can\'t switch. Tap to try again.</string> <!-- Title for connecting item [CHAR LIMIT=60] --> <string name="media_output_dialog_pairing_new">Connect a device</string> + <!-- Button text for connecting a new device [CHAR LIMIT=60] --> + <string name="media_output_dialog_button_connect_device">Connect Device</string> <!-- App name when can't get app name [CHAR LIMIT=60] --> <string name="media_output_dialog_unknown_launch_app_name">Unknown app</string> <!-- Button text for stopping casting [CHAR LIMIT=60] --> @@ -3300,6 +3308,8 @@ <string name="media_output_dialog_accessibility_seekbar">Volume</string> <!-- Summary for media output volume of a device in percentage [CHAR LIMIT=NONE] --> <string name="media_output_dialog_volume_percentage"><xliff:g id="percentage" example="10">%1$d</xliff:g>%%</string> + <!-- Title for Connected speakers expandable group. [CHAR LIMIT=NONE] --> + <string name="media_output_group_title_connected_speakers">Connected speakers</string> <!-- Title for Speakers and Displays group. [CHAR LIMIT=NONE] --> <string name="media_output_group_title_speakers_and_displays">Speakers & Displays</string> <!-- Title for Suggested Devices group. [CHAR LIMIT=NONE] --> @@ -3449,6 +3459,10 @@ <string name="keyguard_try_fingerprint">Use fingerprint to open</string> <!-- Accessibility announcement to inform user to unlock using the fingerprint sensor [CHAR LIMIT=NONE] --> <string name="accessibility_fingerprint_bouncer">Authentication required. Touch the fingerprint sensor to authenticate.</string> + <!-- Accessibility action label for resuming animation --> + <string name="resume_animation">Resume animation</string> + <!-- Accessibility action label for pausing animation --> + <string name="pause_animation">Pause animation</string> <!-- Content description for a chip in the status bar showing that the user is currently on a call. [CHAR LIMIT=NONE] --> <string name="ongoing_call_content_description">Ongoing call</string> @@ -4210,9 +4224,9 @@ </string> - <!-- Content of the Reset Tiles dialog in QS Edit mode. [CHAR LIMIT=NONE] --> + <!-- Content of interstitial shown after user revokes app permission to post Live Updates. [CHAR LIMIT=NONE] --> <string name="demote_explain_text"> - <xliff:g id="application" example= "Superfast Food Delivery">%1$s</xliff:g> will no longer show Live Updates here. You can change this any time in Settings. + <xliff:g id="application" example= "Superfast Food Delivery">%1$s</xliff:g> will no longer show Live Updates. You can change this any time in Settings. </string> <!-- Template that joins disabled message with the label for the voice over. [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index a479f1841ca4..fb72123a0a3b 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -17,18 +17,15 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> - <style name="TextAppearance.StatusBar.Clock" parent="@*android:style/TextAppearance.StatusBar.Icon"> + <style name="TextAppearance.StatusBar.Default" parent="@*android:style/TextAppearance.StatusBar.Icon"> <item name="android:textSize">@dimen/status_bar_clock_size</item> <item name="android:fontFamily" android:featureFlag="!com.android.systemui.status_bar_font_updates">@*android:string/config_headlineFontFamilyMedium</item> <item name="android:fontFamily" android:featureFlag="com.android.systemui.status_bar_font_updates">"variable-label-large-emphasized"</item> <item name="android:textColor">@color/status_bar_clock_color</item> - <item name="android:fontFeatureSettings">tnum</item> </style> - <style name="TextAppearance.StatusBar.Carrier" parent="@*android:style/TextAppearance.StatusBar.Icon"> - <item name="android:textSize">@dimen/status_bar_clock_size</item> - <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item> - <item name="android:textColor">@color/status_bar_clock_color</item> + <style name="TextAppearance.StatusBar.Default.Clock"> + <item name="android:fontFeatureSettings">tnum</item> </style> <style name="TextAppearance.StatusBar.UserChip" parent="@*android:style/TextAppearance.StatusBar.Icon"> @@ -184,15 +181,19 @@ <item name="android:textColor">@androidprv:color/materialColorOnSurfaceVariant</item> </style> - <!-- This is hard coded to be sans-serif-condensed to match the icons --> - <style name="TextAppearance.QS.Status"> - <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item> + <item name="android:fontFamily" android:featureFlag="!com.android.systemui.shade_header_font_update">@*android:string/config_headlineFontFamily</item> + <item name="android:fontFamily" android:featureFlag="com.android.systemui.shade_header_font_update">variable-body-medium-emphasized</item> <item name="android:textColor">@color/shade_header_text_color</item> <item name="android:textSize">14sp</item> <item name="android:letterSpacing">0.01</item> </style> + <style name="TextAppearance.QS.Status.Clock"> + <item name="android:fontFamily" android:featureFlag="!com.android.systemui.shade_header_font_update">@*android:string/config_headlineFontFamily</item> + <item name="android:fontFamily" android:featureFlag="com.android.systemui.shade_header_font_update">variable-display-small-emphasized</item> + </style> + <style name="TextAppearance.QS.Status.Build"> <item name="android:textColor">?attr/onShadeInactiveVariant</item> </style> @@ -707,6 +708,33 @@ <item name="android:colorBackground">@color/media_dialog_background</item> </style> + <style name="MediaOutput" /> + <style name="MediaOutput.Dialog" /> + <style name="MediaOutput.Dialog.QuickAccessButton" parent="@style/Widget.Material3.Button.OutlinedButton.Icon"> + <item name="theme">@style/Theme.Material3.DynamicColors.DayNight</item> + <item name="android:paddingTop">6dp</item> + <item name="android:minHeight">32dp</item> + <item name="android:paddingBottom">6dp</item> + <item name="android:paddingStart">8dp</item> + <item name="android:paddingEnd">12dp</item> + <item name="android:insetTop">0dp</item> + <item name="android:insetBottom">0dp</item> + <item name="android:textColor">@color/media_dialog_on_surface_variant</item> + <item name="iconSize">18dp</item> + <item name="iconTint">@color/media_dialog_primary</item> + <item name="shapeAppearance">@style/ShapeAppearance.Material3.Corner.Small</item> + <item name="strokeColor">@color/media_dialog_outline_variant</item> + </style> + + <style name="MediaOutput.Item" /> + <style name="MediaOutput.Item.Icon"> + <item name="android:layout_width">@dimen/media_output_item_icon_size</item> + <item name="android:layout_height">@dimen/media_output_item_icon_size</item> + <item name="android:padding">@dimen/media_output_item_icon_padding</item> + <item name="android:scaleType">fitCenter</item> + <item name="tint">@color/media_dialog_on_surface</item> + </style> + <style name="MediaOutputItemInactiveTitle"> <item name="android:textSize">16sp</item> <item name="android:textColor">@color/media_dialog_item_main_content</item> @@ -818,6 +846,14 @@ <item name="android:minWidth">0dp</item> </style> + <style name="TextAppearance.NotificationMenuButtonText"> + <item name="android:textSize">@dimen/notification_importance_header_text</item> + <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item> + <item name="android:textColor">@androidprv:color/materialColorOnSurface</item> + <item name="android:gravity">center</item> + </style> + + <style name="TextAppearance.HeadsUpStatusBarText" parent="@*android:style/TextAppearance.DeviceDefault.Notification.Info"> </style> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java index a518c57bdd16..96307c7c301f 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java @@ -309,6 +309,13 @@ public class Task { taskInfo.topActivity); } + /** + * Creates a task object from the given [taskInfo]. + */ + public static Task from(TaskInfo taskInfo) { + return from(new TaskKey(taskInfo), taskInfo, /* isLocked= */ false); + } + public Task(TaskKey key) { this.key = key; this.taskDescription = new TaskDescription(); diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index f2f177356fab..63189083e22e 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -72,6 +72,7 @@ import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChang import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ZenModeController import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor +import com.android.systemui.util.annotations.DeprecatedSysuiVisibleForTesting import com.android.systemui.util.concurrency.DelayableExecutor import java.util.Locale import java.util.TimeZone @@ -392,8 +393,9 @@ constructor( } } - @VisibleForTesting - internal fun listenForDnd(scope: CoroutineScope): Job { + @DeprecatedSysuiVisibleForTesting + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun listenForDnd(scope: CoroutineScope): Job { ModesUi.unsafeAssertInNewMode() return scope.launch { zenModeInteractor.dndMode.collect { @@ -592,8 +594,9 @@ constructor( dozeAmount.value = doze } - @VisibleForTesting - internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job { + @DeprecatedSysuiVisibleForTesting + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun listenForDozeAmountTransition(scope: CoroutineScope): Job { return scope.launch { merge( keyguardTransitionInteractor.transition(Edge.create(AOD, LOCKSCREEN)).map { @@ -609,8 +612,9 @@ constructor( /** * When keyguard is displayed again after being gone, the clock must be reset to full dozing. */ - @VisibleForTesting - internal fun listenForAnyStateToAodTransition(scope: CoroutineScope): Job { + @DeprecatedSysuiVisibleForTesting + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun listenForAnyStateToAodTransition(scope: CoroutineScope): Job { return scope.launch { keyguardTransitionInteractor .transition(Edge.create(to = AOD)) @@ -620,8 +624,9 @@ constructor( } } - @VisibleForTesting - internal fun listenForAnyStateToLockscreenTransition(scope: CoroutineScope): Job { + @DeprecatedSysuiVisibleForTesting + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun listenForAnyStateToLockscreenTransition(scope: CoroutineScope): Job { return scope.launch { keyguardTransitionInteractor .transition(Edge.create(to = LOCKSCREEN)) @@ -635,8 +640,9 @@ constructor( * When keyguard is displayed due to pulsing notifications when AOD is off, we should make sure * clock is in dozing state instead of LS state */ - @VisibleForTesting - internal fun listenForAnyStateToDozingTransition(scope: CoroutineScope): Job { + @DeprecatedSysuiVisibleForTesting + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun listenForAnyStateToDozingTransition(scope: CoroutineScope): Job { return scope.launch { keyguardTransitionInteractor .transition(Edge.create(to = DOZING)) diff --git a/packages/SystemUI/src/com/android/keyguard/UserActivityNotifier.kt b/packages/SystemUI/src/com/android/keyguard/UserActivityNotifier.kt index 9b1ddb74c3b2..76c032a616ef 100644 --- a/packages/SystemUI/src/com/android/keyguard/UserActivityNotifier.kt +++ b/packages/SystemUI/src/com/android/keyguard/UserActivityNotifier.kt @@ -15,27 +15,30 @@ */ package com.android.keyguard +import android.annotation.SuppressLint import android.os.PowerManager import android.os.SystemClock +import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.UiBackground import java.util.concurrent.Executor import javax.inject.Inject /** Wrapper class for notifying the system about user activity in the background. */ +@SysUISingleton class UserActivityNotifier @Inject constructor( @UiBackground private val uiBgExecutor: Executor, - private val powerManager: PowerManager + private val powerManager: PowerManager, ) { - fun notifyUserActivity() { - uiBgExecutor.execute { - powerManager.userActivity( - SystemClock.uptimeMillis(), - PowerManager.USER_ACTIVITY_EVENT_OTHER, - 0 - ) - } + @SuppressLint("MissingPermission") + @JvmOverloads + fun notifyUserActivity( + timeOfActivity: Long = SystemClock.uptimeMillis(), + event: Int = PowerManager.USER_ACTIVITY_EVENT_OTHER, + flags: Int = 0, + ) { + uiBgExecutor.execute { powerManager.userActivity(timeOfActivity, event, flags) } } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java index 375137c67f7c..63d56e662a50 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java @@ -212,15 +212,13 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks protected MagnificationSettingsController createInstance(Display display) { final Context windowContext = mContext.createWindowContext(display, TYPE_ACCESSIBILITY_OVERLAY, /* options */ null); - final WindowManager windowManager = mWindowManagerProvider - .getWindowManager(windowContext); windowContext.setTheme(com.android.systemui.res.R.style.Theme_SystemUI); return new MagnificationSettingsController( windowContext, new SfVsyncFrameCallbackProvider(), mSettingsControllerCallback, mSecureSettings, - windowManager); + mWindowManagerProvider); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java index 2d5dc8d23383..95206b88f6aa 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java @@ -30,6 +30,7 @@ import com.android.internal.accessibility.common.MagnificationConstants; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; /** * A class to control {@link WindowMagnificationSettings} and receive settings panel callbacks by @@ -61,9 +62,9 @@ public class MagnificationSettingsController implements ComponentCallbacks { SfVsyncFrameCallbackProvider sfVsyncFrameProvider, @NonNull Callback settingsControllerCallback, SecureSettings secureSettings, - WindowManager windowManager) { + WindowManagerProvider windowManagerProvider) { this(context, sfVsyncFrameProvider, settingsControllerCallback, secureSettings, - windowManager, null); + windowManagerProvider, null); } @VisibleForTesting @@ -72,7 +73,7 @@ public class MagnificationSettingsController implements ComponentCallbacks { SfVsyncFrameCallbackProvider sfVsyncFrameProvider, @NonNull Callback settingsControllerCallback, SecureSettings secureSettings, - WindowManager windowManager, + WindowManagerProvider windowManagerProvider, WindowMagnificationSettings windowMagnificationSettings) { mContext = context.createWindowContext( context.getDisplay(), @@ -85,6 +86,7 @@ public class MagnificationSettingsController implements ComponentCallbacks { if (windowMagnificationSettings != null) { mWindowMagnificationSettings = windowMagnificationSettings; } else { + WindowManager windowManager = windowManagerProvider.getWindowManager(mContext); mWindowMagnificationSettings = new WindowMagnificationSettings(mContext, mWindowMagnificationSettingsCallback, sfVsyncFrameProvider, secureSettings, windowManager); diff --git a/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt index a107322423bb..c5cd39ccbc9f 100644 --- a/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt @@ -28,6 +28,7 @@ import android.os.ParcelFileDescriptor import android.os.UserHandle import android.util.Log import com.android.app.tracing.traceSection +import com.android.systemui.Flags import com.android.systemui.backup.BackupHelper.Companion.ACTION_RESTORE_FINISHED import com.android.systemui.communal.data.backup.CommunalBackupHelper import com.android.systemui.communal.data.backup.CommunalBackupUtils @@ -118,7 +119,9 @@ open class BackupHelper : BackupAgentHelper() { } private fun communalEnabled(): Boolean { - return resources.getBoolean(R.bool.config_communalServiceEnabled) + return resources.getBoolean(R.bool.config_communalServiceEnabled) || + (Flags.glanceableHubV2() && + resources.getBoolean(com.android.internal.R.bool.config_glanceableHubEnabled)) } /** diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index dbaa90c10313..9064966b25c6 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -69,7 +69,9 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.InstanceId; import com.android.internal.util.LatencyTracker; import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.keyguard.UserActivityNotifier; import com.android.systemui.Dumpable; +import com.android.systemui.Flags; import com.android.systemui.animation.ActivityTransitionAnimator; import com.android.systemui.biometrics.dagger.BiometricsBackground; import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor; @@ -146,6 +148,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { private final Execution mExecution; private final FingerprintManager mFingerprintManager; @NonNull private final LayoutInflater mInflater; + private final UserActivityNotifier mUserActivityNotifier; private final WindowManager mWindowManager; private final DelayableExecutor mFgExecutor; @NonNull private final Executor mBiometricExecutor; @@ -696,11 +699,13 @@ public class UdfpsController implements DozeReceiver, Dumpable { Lazy<DefaultUdfpsTouchOverlayViewModel> defaultUdfpsTouchOverlayViewModel, @NonNull UdfpsOverlayInteractor udfpsOverlayInteractor, @NonNull PowerInteractor powerInteractor, - @Application CoroutineScope scope) { + @Application CoroutineScope scope, + UserActivityNotifier userActivityNotifier) { mContext = context; mExecution = execution; mVibrator = vibrator; mInflater = inflater; + mUserActivityNotifier = userActivityNotifier; mIgnoreRefreshRate = mContext.getResources() .getBoolean(R.bool.config_ignoreUdfpsVote); // The fingerprint manager is queried for UDFPS before this class is constructed, so the @@ -1045,8 +1050,13 @@ public class UdfpsController implements DozeReceiver, Dumpable { mLatencyTracker.onActionStart(ACTION_UDFPS_ILLUMINATE); } // Refresh screen timeout and boost process priority if possible. - mPowerManager.userActivity(mSystemClock.uptimeMillis(), - PowerManager.USER_ACTIVITY_EVENT_TOUCH, 0); + if (Flags.bouncerUiRevamp()) { + mUserActivityNotifier.notifyUserActivity(mSystemClock.uptimeMillis(), + PowerManager.USER_ACTIVITY_EVENT_TOUCH); + } else { + mPowerManager.userActivity(mSystemClock.uptimeMillis(), + PowerManager.USER_ACTIVITY_EVENT_TOUCH, 0); + } if (!mOnFingerDown) { playStartHaptic(); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt index 8a5e011cd3ce..2bb9809af30e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.biometrics.domain.interactor +import android.annotation.SuppressLint import android.content.Context import android.hardware.fingerprint.FingerprintManager import android.util.Log @@ -32,10 +33,14 @@ import javax.inject.Inject import kotlin.math.max import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -131,6 +136,25 @@ constructor( } .distinctUntilChanged() + /** + * Event flow that emits every time the user taps the screen and a UDFPS guidance message is + * surfaced and then cleared. Modeled as a SharedFlow because a StateFlow fails to emit every + * event to the subscriber, causing missed Talkback feedback and incorrect focusability state of + * the UDFPS accessibility overlay. + */ + @SuppressLint("SharedFlowCreation") + private val _clearAccessibilityOverlayMessageReason = MutableSharedFlow<String?>() + + /** Indicates the reason for clearing the UDFPS accessibility overlay content description */ + val clearAccessibilityOverlayMessageReason: SharedFlow<String?> = + _clearAccessibilityOverlayMessageReason.asSharedFlow() + + suspend fun clearUdfpsAccessibilityOverlayMessage(reason: String) { + // Add delay to make sure we read the guidance message before clearing it + delay(1000) + _clearAccessibilityOverlayMessageReason.emit(reason) + } + companion object { private const val TAG = "UdfpsOverlayInteractor" } 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 3b22e13f29a2..80d06f4a2d37 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 @@ -27,6 +27,7 @@ import android.util.Log import android.view.MotionEvent import android.view.View import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO +import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES import android.view.accessibility.AccessibilityManager import android.widget.Button import android.widget.ImageView @@ -43,7 +44,6 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.airbnb.lottie.LottieAnimationView import com.airbnb.lottie.LottieCompositionFactory -import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.biometrics.Utils.ellipsize import com.android.systemui.biometrics.shared.model.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricModality @@ -63,6 +63,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch private const val TAG = "BiometricViewBinder" @@ -123,25 +124,6 @@ object BiometricViewBinder { val confirmationButton = view.requireViewById<Button>(R.id.button_confirm) val retryButton = view.requireViewById<Button>(R.id.button_try_again) - // Handles custom "Cancel Authentication" talkback action - val cancelDelegate: AccessibilityDelegateCompat = - object : AccessibilityDelegateCompat() { - override fun onInitializeAccessibilityNodeInfo( - host: View, - info: AccessibilityNodeInfoCompat, - ) { - super.onInitializeAccessibilityNodeInfo(host, info) - info.addAction( - AccessibilityActionCompat( - AccessibilityNodeInfoCompat.ACTION_CLICK, - view.context.getString(R.string.biometric_dialog_cancel_authentication), - ) - ) - } - } - ViewCompat.setAccessibilityDelegate(backgroundView, cancelDelegate) - ViewCompat.setAccessibilityDelegate(cancelButton, cancelDelegate) - // TODO(b/330788871): temporary workaround for the unsafe callbacks & legacy controllers val adapter = Spaghetti( @@ -155,6 +137,33 @@ object BiometricViewBinder { var boundSize = false view.repeatWhenAttached { + // Handles custom "Cancel Authentication" talkback action + val cancelDelegate: AccessibilityDelegateCompat = + object : AccessibilityDelegateCompat() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfoCompat, + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + lifecycleScope.launch { + // Clears UDFPS guidance hint after focus moves to cancel view + viewModel.onClearUdfpsGuidanceHint( + accessibilityManager.isTouchExplorationEnabled + ) + } + info.addAction( + AccessibilityActionCompat( + AccessibilityNodeInfoCompat.ACTION_CLICK, + view.context.getString( + R.string.biometric_dialog_cancel_authentication + ), + ) + ) + } + } + ViewCompat.setAccessibilityDelegate(backgroundView, cancelDelegate) + ViewCompat.setAccessibilityDelegate(cancelButton, cancelDelegate) + // these do not change and need to be set before any size transitions val modalities = viewModel.modalities.first() @@ -404,11 +413,16 @@ object BiometricViewBinder { } false } + launch { viewModel.accessibilityHint.collect { message -> - if (message.isNotBlank()) { - udfpsGuidanceView.contentDescription = message - } + udfpsGuidanceView.importantForAccessibility = + if (message == null) { + IMPORTANT_FOR_ACCESSIBILITY_NO + } else { + IMPORTANT_FOR_ACCESSIBILITY_YES + } + udfpsGuidanceView.contentDescription = message } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt index ceb2b10ab517..1d7562ea64b9 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt @@ -25,6 +25,9 @@ import android.view.LayoutInflater import android.view.View import android.view.WindowManager import android.view.accessibility.AccessibilityEvent +import androidx.core.view.AccessibilityDelegateCompat +import androidx.core.view.ViewCompat +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.airbnb.lottie.LottieAnimationView @@ -67,6 +70,59 @@ constructor( private val sfpsSensorInteractor: Lazy<SideFpsSensorInteractor>, private val windowManager: Lazy<WindowManager>, ) : CoreStartable { + private val pauseDelegate: AccessibilityDelegateCompat = + object : AccessibilityDelegateCompat() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfoCompat, + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + info.addAction( + AccessibilityNodeInfoCompat.AccessibilityActionCompat( + AccessibilityNodeInfoCompat.ACTION_CLICK, + host.context.getString(R.string.pause_animation), + ) + ) + } + + override fun dispatchPopulateAccessibilityEvent( + host: View, + event: AccessibilityEvent, + ): Boolean { + return if (event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { + true + } else { + super.dispatchPopulateAccessibilityEvent(host, event) + } + } + } + + private val resumeDelegate: AccessibilityDelegateCompat = + object : AccessibilityDelegateCompat() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfoCompat, + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + info.addAction( + AccessibilityNodeInfoCompat.AccessibilityActionCompat( + AccessibilityNodeInfoCompat.ACTION_CLICK, + host.context.getString(R.string.resume_animation), + ) + ) + } + + override fun dispatchPopulateAccessibilityEvent( + host: View, + event: AccessibilityEvent, + ): Boolean { + return if (event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { + true + } else { + super.dispatchPopulateAccessibilityEvent(host, event) + } + } + } override fun start() { applicationScope.launch { @@ -135,6 +191,7 @@ constructor( overlayView!!.setOnClickListener { v -> v.requireViewById<LottieAnimationView>(R.id.sidefps_animation).toggleAnimation() } + ViewCompat.setAccessibilityDelegate(overlayView!!, pauseDelegate) Log.d(TAG, "show(): adding overlayView $overlayView") windowManager.get().addView(overlayView, overlayViewModel.defaultOverlayViewParams) } @@ -177,29 +234,6 @@ constructor( overlayShowAnimator.start() - /** - * Intercepts TYPE_WINDOW_STATE_CHANGED accessibility event, preventing Talkback - * from speaking @string/accessibility_fingerprint_label twice when sensor location - * indicator is in focus - */ - it.setAccessibilityDelegate( - object : View.AccessibilityDelegate() { - override fun dispatchPopulateAccessibilityEvent( - host: View, - event: AccessibilityEvent, - ): Boolean { - return if ( - event.getEventType() === - AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED - ) { - true - } else { - super.dispatchPopulateAccessibilityEvent(host, event) - } - } - } - ) - repeatOnLifecycle(Lifecycle.State.STARTED) { launch { viewModel.lottieCallbacks.collect { callbacks -> @@ -224,6 +258,16 @@ constructor( } } } + + private fun LottieAnimationView.toggleAnimation() { + if (isAnimating) { + pauseAnimation() + ViewCompat.setAccessibilityDelegate(this, resumeDelegate) + } else { + resumeAnimation() + ViewCompat.setAccessibilityDelegate(this, pauseDelegate) + } + } } private fun LottieAnimationView.addOverlayDynamicColor(colorCallbacks: List<LottieCallback>) { @@ -236,11 +280,3 @@ private fun LottieAnimationView.addOverlayDynamicColor(colorCallbacks: List<Lott resumeAnimation() } } - -fun LottieAnimationView.toggleAnimation() { - if (isAnimating) { - pauseAnimation() - } else { - resumeAnimation() - } -} 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 4e17a2658ee7..27fc1878cc99 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 @@ -187,10 +187,10 @@ constructor( } } - private val _accessibilityHint = MutableSharedFlow<String>() + private val _accessibilityHint = MutableSharedFlow<String?>() /** Hint for talkback directional guidance */ - val accessibilityHint: Flow<String> = _accessibilityHint.asSharedFlow() + val accessibilityHint: Flow<String?> = _accessibilityHint.asSharedFlow() private val _isAuthenticating: MutableStateFlow<Boolean> = MutableStateFlow(false) @@ -923,6 +923,19 @@ constructor( return false } + /** Clears the message used for UDFPS directional guidance */ + suspend fun onClearUdfpsGuidanceHint(touchExplorationEnabled: Boolean) { + if ( + modalities.first().hasUdfps && + touchExplorationEnabled && + !isAuthenticated.first().isAuthenticated + ) { + // Add delay to make sure we read the guidance message before clearing it + delay(1000) + _accessibilityHint.emit(null) + } + } + /** * Switch to the credential view. * diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt index 6aeb35b3b158..99f299918969 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt @@ -37,6 +37,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider +import androidx.compose.material3.SliderColors import androidx.compose.material3.SliderDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -73,6 +74,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.modifiers.padding +import com.android.compose.theme.LocalAndroidColorScheme import com.android.compose.ui.graphics.drawInOverlay import com.android.systemui.Flags import com.android.systemui.biometrics.Utils.toBitmap @@ -139,7 +141,7 @@ fun BrightnessSlider( } else { null } - val colors = SliderDefaults.colors() + val colors = colors() // The value state is recreated every time gammaValue changes, so we recreate this derivedState // We have to use value as that's the value that changes when the user is dragging (gammaValue @@ -211,6 +213,7 @@ fun BrightnessSlider( interactionSource = interactionSource, enabled = enabled, thumbSize = DpSize(4.dp, 52.dp), + colors = colors, ) }, track = { sliderState -> @@ -293,6 +296,7 @@ fun BrightnessSlider( trackInsideCornerSize = 2.dp, drawStopIndicator = null, thumbTrackGapSize = ThumbTrackGapSize, + colors = colors, ) }, ) @@ -441,3 +445,13 @@ object BrightnessSliderMotionTestKeys { val ActiveIconAlpha = MotionTestValueKey<Float>("activeIconAlpha") val InactiveIconAlpha = MotionTestValueKey<Float>("inactiveIconAlpha") } + +@Composable +private fun colors(): SliderColors { + return SliderDefaults.colors() + .copy( + inactiveTrackColor = LocalAndroidColorScheme.current.surfaceEffect2, + activeTickColor = MaterialTheme.colorScheme.onPrimary, + inactiveTickColor = MaterialTheme.colorScheme.onSurface, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ActionIntentCreator.kt b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ActionIntentCreator.kt index 8cebe04d4e01..96dbcc5867c1 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ActionIntentCreator.kt +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ActionIntentCreator.kt @@ -21,20 +21,30 @@ import android.content.ClipDescription import android.content.ComponentName import android.content.Context import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.PackageManager.NameNotFoundException import android.net.Uri import android.text.TextUtils import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.res.R import java.util.function.Consumer import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @SysUISingleton class ActionIntentCreator @Inject -constructor(@Application private val applicationScope: CoroutineScope) : IntentCreator { +constructor( + private val context: Context, + private val packageManager: PackageManager, + @Application private val applicationScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) : IntentCreator { override fun getTextEditorIntent(context: Context?) = Intent(context, EditTextActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) @@ -72,11 +82,9 @@ constructor(@Application private val applicationScope: CoroutineScope) : IntentC } suspend fun getImageEditIntent(uri: Uri?, context: Context): Intent { - val editorPackage = context.getString(R.string.config_screenshotEditor) return Intent(Intent.ACTION_EDIT).apply { - if (!TextUtils.isEmpty(editorPackage)) { - setComponent(ComponentName.unflattenFromString(editorPackage)) - } + // Use the preferred editor if it's available, otherwise fall back to the default editor + component = preferredEditor() ?: defaultEditor() setDataAndType(uri, "image/*") addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) @@ -105,6 +113,39 @@ constructor(@Application private val applicationScope: CoroutineScope) : IntentC } } + private suspend fun preferredEditor(): ComponentName? = + runCatching { + val preferredEditor = context.getString(R.string.config_preferredScreenshotEditor) + val component = ComponentName.unflattenFromString(preferredEditor) ?: return null + + return if (isComponentAvailable(component)) component else null + } + .getOrNull() + + private suspend fun isComponentAvailable(component: ComponentName): Boolean = + withContext(backgroundDispatcher) { + try { + val info = + packageManager.getPackageInfo( + component.packageName, + PackageManager.GET_ACTIVITIES, + ) + info.activities?.firstOrNull { + it.componentName.className == component.className + } != null + } catch (e: NameNotFoundException) { + false + } + } + + private fun defaultEditor(): ComponentName? = + runCatching { + context.getString(R.string.config_screenshotEditor).let { + ComponentName.unflattenFromString(it) + } + } + .getOrNull() + companion object { private const val EXTRA_EDIT_SOURCE: String = "edit_source" private const val EDIT_SOURCE_CLIPBOARD: String = "clipboard" 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 a31c0bd35453..2875b7e2ae92 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt @@ -108,6 +108,9 @@ interface CommunalModule { const val LAUNCHER_PACKAGE = "launcher_package" const val SWIPE_TO_HUB = "swipe_to_hub" const val SHOW_UMO = "show_umo" + const val TOUCH_NOTIFICATION_RATE_LIMIT = "TOUCH_NOTIFICATION_RATE_LIMIT" + + const val TOUCH_NOTIFIFCATION_RATE_LIMIT_MS = 100 @Provides @Communal @@ -159,5 +162,11 @@ interface CommunalModule { fun provideShowUmo(@Main resources: Resources): Boolean { return resources.getBoolean(R.bool.config_showUmoOnHub) } + + @Provides + @Named(TOUCH_NOTIFICATION_RATE_LIMIT) + fun providesRateLimit(): Int { + return TOUCH_NOTIFIFCATION_RATE_LIMIT_MS + } } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt index bf4445ba18db..100e21d34c42 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt @@ -16,18 +16,16 @@ package com.android.systemui.communal.data.repository +import android.content.res.Configuration import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.animation.scene.ObservableTransitionState -import com.android.compose.animation.scene.OverlayKey import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.TransitionKey import com.android.systemui.communal.dagger.Communal import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.scene.shared.model.SceneDataSource -import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -49,31 +47,32 @@ interface CommunalSceneRepository { /** Exposes the transition state of the communal [SceneTransitionLayout]. */ val transitionState: StateFlow<ObservableTransitionState> + /** Current orientation of the communal container. */ + val communalContainerOrientation: StateFlow<Int> + /** Updates the requested scene. */ fun changeScene(toScene: SceneKey, transitionKey: TransitionKey? = null) /** Immediately snaps to the desired scene. */ fun snapToScene(toScene: SceneKey) - /** Shows the hub from a power button press. */ - suspend fun showHubFromPowerButton() - /** * Updates the transition state of the hub [SceneTransitionLayout]. * * Note that you must call is with `null` when the UI is done or risk a memory leak. */ fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) + + /** Set the current orientation of the communal container. */ + fun setCommunalContainerOrientation(orientation: Int) } @SysUISingleton class CommunalSceneRepositoryImpl @Inject constructor( - @Application private val applicationScope: CoroutineScope, @Background backgroundScope: CoroutineScope, @Communal private val sceneDataSource: SceneDataSource, - @Communal private val delegator: SceneDataSourceDelegator, ) : CommunalSceneRepository { override val currentScene: StateFlow<SceneKey> = sceneDataSource.currentScene @@ -89,32 +88,21 @@ constructor( initialValue = defaultTransitionState, ) + private val _communalContainerOrientation = + MutableStateFlow(Configuration.ORIENTATION_UNDEFINED) + override val communalContainerOrientation: StateFlow<Int> = + _communalContainerOrientation.asStateFlow() + override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) { - applicationScope.launch { - // SceneTransitionLayout state updates must be triggered on the thread the STL was - // created on. - sceneDataSource.changeScene(toScene, transitionKey) - } + sceneDataSource.changeScene(toScene, transitionKey) } override fun snapToScene(toScene: SceneKey) { - applicationScope.launch { - // SceneTransitionLayout state updates must be triggered on the thread the STL was - // created on. - sceneDataSource.snapToScene(toScene) - } + sceneDataSource.snapToScene(toScene) } - override suspend fun showHubFromPowerButton() { - // If keyguard is not showing yet, the hub view is not ready and the - // [SceneDataSourceDelegator] will still be using the default [NoOpSceneDataSource] - // and initial key, which is Blank. This means that when the hub container loads, it - // will default to not showing the hub. Attempting to set the scene in this state - // is simply ignored by the [NoOpSceneDataSource]. Instead, we temporarily override - // it with a new one that defaults to Communal. This delegate will be overwritten - // once the [CommunalContainer] loads. - // TODO(b/392969914): show the hub first instead of forcing the scene. - delegator.setDelegate(NoOpSceneDataSource(CommunalScenes.Communal)) + override fun setCommunalContainerOrientation(orientation: Int) { + _communalContainerOrientation.value = orientation } /** @@ -125,33 +113,4 @@ constructor( override fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) { _transitionState.value = transitionState } - - /** Noop implementation of a scene data source that always returns the initial [SceneKey]. */ - private class NoOpSceneDataSource(initialSceneKey: SceneKey) : SceneDataSource { - override val currentScene: StateFlow<SceneKey> = - MutableStateFlow(initialSceneKey).asStateFlow() - - override val currentOverlays: StateFlow<Set<OverlayKey>> = - MutableStateFlow(emptySet<OverlayKey>()).asStateFlow() - - override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) = Unit - - override fun snapToScene(toScene: SceneKey) = Unit - - override fun showOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) = Unit - - override fun hideOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) = Unit - - override fun replaceOverlay( - from: OverlayKey, - to: OverlayKey, - transitionKey: TransitionKey?, - ) = Unit - - override fun instantlyShowOverlay(overlay: OverlayKey) = Unit - - override fun instantlyHideOverlay(overlay: OverlayKey) = Unit - - override fun freezeAndAnimateToCurrentState() = Unit - } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt index 42a345b7deb4..8d599541b184 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt @@ -206,8 +206,11 @@ constructor( } .flowOn(bgDispatcher) - override fun getWhenToStartHubState(user: UserInfo): Flow<WhenToStartHub> = - secureSettings + override fun getWhenToStartHubState(user: UserInfo): Flow<WhenToStartHub> { + if (!getV2FlagEnabled()) { + return MutableStateFlow(WhenToStartHub.NEVER) + } + return secureSettings .observerFlow( userId = user.id, names = arrayOf(Settings.Secure.WHEN_TO_START_GLANCEABLE_HUB), @@ -225,11 +228,13 @@ constructor( Settings.Secure.GLANCEABLE_HUB_START_CHARGING -> WhenToStartHub.WHILE_CHARGING Settings.Secure.GLANCEABLE_HUB_START_CHARGING_UPRIGHT -> WhenToStartHub.WHILE_CHARGING_AND_POSTURED + Settings.Secure.GLANCEABLE_HUB_START_DOCKED -> WhenToStartHub.WHILE_DOCKED else -> WhenToStartHub.NEVER } } .flowOn(bgDispatcher) + } override fun getAllowedByDevicePolicy(user: UserInfo): Flow<Boolean> = broadcastDispatcher diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 684c52ad45f3..272439e68f71 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -149,9 +149,18 @@ constructor( val isCommunalEnabled: StateFlow<Boolean> = communalSettingsInteractor.isCommunalEnabled /** Whether communal features are enabled and available. */ - val isCommunalAvailable: Flow<Boolean> = - allOf(communalSettingsInteractor.isCommunalEnabled, keyguardInteractor.isKeyguardShowing) - .distinctUntilChanged() + @Deprecated("Use isCommunalEnabled instead", replaceWith = ReplaceWith("isCommunalEnabled")) + val isCommunalAvailable: Flow<Boolean> by lazy { + val availableFlow = + if (communalSettingsInteractor.isV2FlagEnabled()) { + communalSettingsInteractor.isCommunalEnabled + } else { + allOf( + communalSettingsInteractor.isCommunalEnabled, + keyguardInteractor.isKeyguardShowing, + ) + } + availableFlow .onEach { available -> logger.i({ "Communal is ${if (bool1) "" else "un"}available" }) { bool1 = available @@ -167,6 +176,7 @@ constructor( started = SharingStarted.WhileSubscribed(), replay = 1, ) + } private val _isDisclaimerDismissed = MutableStateFlow(false) val isDisclaimerDismissed: Flow<Boolean> = _isDisclaimerDismissed.asStateFlow() @@ -467,6 +477,7 @@ constructor( size = CommunalContentSize.toSize(widget.spanY), ) } + is CommunalWidgetContentModel.Pending -> { WidgetContent.PendingWidget( appWidgetId = widget.appWidgetId, @@ -493,6 +504,7 @@ constructor( when (model) { is CommunalWidgetContentModel.Available -> model.providerInfo.profile.identifier + is CommunalWidgetContentModel.Pending -> model.user.identifier } uid != disallowedByDevicePolicyUser.id @@ -576,6 +588,7 @@ constructor( when (widget) { is CommunalWidgetContentModel.Available -> currentUserIds.contains(widget.providerInfo.profile?.identifier) + is CommunalWidgetContentModel.Pending -> true } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt index fed99d71fa3b..80222299177b 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal.domain.interactor +import android.content.res.Configuration import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey @@ -28,13 +29,16 @@ import com.android.systemui.communal.shared.model.CommunalScenes.toSceneContaine import com.android.systemui.communal.shared.model.EditModeState import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.util.kotlin.pairwiseBy import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -55,9 +59,11 @@ class CommunalSceneInteractor @Inject constructor( @Application private val applicationScope: CoroutineScope, + @Main private val mainImmediateDispatcher: CoroutineDispatcher, private val repository: CommunalSceneRepository, private val logger: CommunalSceneLogger, private val sceneInteractor: SceneInteractor, + private val keyguardStateController: KeyguardStateController, ) { private val _isLaunchingWidget = MutableStateFlow(false) @@ -68,6 +74,30 @@ constructor( _isLaunchingWidget.value = launching } + /** + * Whether screen will be rotated to portrait if transitioned out of hub to keyguard screens. + */ + var willRotateToPortrait: Flow<Boolean> = + repository.communalContainerOrientation + .map { + it == Configuration.ORIENTATION_LANDSCAPE && + !keyguardStateController.isKeyguardScreenRotationAllowed() + } + .distinctUntilChanged() + + /** Whether communal container is rotated to portrait. Emits an initial value of false. */ + val rotatedToPortrait: StateFlow<Boolean> = + repository.communalContainerOrientation + .pairwiseBy(initialValue = false) { old, new -> + old == Configuration.ORIENTATION_LANDSCAPE && + new == Configuration.ORIENTATION_PORTRAIT + } + .stateIn(applicationScope, SharingStarted.Eagerly, false) + + fun setCommunalContainerOrientation(orientation: Int) { + repository.setCommunalContainerOrientation(orientation) + } + fun interface OnSceneAboutToChangeListener { /** Notifies that the scene is about to change to [toScene]. */ fun onSceneAboutToChange(toScene: SceneKey, keyguardState: KeyguardState?) @@ -86,6 +116,12 @@ constructor( onSceneAboutToChangeListener.add(processor) } + /** Unregisters a previously registered listener. */ + fun unregisterSceneStateProcessor(processor: OnSceneAboutToChangeListener) { + SceneContainerFlag.assertInLegacyMode() + onSceneAboutToChangeListener.remove(processor) + } + /** * Asks for an asynchronous scene witch to [newScene], which will use the corresponding * installed transition or the one specified by [transitionKey], if provided. @@ -96,7 +132,7 @@ constructor( transitionKey: TransitionKey? = null, keyguardState: KeyguardState? = null, ) { - applicationScope.launch("$TAG#changeScene") { + applicationScope.launch("$TAG#changeScene", mainImmediateDispatcher) { if (SceneContainerFlag.isEnabled) { sceneInteractor.changeScene( toScene = newScene.toSceneContainerSceneKey(), @@ -148,29 +184,6 @@ constructor( } } - fun showHubFromPowerButton() { - val loggingReason = "showing hub from power button" - applicationScope.launch("$TAG#showHubFromPowerButton") { - if (SceneContainerFlag.isEnabled) { - sceneInteractor.changeScene( - toScene = CommunalScenes.Communal.toSceneContainerSceneKey(), - loggingReason = loggingReason, - ) - return@launch - } - - if (currentScene.value == CommunalScenes.Communal) return@launch - logger.logSceneChangeRequested( - from = currentScene.value, - to = CommunalScenes.Communal, - reason = loggingReason, - isInstant = true, - ) - notifyListeners(CommunalScenes.Communal, null) - repository.showHubFromPowerButton() - } - } - private fun notifyListeners(newScene: SceneKey, keyguardState: KeyguardState?) { onSceneAboutToChangeListener.forEach { it.onSceneAboutToChange(newScene, keyguardState) } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt index 477b87119563..89d738ef3bcc 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt @@ -25,6 +25,7 @@ import com.android.systemui.communal.data.repository.CommunalSceneTransitionRepo import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.domain.interactor.InternalKeyguardTransitionInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor @@ -37,6 +38,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.util.kotlin.pairwise import java.util.UUID import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.SharingStarted @@ -64,6 +66,7 @@ constructor( val internalTransitionInteractor: InternalKeyguardTransitionInteractor, private val settingsInteractor: CommunalSettingsInteractor, @Application private val applicationScope: CoroutineScope, + @Main private val mainImmediateDispatcher: CoroutineDispatcher, private val sceneInteractor: CommunalSceneInteractor, private val repository: CommunalSceneTransitionRepository, private val powerInteractor: PowerInteractor, @@ -143,7 +146,7 @@ constructor( /** Monitors [SceneTransitionLayout] state and updates KTF state accordingly. */ private fun listenForSceneTransitionProgress() { - applicationScope.launch { + applicationScope.launch("$TAG#listenForSceneTransitionProgress", mainImmediateDispatcher) { sceneInteractor.transitionState .pairwise(ObservableTransitionState.Idle(CommunalScenes.Blank)) .collect { (prevTransition, transition) -> @@ -256,7 +259,10 @@ constructor( private fun collectProgress(transition: ObservableTransitionState.Transition) { progressJob?.cancel() - progressJob = applicationScope.launch { transition.progress.collect { updateProgress(it) } } + progressJob = + applicationScope.launch("$TAG#collectProgress", mainImmediateDispatcher) { + transition.progress.collect { updateProgress(it) } + } } private suspend fun startTransitionFromGlanceableHub() { @@ -300,4 +306,8 @@ constructor( TransitionState.RUNNING, ) } + + private companion object { + const val TAG = "CommunalSceneTransitionInteractor" + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt index e487590d87d7..b9a420c45262 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt @@ -18,9 +18,6 @@ package com.android.systemui.communal.posturing.domain.interactor import android.annotation.SuppressLint import android.hardware.Sensor -import android.hardware.TriggerEvent -import android.hardware.TriggerEventListener -import android.service.dreams.Flags.allowDreamWhenPostured import com.android.systemui.communal.posturing.data.model.PositionState import com.android.systemui.communal.posturing.data.repository.PosturingRepository import com.android.systemui.communal.posturing.shared.model.PosturedState @@ -31,20 +28,19 @@ import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf +import com.android.systemui.util.kotlin.observeTriggerSensor import com.android.systemui.util.kotlin.slidingWindow import com.android.systemui.util.sensors.AsyncSensorManager import com.android.systemui.util.time.SystemClock -import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow -import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -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.emptyFlow import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map @@ -170,49 +166,18 @@ constructor( * NOTE: Due to smoothing, this signal may be delayed to ensure we have a stable reading before * being considered postured. */ - val postured: Flow<Boolean> by lazy { - if (allowDreamWhenPostured()) { - combine(posturedSmoothed, debugPostured) { postured, debugValue -> - debugValue.asBoolean() ?: postured.asBoolean() ?: false - } - } else { - MutableStateFlow(false) + val postured: Flow<Boolean> = + combine(posturedSmoothed, debugPostured) { postured, debugValue -> + debugValue.asBoolean() ?: postured.asBoolean() ?: false } - } /** * Helper for observing a trigger sensor, which automatically unregisters itself after it * executes once. */ - private fun observeTriggerSensor(type: Int): Flow<Unit> = conflatedCallbackFlow { - val sensor = asyncSensorManager.getDefaultSensor(type) - val isRegistered = AtomicBoolean(false) - - fun registerCallbackInternal(callback: TriggerEventListener) { - if (isRegistered.compareAndSet(false, true)) { - asyncSensorManager.requestTriggerSensor(callback, sensor) - } - } - - val callback = - object : TriggerEventListener() { - override fun onTrigger(event: TriggerEvent) { - trySend(Unit) - if (isRegistered.getAndSet(false)) { - registerCallbackInternal(this) - } - } - } - - if (sensor != null) { - registerCallbackInternal(callback) - } - - awaitClose { - if (isRegistered.getAndSet(false)) { - asyncSensorManager.cancelTriggerSensor(callback, sensor) - } - } + private fun observeTriggerSensor(type: Int): Flow<Unit> { + val sensor = asyncSensorManager.getDefaultSensor(type) ?: return emptyFlow() + return asyncSensorManager.observeTriggerSensor(sensor) } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt index a84c45732169..49dc59ac0004 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt @@ -33,4 +33,6 @@ object CommunalTransitionKeys { val FromEditMode = TransitionKey("FromEditMode") /** Swipes the glanceable hub in/out of view */ val Swipe = TransitionKey("Swipe") + /** Swipes out of glanceable hub in landscape orientation */ + val SwipeInLandscape = TransitionKey("SwipeInLandscape") } 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 5a4b0b0e2d24..a6309d1be03d 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 @@ -386,6 +386,11 @@ constructor( } } + val swipeFromHubInLandscape: Flow<Boolean> = communalSceneInteractor.willRotateToPortrait + + fun onOrientationChange(orientation: Int) = + communalSceneInteractor.setCommunalContainerOrientation(orientation) + companion object { const val POPUP_AUTO_HIDE_TIMEOUT_MS = 12000L } diff --git a/packages/SystemUI/src/com/android/systemui/communal/util/UserTouchActivityNotifier.kt b/packages/SystemUI/src/com/android/systemui/communal/util/UserTouchActivityNotifier.kt new file mode 100644 index 000000000000..fec98a311fbd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/util/UserTouchActivityNotifier.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.util + +import android.view.MotionEvent +import com.android.systemui.communal.dagger.CommunalModule.Companion.TOUCH_NOTIFICATION_RATE_LIMIT +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.power.domain.interactor.PowerInteractor +import javax.inject.Inject +import javax.inject.Named +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * {@link UserTouchActivityNotifier} helps rate limit the user activity notifications sent to {@link + * PowerManager} from a single touch source. + */ +class UserTouchActivityNotifier +@Inject +constructor( + @Background private val scope: CoroutineScope, + private val powerInteractor: PowerInteractor, + @Named(TOUCH_NOTIFICATION_RATE_LIMIT) private val rateLimitMs: Int, +) { + private var lastNotification: Long? = null + + fun notifyActivity(event: MotionEvent) { + val metered = + when (event.action) { + MotionEvent.ACTION_CANCEL -> false + MotionEvent.ACTION_UP -> false + MotionEvent.ACTION_DOWN -> false + else -> true + } + + if (metered && lastNotification?.let { event.eventTime - it < rateLimitMs } == true) { + return + } + + lastNotification = event.eventTime + + scope.launch { powerInteractor.onUserTouch() } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt index 440c3001a2f9..701aa5c8d2c5 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt @@ -92,7 +92,17 @@ constructor( !glanceableHubMultiUserHelper.glanceableHubHsumFlagEnabled || !glanceableHubMultiUserHelper.isHeadlessSystemUserMode() ) { - anyOf(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen) + val isAvailable = + if (communalSettingsInteractor.isV2FlagEnabled()) { + allOf( + communalInteractor.isCommunalEnabled, + keyguardInteractor.isKeyguardShowing, + ) + } else { + communalInteractor.isCommunalAvailable + } + + anyOf(isAvailable, communalInteractor.editModeOpen) // Only trigger updates on state changes, ignoring the initial false value. .pairwise(false) .filter { (previous, new) -> previous != new } @@ -153,6 +163,7 @@ constructor( is CommunalWidgetContentModel.Available -> widget.providerInfo.widgetCategory and AppWidgetProviderInfo.WIDGET_CATEGORY_NOT_KEYGUARD != 0 + else -> false } } @@ -171,6 +182,7 @@ constructor( when (widget) { is CommunalWidgetContentModel.Available -> widget.providerInfo.profile?.identifier + is CommunalWidgetContentModel.Pending -> widget.user.identifier } !currentUserIds.contains(uid) diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManagerServiceSupplier.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManagerServiceSupplier.kt index ed77e6f5d7ea..b86c23bc3e71 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManagerServiceSupplier.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManagerServiceSupplier.kt @@ -62,6 +62,10 @@ constructor( userTracker.removeCallback(this) } + override fun alertUnstableService(unstableService: String?) { + // Unused. Do nothing. + } + override fun onBeforeUserSwitching(newUser: Int) { userAboutToSwitch = true listener?.onServiceChanged() diff --git a/packages/SystemUI/src/com/android/systemui/cursorposition/data/model/CursorPosition.kt b/packages/SystemUI/src/com/android/systemui/cursorposition/data/model/CursorPosition.kt new file mode 100644 index 000000000000..65174cc41028 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cursorposition/data/model/CursorPosition.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.cursorposition.data.model + +/** + * Represents the position of cursor hotspot on the screen. Hotspot is the specific pixel that + * signifies the location of the pointer's interaction with the user interface. By default, hotspot + * of a cursor is the tip of arrow. + * + * @property x The x-coordinate of the cursor hotspot, relative to the top-left corner of the + * screen. + * @property y The y-coordinate of the cursor hotspot, relative to the top-left corner of the + * screen. + * @property displayId The display on which the cursor is located. + */ +data class CursorPosition(val x: Float, val y: Float, val displayId: Int) diff --git a/packages/SystemUI/src/com/android/systemui/cursorposition/data/repository/MultiDisplayCursorPositionRepository.kt b/packages/SystemUI/src/com/android/systemui/cursorposition/data/repository/MultiDisplayCursorPositionRepository.kt new file mode 100644 index 000000000000..37f4a4c87114 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cursorposition/data/repository/MultiDisplayCursorPositionRepository.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.cursorposition.data.repository + +import com.android.app.displaylib.PerDisplayRepository +import com.android.systemui.cursorposition.data.model.CursorPosition +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.display.data.repository.DisplayRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +/** Repository for cursor position of multi displays. */ +interface MultiDisplayCursorPositionRepository { + val cursorPositions: Flow<CursorPosition?> +} + +/** + * Implementation of [MultiDisplayCursorPositionRepository] that aggregates cursor position updates + * from multiple displays. + * + * This class uses a [DisplayRepository] to track added displays and a [PerDisplayRepository] to + * manage [SingleDisplayCursorPositionRepository] instances for each display. [PerDisplayRepository] + * would destroy the instance if the display is removed. This class combines the cursor position + * from all displays into a single cursorPositions StateFlow. + */ +@SysUISingleton +class MultiDisplayCursorPositionRepositoryImpl +@Inject +constructor( + private val displayRepository: DisplayRepository, + @Background private val backgroundScope: CoroutineScope, + private val cursorRepositories: PerDisplayRepository<SingleDisplayCursorPositionRepository>, +) : MultiDisplayCursorPositionRepository { + + private val allDisplaysCursorPositions: Flow<CursorPosition> = + displayRepository.displayAdditionEvent + .mapNotNull { c -> c?.displayId } + .onStart { emitAll(displayRepository.displayIds.value.asFlow()) } + .flatMapMerge { + val repo = cursorRepositories[it] + repo?.cursorPositions ?: emptyFlow() + } + + override val cursorPositions: StateFlow<CursorPosition?> = + allDisplaysCursorPositions.stateIn(backgroundScope, SharingStarted.WhileSubscribed(), null) +} diff --git a/packages/SystemUI/src/com/android/systemui/cursorposition/data/repository/SingleDisplayCursorPositionRepository.kt b/packages/SystemUI/src/com/android/systemui/cursorposition/data/repository/SingleDisplayCursorPositionRepository.kt new file mode 100644 index 000000000000..f532fb22a19c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cursorposition/data/repository/SingleDisplayCursorPositionRepository.kt @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.cursorposition.data.repository + +import android.os.Handler +import android.os.Looper +import android.view.Choreographer +import android.view.InputDevice.SOURCE_MOUSE +import android.view.InputDevice.SOURCE_TOUCHPAD +import android.view.MotionEvent +import com.android.app.displaylib.PerDisplayInstanceProviderWithTeardown +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.cursorposition.data.model.CursorPosition +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.shared.system.InputChannelCompat +import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver +import com.android.systemui.shared.system.InputMonitorCompat +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import javax.inject.Inject +import kotlinx.coroutines.android.asCoroutineDispatcher +import kotlinx.coroutines.channels.ProducerScope +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn + +/** Repository for cursor position in single display. */ +interface SingleDisplayCursorPositionRepository { + /** Flow of [CursorPosition] for the display. */ + val cursorPositions: Flow<CursorPosition> + + /** Destroys the repository. */ + fun destroy() +} + +/** + * Implementation of [SingleDisplayCursorPositionRepository]. + * + * @param displayId the display id + * @param backgroundHandler the background handler + * @param listenerBuilder the builder for [InputChannelCompat.InputEventListener] + * @param inputMonitorBuilder the builder for [InputMonitorCompat] + */ +class SingleDisplayCursorPositionRepositoryImpl +@AssistedInject +constructor( + @Assisted displayId: Int, + @Background private val backgroundHandler: Handler, + @Assisted + private val listenerBuilder: InputEventListenerBuilder = defaultInputEventListenerBuilder, + @Assisted private val inputMonitorBuilder: InputMonitorBuilder = defaultInputMonitorBuilder, +) : SingleDisplayCursorPositionRepository { + + private var scope: ProducerScope<CursorPosition>? = null + + private fun createInputMonitorCallbackFlow(displayId: Int): Flow<CursorPosition> = + conflatedCallbackFlow { + val inputMonitor: InputMonitorCompat = inputMonitorBuilder.build(TAG, displayId) + val inputReceiver: InputEventReceiver = + inputMonitor.getInputReceiver( + Looper.myLooper(), + Choreographer.getInstance(), + listenerBuilder.build(this), + ) + scope = this + awaitClose { + inputMonitor.dispose() + inputReceiver.dispose() + } + } + // Use backgroundHandler as dispatcher because it has a looper (unlike + // "backgroundDispatcher" which does not have a looper) and input receiver could use + // its background looper and choreographer + .flowOn(backgroundHandler.asCoroutineDispatcher()) + + override val cursorPositions: Flow<CursorPosition> = createInputMonitorCallbackFlow(displayId) + + override fun destroy() { + scope?.close() + } + + @AssistedFactory + interface Factory { + /** + * Creates a new instance of [SingleDisplayCursorPositionRepositoryImpl] for a given + * [displayId]. + */ + fun create( + displayId: Int, + listenerBuilder: InputEventListenerBuilder = defaultInputEventListenerBuilder, + inputMonitorBuilder: InputMonitorBuilder = defaultInputMonitorBuilder, + ): SingleDisplayCursorPositionRepositoryImpl + } + + companion object { + private const val TAG = "CursorPositionPerDisplayRepositoryImpl" + + private val defaultInputMonitorBuilder = InputMonitorBuilder { name, displayId -> + InputMonitorCompat(name, displayId) + } + + val defaultInputEventListenerBuilder = InputEventListenerBuilder { channel -> + InputChannelCompat.InputEventListener { event -> + if ( + event is MotionEvent && + (event.source == SOURCE_MOUSE || event.source == SOURCE_TOUCHPAD) + ) { + val cursorEvent = CursorPosition(event.x, event.y, event.displayId) + channel.trySendWithFailureLogging(cursorEvent, TAG) + } + } + } + } +} + +fun interface InputEventListenerBuilder { + fun build(channel: SendChannel<CursorPosition>): InputChannelCompat.InputEventListener +} + +fun interface InputMonitorBuilder { + fun build(name: String, displayId: Int): InputMonitorCompat +} + +@SysUISingleton +class SingleDisplayCursorPositionRepositoryFactory +@Inject +constructor(private val factory: SingleDisplayCursorPositionRepositoryImpl.Factory) : + PerDisplayInstanceProviderWithTeardown<SingleDisplayCursorPositionRepository> { + override fun createInstance(displayId: Int): SingleDisplayCursorPositionRepository { + return factory.create(displayId) + } + + override fun destroyInstance(instance: SingleDisplayCursorPositionRepository) { + instance.destroy() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractor.kt index 54f3d7963e61..a871fb6b3d82 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractor.kt @@ -16,10 +16,12 @@ package com.android.systemui.deviceentry.domain.interactor import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge @@ -31,13 +33,19 @@ class AuthRippleInteractor constructor( deviceEntrySourceInteractor: DeviceEntrySourceInteractor, deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor, + keyguardInteractor: KeyguardInteractor, ) { + private val successfulEntryFromDeviceEntryIcon: Flow<Unit> = + deviceEntrySourceInteractor.attemptEnterDeviceFromDeviceEntryIcon + .map { keyguardInteractor.isKeyguardDismissible.value } + .filter { it } // only emit events if the keyguard is dismissible + // map to Unit + .map {} + private val showUnlockRippleFromDeviceEntryIcon: Flow<BiometricUnlockSource> = deviceEntryUdfpsInteractor.isUdfpsSupported.flatMapLatest { isUdfpsSupported -> if (isUdfpsSupported) { - deviceEntrySourceInteractor.deviceEntryFromDeviceEntryIcon.map { - BiometricUnlockSource.FINGERPRINT_SENSOR - } + successfulEntryFromDeviceEntryIcon.map { BiometricUnlockSource.FINGERPRINT_SENSOR } } else { emptyFlow() } @@ -46,8 +54,5 @@ constructor( private val showUnlockRippleFromBiometricUnlock: Flow<BiometricUnlockSource> = deviceEntrySourceInteractor.deviceEntryFromBiometricSource val showUnlockRipple: Flow<BiometricUnlockSource> = - merge( - showUnlockRippleFromDeviceEntryIcon, - showUnlockRippleFromBiometricUnlock, - ) + merge(showUnlockRippleFromDeviceEntryIcon, showUnlockRippleFromBiometricUnlock) } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt index 09936839c590..452cc435a36d 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt @@ -16,12 +16,14 @@ package com.android.systemui.deviceentry.domain.interactor import com.android.keyguard.logging.BiometricUnlockLogger +import com.android.systemui.Flags import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.util.kotlin.FlowDumperImpl @@ -54,6 +56,7 @@ constructor( keyEventInteractor: KeyEventInteractor, private val logger: BiometricUnlockLogger, powerInteractor: PowerInteractor, + keyguardInteractor: KeyguardInteractor, private val systemClock: SystemClock, dumpManager: DumpManager, ) : FlowDumperImpl(dumpManager) { @@ -80,12 +83,7 @@ constructor( emit(recentPowerButtonPressThresholdMs * -1L - 1L) } - /** - * Indicates when success haptics should play when the device is entered. This always occurs on - * successful fingerprint authentications. It also occurs on successful face authentication but - * only if the lockscreen is bypassed. - */ - val playSuccessHapticOnDeviceEntry: Flow<Unit> = + private val playSuccessHapticOnDeviceEntryFromBiometricSource: Flow<Unit> = deviceEntrySourceInteractor.deviceEntryFromBiometricSource .sample( combine( @@ -108,7 +106,31 @@ constructor( } // map to Unit .map {} - .dumpWhileCollecting("playSuccessHaptic") + + private val playSuccessHapticOnDeviceEntryFromDeviceEntryIcon: Flow<Unit> = + deviceEntrySourceInteractor.attemptEnterDeviceFromDeviceEntryIcon + .map { keyguardInteractor.isKeyguardDismissible.value } + .filter { it } // only play if the keyguard is dismissible + // map to Unit + .map {} + + /** + * Indicates when success haptics should play when the device is entered. When entering via a + * biometric sources, this always occurs on successful fingerprint authentications. It also + * occurs on successful face authentication but only if the lockscreen is bypassed. + */ + val playSuccessHapticOnDeviceEntry: Flow<Unit> = + if (Flags.msdlFeedback()) { + merge( + playSuccessHapticOnDeviceEntryFromBiometricSource, + playSuccessHapticOnDeviceEntryFromDeviceEntryIcon, + ) + .dumpWhileCollecting("playSuccessHaptic") + } else { + playSuccessHapticOnDeviceEntryFromBiometricSource.dumpWhileCollecting( + "playSuccessHaptic" + ) + } private val playErrorHapticForBiometricFailure: Flow<Unit> = merge( diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractor.kt index 5de2ed231275..19a15fc55976 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractor.kt @@ -254,15 +254,12 @@ constructor( } .dumpWhileCollecting("deviceEntryFromBiometricSource") - private val attemptEnterDeviceFromDeviceEntryIcon: MutableSharedFlow<Unit> = MutableSharedFlow() - val deviceEntryFromDeviceEntryIcon: Flow<Unit> = - attemptEnterDeviceFromDeviceEntryIcon - .sample(keyguardInteractor.isKeyguardDismissible) - .filter { it } // only send events if the keyguard is dismissible - .map {} // map to Unit + private val _attemptEnterDeviceFromDeviceEntryIcon: MutableSharedFlow<Unit> = + MutableSharedFlow() + val attemptEnterDeviceFromDeviceEntryIcon = _attemptEnterDeviceFromDeviceEntryIcon suspend fun attemptEnterDeviceFromDeviceEntryIcon() { - attemptEnterDeviceFromDeviceEntryIcon.emit(Unit) + _attemptEnterDeviceFromDeviceEntryIcon.emit(Unit) } private fun biometricModeIntToObject(@WakeAndUnlockMode value: Int): BiometricUnlockMode { diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt index 0d0105404726..1e50205500f9 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt @@ -20,6 +20,7 @@ import android.app.trust.TrustManager import android.content.Context import android.hardware.biometrics.BiometricFaceConstants import android.hardware.biometrics.BiometricSourceType +import android.service.dreams.Flags.dreamsV2 import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.biometrics.data.repository.FacePropertyRepository import com.android.systemui.biometrics.shared.model.LockoutMode @@ -40,6 +41,7 @@ import com.android.systemui.keyguard.shared.model.DevicePosture import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING +import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING 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 @@ -136,11 +138,18 @@ constructor( } .launchIn(applicationScope) - merge( - keyguardTransitionInteractor.transition(Edge.create(AOD, LOCKSCREEN)), - keyguardTransitionInteractor.transition(Edge.create(OFF, LOCKSCREEN)), - keyguardTransitionInteractor.transition(Edge.create(DOZING, LOCKSCREEN)), - ) + val transitionFlows = buildList { + add(keyguardTransitionInteractor.transition(Edge.create(AOD, LOCKSCREEN))) + add(keyguardTransitionInteractor.transition(Edge.create(OFF, LOCKSCREEN))) + add(keyguardTransitionInteractor.transition(Edge.create(DOZING, LOCKSCREEN))) + + if (dreamsV2()) { + add(keyguardTransitionInteractor.transition(Edge.create(DREAMING, LOCKSCREEN))) + } + } + + transitionFlows + .merge() .filter { it.transitionState == TransitionState.STARTED } .sample(powerInteractor.detailedWakefulness) .filter { wakefulnessModel -> diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/UdfpsAccessibilityOverlayBinder.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/UdfpsAccessibilityOverlayBinder.kt index e2172d0773d3..3abc260fdcbd 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/UdfpsAccessibilityOverlayBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/UdfpsAccessibilityOverlayBinder.kt @@ -18,27 +18,58 @@ package com.android.systemui.deviceentry.ui.binder import android.annotation.SuppressLint +import android.util.Log +import android.view.MotionEvent +import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO +import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES import androidx.core.view.isInvisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay import com.android.systemui.deviceentry.ui.viewmodel.UdfpsAccessibilityOverlayViewModel import com.android.systemui.lifecycle.repeatWhenAttached object UdfpsAccessibilityOverlayBinder { + private const val TAG = "UdfpsAccessibilityOverlayBinder" /** Forwards hover events to the view model to make guided announcements for accessibility. */ @SuppressLint("ClickableViewAccessibility") @JvmStatic - fun bind( - view: UdfpsAccessibilityOverlay, - viewModel: UdfpsAccessibilityOverlayViewModel, - ) { - view.setOnHoverListener { v, event -> viewModel.onHoverEvent(v, event) } + fun bind(view: UdfpsAccessibilityOverlay, viewModel: UdfpsAccessibilityOverlayViewModel) { view.repeatWhenAttached { // Repeat on CREATED because we update the visibility of the view repeatOnLifecycle(Lifecycle.State.CREATED) { - viewModel.visible.collect { visible -> view.isInvisible = !visible } + view.setOnHoverListener { v, event -> + if (event.action == MotionEvent.ACTION_HOVER_ENTER) { + launch { viewModel.onHoverEvent(v, event) } + } + false + } + + launch { viewModel.visible.collect { visible -> view.isInvisible = !visible } } + + launch { + viewModel.contentDescription.collect { contentDescription -> + if (contentDescription != null) { + view.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES + view.contentDescription = contentDescription + } + } + } + + launch { + viewModel.clearAccessibilityOverlayMessageReason.collect { reason -> + Log.d( + TAG, + "clearing content description of UDFPS accessibility overlay " + + "for reason: $reason", + ) + view.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO + view.contentDescription = null + viewModel.setContentDescription(null) + } + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt index 9c3b9b273ab5..0a2d10d10a40 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt @@ -23,5 +23,7 @@ import android.view.View class UdfpsAccessibilityOverlay(context: Context?) : View(context) { init { accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_ASSERTIVE + importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_AUTO + isClickable = false } } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/AlternateBouncerUdfpsAccessibilityOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/AlternateBouncerUdfpsAccessibilityOverlayViewModel.kt index 5c7cd5f55942..22ed6da2e5bf 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/AlternateBouncerUdfpsAccessibilityOverlayViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/AlternateBouncerUdfpsAccessibilityOverlayViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.deviceentry.ui.viewmodel import com.android.systemui.accessibility.domain.interactor.AccessibilityInteractor +import com.android.systemui.biometrics.UdfpsUtils import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -26,13 +27,23 @@ import kotlinx.coroutines.flow.flowOf class AlternateBouncerUdfpsAccessibilityOverlayViewModel @Inject constructor( - udfpsOverlayInteractor: UdfpsOverlayInteractor, + private val udfpsOverlayInteractor: UdfpsOverlayInteractor, accessibilityInteractor: AccessibilityInteractor, + udfpsUtils: UdfpsUtils, ) : UdfpsAccessibilityOverlayViewModel( udfpsOverlayInteractor, accessibilityInteractor, + udfpsUtils, ) { /** Overlay is always visible if touch exploration is enabled on the alternate bouncer. */ override fun isVisibleWhenTouchExplorationEnabled(): Flow<Boolean> = flowOf(true) + + /** + * Clears the content description to prevent the view from storing stale UDFPS directional + * guidance messages for accessibility. + */ + suspend fun clearUdfpsAccessibilityOverlayMessage(reason: String) { + udfpsOverlayInteractor.clearUdfpsAccessibilityOverlayMessage(reason) + } } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/DeviceEntryUdfpsAccessibilityOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/DeviceEntryUdfpsAccessibilityOverlayViewModel.kt index b84d65a2b430..5c86514775de 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/DeviceEntryUdfpsAccessibilityOverlayViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/DeviceEntryUdfpsAccessibilityOverlayViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.deviceentry.ui.viewmodel import com.android.systemui.accessibility.domain.interactor.AccessibilityInteractor +import com.android.systemui.biometrics.UdfpsUtils import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.keyguard.ui.view.DeviceEntryIconView import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel @@ -33,10 +34,12 @@ constructor( accessibilityInteractor: AccessibilityInteractor, private val deviceEntryIconViewModel: DeviceEntryIconViewModel, private val deviceEntryFgIconViewModel: DeviceEntryForegroundViewModel, + udfpsUtils: UdfpsUtils, ) : UdfpsAccessibilityOverlayViewModel( udfpsOverlayInteractor, accessibilityInteractor, + udfpsUtils, ) { /** Overlay is only visible if the UDFPS icon is visible on the keyguard. */ override fun isVisibleWhenTouchExplorationEnabled(): Flow<Boolean> = diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt index 1849bf20abdb..a58f3681555c 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt @@ -24,7 +24,10 @@ import com.android.systemui.biometrics.UdfpsUtils import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf @@ -32,8 +35,17 @@ import kotlinx.coroutines.flow.flowOf abstract class UdfpsAccessibilityOverlayViewModel( udfpsOverlayInteractor: UdfpsOverlayInteractor, accessibilityInteractor: AccessibilityInteractor, + private val udfpsUtils: UdfpsUtils, ) { - private val udfpsUtils = UdfpsUtils() + /** Indicates the reason for clearing the UDFPS accessibility overlay content description */ + val clearAccessibilityOverlayMessageReason: SharedFlow<String?> = + udfpsOverlayInteractor.clearAccessibilityOverlayMessageReason + + private val _contentDescription: MutableStateFlow<CharSequence?> = MutableStateFlow(null) + + /** Content description of the UDFPS accessibility overlay */ + val contentDescription: Flow<CharSequence?> = _contentDescription.asStateFlow() + private val udfpsOverlayParams: StateFlow<UdfpsOverlayParams> = udfpsOverlayInteractor.udfpsOverlayParams @@ -46,6 +58,10 @@ abstract class UdfpsAccessibilityOverlayViewModel( } } + fun setContentDescription(contentDescription: CharSequence?) { + _contentDescription.value = contentDescription + } + abstract fun isVisibleWhenTouchExplorationEnabled(): Flow<Boolean> /** Give directional feedback to help the user authenticate with UDFPS. */ @@ -77,8 +93,9 @@ abstract class UdfpsAccessibilityOverlayViewModel( overlayParams, /* touchRotatedToPortrait */ false, ) + if (announceStr != null) { - v.contentDescription = announceStr + _contentDescription.value = announceStr } } // always let the motion events go through to underlying views diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java index 599c945db064..c78231f16437 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java @@ -21,6 +21,7 @@ import static android.service.dreams.Flags.dreamHandlesBeingObscured; import static com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress; import static com.android.keyguard.BouncerPanelExpansionCalculator.getDreamAlphaScaledExpansion; import static com.android.keyguard.BouncerPanelExpansionCalculator.getDreamYPositionScaledExpansion; +import static com.android.systemui.Flags.bouncerUiRevamp; import static com.android.systemui.complication.ComplicationLayoutParams.POSITION_BOTTOM; import static com.android.systemui.complication.ComplicationLayoutParams.POSITION_TOP; import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset; @@ -362,9 +363,11 @@ public class DreamOverlayContainerViewController extends }); } - mBlurUtils.applyBlur(mView.getViewRootImpl(), - (int) mBlurUtils.blurRadiusOfRatio( - 1 - aboutToShowBouncerProgress(bouncerHideAmount)), false); + if (!bouncerUiRevamp()) { + mBlurUtils.applyBlur(mView.getViewRootImpl(), + (int) mBlurUtils.blurRadiusOfRatio( + 1 - aboutToShowBouncerProgress(bouncerHideAmount)), false); + } } private static float getAlpha(int position, float expansion) { diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index fd716eea799a..501883e257ab 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -17,6 +17,7 @@ package com.android.systemui.dreams; import static android.service.dreams.Flags.dreamWakeRedirect; +import static android.service.dreams.Flags.dreamsV2; import static com.android.systemui.Flags.glanceableHubAllowKeyguardWhenDreaming; import static com.android.systemui.dreams.dagger.DreamModule.DREAM_OVERLAY_WINDOW_TITLE; @@ -29,6 +30,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.graphics.drawable.ColorDrawable; +import android.os.PowerManager; import android.util.Log; import android.view.View; import android.view.ViewGroup; @@ -69,6 +71,7 @@ import com.android.systemui.dreams.dagger.DreamOverlayComponent; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.navigationbar.gestural.domain.GestureInteractor; import com.android.systemui.navigationbar.gestural.domain.TaskMatcher; +import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.scene.domain.interactor.SceneInteractor; import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.scene.shared.model.Overlays; @@ -77,6 +80,8 @@ import com.android.systemui.shade.ShadeExpansionChangeEvent; import com.android.systemui.touch.TouchInsetManager; import com.android.systemui.util.concurrency.DelayableExecutor; +import kotlin.Unit; + import kotlinx.coroutines.Job; import java.util.ArrayList; @@ -105,6 +110,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ private final Context mContext; // The Executor ensures actions and ui updates happen on the same thread. private final DelayableExecutor mExecutor; + private final PowerInteractor mPowerInteractor; // A controller for the dream overlay container view (which contains both the status bar and the // content area). private DreamOverlayContainerViewController mDreamOverlayContainerViewController; @@ -230,6 +236,15 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ } }; + private final Consumer<Unit> mPickupConsumer = new Consumer<>() { + @Override + public void accept(Unit unit) { + mExecutor.execute(() -> + mPowerInteractor.wakeUpIfDreaming("pickupGesture", + PowerManager.WAKE_REASON_LIFT)); + } + }; + /** * {@link ResetHandler} protects resetting {@link DreamOverlayService} by making sure reset * requests are processed before subsequent actions proceed. Requests themselves are also @@ -398,6 +413,8 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ DreamOverlayCallbackController dreamOverlayCallbackController, KeyguardInteractor keyguardInteractor, GestureInteractor gestureInteractor, + WakeGestureMonitor wakeGestureMonitor, + PowerInteractor powerInteractor, @Named(DREAM_OVERLAY_WINDOW_TITLE) String windowTitle) { super(executor); mContext = context; @@ -424,6 +441,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mTouchInsetManager = touchInsetManager; mLifecycleOwner = lifecycleOwner; mLifecycleRegistry = lifecycleOwner.getRegistry(); + mPowerInteractor = powerInteractor; mExecutor.execute(() -> setLifecycleStateLocked(Lifecycle.State.CREATED)); @@ -438,6 +456,11 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mFlows.add(collectFlow(getLifecycle(), keyguardInteractor.primaryBouncerShowing, mBouncerShowingConsumer)); } + + if (dreamsV2()) { + mFlows.add(collectFlow(getLifecycle(), wakeGestureMonitor.getWakeUpDetected(), + mPickupConsumer)); + } } @NonNull diff --git a/packages/SystemUI/src/com/android/systemui/dreams/WakeGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/dreams/WakeGestureMonitor.kt new file mode 100644 index 000000000000..1ba170bf656a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/WakeGestureMonitor.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams + +import android.hardware.Sensor +import android.hardware.display.AmbientDisplayConfiguration +import android.provider.Settings +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import com.android.systemui.util.kotlin.emitOnStart +import com.android.systemui.util.kotlin.observeTriggerSensor +import com.android.systemui.util.sensors.AsyncSensorManager +import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +@SysUISingleton +class WakeGestureMonitor +@Inject +constructor( + private val ambientDisplayConfiguration: AmbientDisplayConfiguration, + private val asyncSensorManager: AsyncSensorManager, + @Background bgContext: CoroutineContext, + private val secureSettings: SecureSettings, + selectedUserInteractor: SelectedUserInteractor, +) { + + private val pickupSensor by lazy { + asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE) + } + + private val pickupGestureEnabled: Flow<Boolean> = + selectedUserInteractor.selectedUser.flatMapLatestConflated { userId -> + isPickupEnabledForUser(userId) + } + + private fun isPickupEnabledForUser(userId: Int): Flow<Boolean> = + secureSettings + .observerFlow(userId, Settings.Secure.DOZE_PICK_UP_GESTURE) + .emitOnStart() + .map { ambientDisplayConfiguration.pickupGestureEnabled(userId) } + + val wakeUpDetected: Flow<Unit> = + pickupGestureEnabled + .flatMapLatestConflated { enabled -> + if (enabled && pickupSensor != null) { + asyncSensorManager.observeTriggerSensor(pickupSensor!!) + } else { + emptyFlow() + } + } + .flowOn(bgContext) +} diff --git a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt index 0640351c8149..d9f9a3ea1032 100644 --- a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt @@ -121,16 +121,8 @@ constructor( InputManager.KeyGestureEventListener { event -> // Only store keyboard shortcut time for gestures providing keyboard // education - val shortcutType = - when (event.keyGestureType) { - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS, - KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS -> ALL_APPS - - else -> null - } - - if (shortcutType != null) { - trySendWithFailureLogging(shortcutType, TAG) + if (event.keyGestureType == KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) { + trySendWithFailureLogging(ALL_APPS, TAG) } } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 84bb23140ae7..9a37439e7486 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -200,9 +200,6 @@ object Flags { // TODO(b/266157412): Tracking Bug val MEDIA_RETAIN_SESSIONS = unreleasedFlag("media_retain_sessions") - // TODO(b/267007629): Tracking Bug - val MEDIA_RESUME_PROGRESS = releasedFlag("media_resume_progress") - // TODO(b/270437894): Tracking Bug val MEDIA_REMOTE_RESUME = unreleasedFlag("media_remote_resume") diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index f2a10cc43fd9..8e857b3313a7 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -58,7 +58,9 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; +import android.os.PowerManager; import android.os.RemoteException; +import android.os.SystemClock; import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; @@ -194,6 +196,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene static final String GLOBAL_ACTION_KEY_EMERGENCY = "emergency"; static final String GLOBAL_ACTION_KEY_SCREENSHOT = "screenshot"; static final String GLOBAL_ACTION_KEY_SYSTEM_UPDATE = "system_update"; + static final String GLOBAL_ACTION_KEY_STANDBY = "standby"; // See NotificationManagerService#scheduleDurationReachedLocked private static final long TOAST_FADE_TIME = 333; @@ -270,6 +273,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene private final UserLogoutInteractor mLogoutInteractor; private final GlobalActionsInteractor mInteractor; private final Lazy<DisplayWindowPropertiesRepository> mDisplayWindowPropertiesRepositoryLazy; + private final PowerManager mPowerManager; private final Handler mHandler; private final UserTracker.Callback mOnUserSwitched = new UserTracker.Callback() { @@ -341,7 +345,10 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene GA_CLOSE_POWER_VOLUP(811), @UiEvent(doc = "System Update button was pressed.") - GA_SYSTEM_UPDATE_PRESS(1716); + GA_SYSTEM_UPDATE_PRESS(1716), + + @UiEvent(doc = "The global actions standby button was pressed.") + GA_STANDBY_PRESS(2210); private final int mId; @@ -396,7 +403,8 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene SelectedUserInteractor selectedUserInteractor, UserLogoutInteractor logoutInteractor, GlobalActionsInteractor interactor, - Lazy<DisplayWindowPropertiesRepository> displayWindowPropertiesRepository) { + Lazy<DisplayWindowPropertiesRepository> displayWindowPropertiesRepository, + PowerManager powerManager) { mContext = context; mWindowManagerFuncs = windowManagerFuncs; mAudioManager = audioManager; @@ -434,6 +442,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mLogoutInteractor = logoutInteractor; mInteractor = interactor; mDisplayWindowPropertiesRepositoryLazy = displayWindowPropertiesRepository; + mPowerManager = powerManager; mHandler = new Handler(mMainHandler.getLooper()) { public void handleMessage(Message msg) { @@ -697,6 +706,8 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene } } else if (GLOBAL_ACTION_KEY_SYSTEM_UPDATE.equals(actionKey)) { addIfShouldShowAction(tempActions, new SystemUpdateAction()); + } else if (GLOBAL_ACTION_KEY_STANDBY.equals(actionKey)) { + addIfShouldShowAction(tempActions, new StandbyAction()); } else { Log.e(TAG, "Invalid global action key " + actionKey); } @@ -1245,6 +1256,36 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene } } + @VisibleForTesting + class StandbyAction extends SinglePressAction { + StandbyAction() { + super(R.drawable.ic_standby, R.string.global_action_standby); + } + + @Override + public void onPress() { + // Add a little delay before executing, to give the dialog a chance to go away before + // going to sleep. Otherwise, we see screen flicker randomly. + mHandler.postDelayed(() -> { + mUiEventLogger.log(GlobalActionsEvent.GA_STANDBY_PRESS); + mBackgroundExecutor.execute(() -> { + mPowerManager.goToSleep(SystemClock.uptimeMillis(), + PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON, 0); + }); + }, mDialogPressDelay); + } + + @Override + public boolean showDuringKeyguard() { + return true; + } + + @Override + public boolean showBeforeProvisioning() { + return true; + } + } + private Action getSettingsAction() { return new SinglePressAction(R.drawable.ic_settings, R.string.global_action_settings) { diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCustomizationModeRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCustomizationModeRepository.kt new file mode 100644 index 000000000000..2f16b9f6ed72 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCustomizationModeRepository.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.shortcut.data.repository + +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine + +class ShortcutHelperCustomizationModeRepository +@Inject +constructor(shortcutHelperStateRepository: ShortcutHelperStateRepository) { + private val _isCustomizationModeEnabled = MutableStateFlow(false) + val isCustomizationModeEnabled = + combine(_isCustomizationModeEnabled, shortcutHelperStateRepository.state) { + isCustomizationModeEnabled, + shortcutHelperState -> + isCustomizationModeEnabled && shortcutHelperState is ShortcutHelperState.Active + } + + fun toggleCustomizationMode(isCustomizing: Boolean) { + _isCustomizationModeEnabled.value = isCustomizing + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/UserVisibleAppsRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/UserVisibleAppsRepository.kt new file mode 100644 index 000000000000..5a4ee16e0e64 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/UserVisibleAppsRepository.kt @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.shortcut.data.repository + +import android.content.Context +import android.content.pm.LauncherActivityInfo +import android.content.pm.LauncherApps +import android.os.Handler +import android.os.UserHandle +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.settings.UserTracker +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import java.util.concurrent.Executor +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow + +@SysUISingleton +class UserVisibleAppsRepository +@Inject +constructor( + private val userTracker: UserTracker, + @Background private val bgExecutor: Executor, + @Background private val bgHandler: Handler, + private val launcherApps: LauncherApps, +) { + + val userVisibleApps: Flow<List<LauncherActivityInfo>> + get() = conflatedCallbackFlow { + val packageChangeCallback: LauncherApps.Callback = + object : LauncherApps.Callback() { + override fun onPackageAdded(packageName: String, userHandle: UserHandle) { + trySendWithFailureLogging( + element = retrieveLauncherApps(), + loggingTag = TAG, + elementDescription = ON_PACKAGE_ADDED, + ) + } + + override fun onPackageChanged(packageName: String, userHandle: UserHandle) { + trySendWithFailureLogging( + element = retrieveLauncherApps(), + loggingTag = TAG, + elementDescription = ON_PACKAGE_CHANGED, + ) + } + + override fun onPackageRemoved(packageName: String, userHandle: UserHandle) { + trySendWithFailureLogging( + element = retrieveLauncherApps(), + loggingTag = TAG, + elementDescription = ON_PACKAGE_REMOVED, + ) + } + + override fun onPackagesAvailable( + packages: Array<out String>, + userHandle: UserHandle, + replacing: Boolean, + ) { + trySendWithFailureLogging( + element = retrieveLauncherApps(), + loggingTag = TAG, + elementDescription = ON_PACKAGES_AVAILABLE, + ) + } + + override fun onPackagesUnavailable( + packages: Array<out String>, + userHandle: UserHandle, + replacing: Boolean, + ) { + trySendWithFailureLogging( + element = retrieveLauncherApps(), + loggingTag = TAG, + elementDescription = ON_PACKAGES_UNAVAILABLE, + ) + } + } + + val userChangeCallback = + object : UserTracker.Callback { + override fun onUserChanged(newUser: Int, userContext: Context) { + trySendWithFailureLogging( + element = retrieveLauncherApps(), + loggingTag = TAG, + elementDescription = ON_USER_CHANGED, + ) + } + } + + userTracker.addCallback(userChangeCallback, bgExecutor) + launcherApps.registerCallback(packageChangeCallback, bgHandler) + + trySendWithFailureLogging( + element = retrieveLauncherApps(), + loggingTag = TAG, + elementDescription = INITIAL_VALUE, + ) + + awaitClose { + userTracker.removeCallback(userChangeCallback) + launcherApps.unregisterCallback(packageChangeCallback) + } + } + + private fun retrieveLauncherApps(): List<LauncherActivityInfo> { + return launcherApps.getActivityList(/* packageName= */ null, userTracker.userHandle) + } + + private companion object { + const val TAG = "UserVisibleAppsRepository" + const val ON_PACKAGE_ADDED = "onPackageAdded" + const val ON_PACKAGE_CHANGED = "onPackageChanged" + const val ON_PACKAGE_REMOVED = "onPackageRemoved" + const val ON_PACKAGES_AVAILABLE = "onPackagesAvailable" + const val ON_PACKAGES_UNAVAILABLE = "onPackagesUnavailable" + const val ON_USER_CHANGED = "onUserChanged" + const val INITIAL_VALUE = "InitialValue" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCustomizationModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCustomizationModeInteractor.kt new file mode 100644 index 000000000000..2cec991080b8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCustomizationModeInteractor.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.shortcut.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperCustomizationModeRepository +import javax.inject.Inject + +@SysUISingleton +class ShortcutHelperCustomizationModeInteractor +@Inject +constructor(private val customizationModeRepository: ShortcutHelperCustomizationModeRepository) { + val customizationMode = customizationModeRepository.isCustomizationModeEnabled + + fun toggleCustomizationMode(isCustomizing: Boolean) { + customizationModeRepository.toggleCustomizationMode(isCustomizing) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt index ea36a10fb01a..81efaac06d26 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt @@ -85,9 +85,12 @@ constructor( shortcutsUiState = shortcutsUiState, onKeyboardSettingsClicked = { onKeyboardSettingsClicked(dialog) }, onSearchQueryChanged = { shortcutHelperViewModel.onSearchQueryChanged(it) }, - onCustomizationRequested = { + onShortcutCustomizationRequested = { shortcutCustomizationDialogStarter.onShortcutCustomizationRequested(it) }, + onCustomizationModeToggled = { isCustomizing -> + shortcutHelperViewModel.toggleCustomizationMode(isCustomizing) + }, ) dialog.setOnDismissListener { shortcutHelperViewModel.onViewClosed() } dialog.setTitle(stringResource(R.string.shortcut_helper_title)) diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt index b0e554540371..72b984e226e2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt @@ -138,7 +138,8 @@ fun ShortcutHelper( modifier: Modifier = Modifier, shortcutsUiState: ShortcutsUiState, useSinglePane: @Composable () -> Boolean = { shouldUseSinglePane() }, - onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {}, + onShortcutCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {}, + onCustomizationModeToggled: (Boolean) -> Unit = {}, ) { when (shortcutsUiState) { is ShortcutsUiState.Active -> { @@ -146,9 +147,10 @@ fun ShortcutHelper( shortcutsUiState, useSinglePane, onSearchQueryChanged, + onCustomizationModeToggled, modifier, onKeyboardSettingsClicked, - onCustomizationRequested, + onShortcutCustomizationRequested, ) } @@ -163,9 +165,10 @@ private fun ActiveShortcutHelper( shortcutsUiState: ShortcutsUiState.Active, useSinglePane: @Composable () -> Boolean, onSearchQueryChanged: (String) -> Unit, + onCustomizationModeToggled: (Boolean) -> Unit, modifier: Modifier, onKeyboardSettingsClicked: () -> Unit, - onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {}, + onShortcutCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {}, ) { var selectedCategoryType by remember(shortcutsUiState.defaultSelectedCategory) { @@ -185,14 +188,16 @@ private fun ActiveShortcutHelper( ShortcutHelperTwoPane( shortcutsUiState.searchQuery, onSearchQueryChanged, - modifier, shortcutsUiState.shortcutCategories, selectedCategoryType, onCategorySelected = { selectedCategoryType = it }, onKeyboardSettingsClicked, shortcutsUiState.isShortcutCustomizerFlagEnabled, - onCustomizationRequested, shortcutsUiState.shouldShowResetButton, + shortcutsUiState.isCustomizationModeEnabled, + onCustomizationModeToggled, + modifier, + onShortcutCustomizationRequested, ) } } @@ -376,17 +381,18 @@ private fun ShortcutSubCategorySinglePane(searchQuery: String, subCategory: Shor private fun ShortcutHelperTwoPane( searchQuery: String, onSearchQueryChanged: (String) -> Unit, - modifier: Modifier = Modifier, categories: List<ShortcutCategoryUi>, selectedCategoryType: ShortcutCategoryType?, onCategorySelected: (ShortcutCategoryType?) -> Unit, onKeyboardSettingsClicked: () -> Unit, isShortcutCustomizerFlagEnabled: Boolean, - onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {}, shouldShowResetButton: Boolean, + isCustomizationModeEnabled: Boolean, + onCustomizationModeToggled: (isCustomizing: Boolean) -> Unit, + modifier: Modifier = Modifier, + onShortcutCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {}, ) { val selectedCategory = categories.fastFirstOrNull { it.type == selectedCategoryType } - var isCustomizing by remember { mutableStateOf(false) } Column(modifier = modifier.fillMaxSize().padding(horizontal = 24.dp)) { Row( @@ -397,14 +403,18 @@ private fun ShortcutHelperTwoPane( // Keep title centered whether customize button is visible or not. Spacer(modifier = Modifier.weight(1f)) Box(modifier = Modifier.width(412.dp), contentAlignment = Alignment.Center) { - TitleBar(isCustomizing) + TitleBar(isCustomizationModeEnabled) } if (isShortcutCustomizerFlagEnabled) { CustomizationButtonsContainer( modifier = Modifier.weight(1f), - isCustomizing = isCustomizing, - onToggleCustomizationMode = { isCustomizing = !isCustomizing }, - onReset = { onCustomizationRequested(ShortcutCustomizationRequestInfo.Reset) }, + isCustomizing = isCustomizationModeEnabled, + onToggleCustomizationMode = { + onCustomizationModeToggled(!isCustomizationModeEnabled) + }, + onReset = { + onShortcutCustomizationRequested(ShortcutCustomizationRequestInfo.Reset) + }, shouldShowResetButton = shouldShowResetButton, ) } else { @@ -426,8 +436,8 @@ private fun ShortcutHelperTwoPane( searchQuery, Modifier.fillMaxSize().padding(top = 8.dp).semantics { isTraversalGroup = true }, selectedCategory, - isCustomizing = isCustomizing, - onCustomizationRequested = onCustomizationRequested, + isCustomizing = isCustomizationModeEnabled, + onShortcutCustomizationRequested = onShortcutCustomizationRequested, ) } } @@ -496,7 +506,7 @@ private fun EndSidePanel( modifier: Modifier, category: ShortcutCategoryUi?, isCustomizing: Boolean, - onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {}, + onShortcutCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {}, ) { val listState = rememberLazyListState() LaunchedEffect(key1 = category) { if (category != null) listState.animateScrollToItem(0) } @@ -510,16 +520,20 @@ private fun EndSidePanel( searchQuery = searchQuery, subCategory = subcategory, isCustomizing = isCustomizing and category.type.includeInCustomization, - onCustomizationRequested = { requestInfo -> + onShortcutCustomizationRequested = { requestInfo -> when (requestInfo) { is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add -> - onCustomizationRequested(requestInfo.copy(categoryType = category.type)) + onShortcutCustomizationRequested( + requestInfo.copy(categoryType = category.type) + ) is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete -> - onCustomizationRequested(requestInfo.copy(categoryType = category.type)) + onShortcutCustomizationRequested( + requestInfo.copy(categoryType = category.type) + ) ShortcutCustomizationRequestInfo.Reset -> - onCustomizationRequested(requestInfo) + onShortcutCustomizationRequested(requestInfo) } }, ) @@ -551,7 +565,7 @@ private fun SubCategoryContainerDualPane( searchQuery: String, subCategory: ShortcutSubCategory, isCustomizing: Boolean, - onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit, + onShortcutCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit, ) { Surface( modifier = Modifier.fillMaxWidth(), @@ -573,20 +587,20 @@ private fun SubCategoryContainerDualPane( searchQuery = searchQuery, shortcut = shortcut, isCustomizing = isCustomizing && shortcut.isCustomizable, - onCustomizationRequested = { requestInfo -> + onShortcutCustomizationRequested = { requestInfo -> when (requestInfo) { is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add -> - onCustomizationRequested( + onShortcutCustomizationRequested( requestInfo.copy(subCategoryLabel = subCategory.label) ) is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete -> - onCustomizationRequested( + onShortcutCustomizationRequested( requestInfo.copy(subCategoryLabel = subCategory.label) ) ShortcutCustomizationRequestInfo.Reset -> - onCustomizationRequested(requestInfo) + onShortcutCustomizationRequested(requestInfo) } }, ) @@ -610,7 +624,7 @@ private fun Shortcut( searchQuery: String, shortcut: ShortcutModel, isCustomizing: Boolean = false, - onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {}, + onShortcutCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {}, ) { val interactionSource = remember { MutableInteractionSource() } val isFocused by interactionSource.collectIsFocusedAsState() @@ -650,7 +664,7 @@ private fun Shortcut( shortcut = shortcut, isCustomizing = isCustomizing, onAddShortcutRequested = { - onCustomizationRequested( + onShortcutCustomizationRequested( ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add( label = shortcut.label, shortcutCommand = shortcut.commands.first(), @@ -658,7 +672,7 @@ private fun Shortcut( ) }, onDeleteShortcutRequested = { - onCustomizationRequested( + onShortcutCustomizationRequested( ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete( label = shortcut.label, shortcutCommand = shortcut.commands.first(), diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/ShortcutsUiState.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/ShortcutsUiState.kt index 52ab157a0e70..b474ae6ad3f0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/ShortcutsUiState.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/ShortcutsUiState.kt @@ -26,6 +26,7 @@ sealed interface ShortcutsUiState { val defaultSelectedCategory: ShortcutCategoryType?, val isShortcutCustomizerFlagEnabled: Boolean = false, val shouldShowResetButton: Boolean = false, + val isCustomizationModeEnabled: Boolean = false, ) : ShortcutsUiState data object Inactive : ShortcutsUiState diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt index f11205fd7246..5937308898a3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt @@ -21,7 +21,6 @@ import android.content.Context import android.content.pm.PackageManager.NameNotFoundException import android.util.Log import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Accessibility import androidx.compose.material.icons.filled.AccessibilityNew import androidx.compose.material.icons.filled.Android import androidx.compose.material.icons.filled.Apps @@ -32,6 +31,7 @@ import com.android.compose.ui.graphics.painter.DrawablePainter import com.android.systemui.Flags.keyboardShortcutHelperShortcutCustomizer import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperCategoriesInteractor +import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperCustomizationModeInteractor import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperStateInteractor import com.android.systemui.keyboard.shortcut.shared.model.Shortcut import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory @@ -65,6 +65,7 @@ constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, private val stateInteractor: ShortcutHelperStateInteractor, categoriesInteractor: ShortcutHelperCategoriesInteractor, + private val customizationModeInteractor: ShortcutHelperCustomizationModeInteractor, ) { private val searchQuery = MutableStateFlow("") @@ -77,7 +78,11 @@ constructor( .flowOn(backgroundDispatcher) val shortcutsUiState = - combine(searchQuery, categoriesInteractor.shortcutCategories) { query, categories -> + combine( + searchQuery, + categoriesInteractor.shortcutCategories, + customizationModeInteractor.customizationMode, + ) { query, categories, isCustomizationModeEnabled -> if (categories.isEmpty()) { ShortcutsUiState.Inactive } else { @@ -94,6 +99,7 @@ constructor( isShortcutCustomizerFlagEnabled = keyboardShortcutHelperShortcutCustomizer(), shouldShowResetButton = shouldShowResetButton(shortcutCategoriesUi), + isCustomizationModeEnabled = isCustomizationModeEnabled, ) } } @@ -243,6 +249,7 @@ constructor( fun onViewClosed() { stateInteractor.onViewClosed() resetSearchQuery() + resetCustomizationMode() } fun onViewOpened() { @@ -253,7 +260,15 @@ constructor( searchQuery.value = query } + fun toggleCustomizationMode(isCustomizing: Boolean) { + customizationModeInteractor.toggleCustomizationMode(isCustomizing) + } + private fun resetSearchQuery() { searchQuery.value = "" } + + private fun resetCustomizationMode() { + customizationModeInteractor.toggleCustomizationMode(false) + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index 74cf7e4f7359..6caff6432cb2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -147,10 +147,8 @@ constructor( configuration, occludingAppDeviceEntryMessageViewModel, chipbarCoordinator, - screenOffAnimationController, shadeInteractor, - clockInteractor, - keyguardClockViewModel, + smartspaceViewModel, deviceEntryHapticsInteractor, vibratorHelper, falsingManager, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 099a7f067482..170966b45618 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -2433,11 +2433,7 @@ public class KeyguardViewMediator implements CoreStartable, private void doKeyguardLocked(Bundle options) { // If the power button behavior requests to open the glanceable hub. if (options != null && options.getBoolean(EXTRA_TRIGGER_HUB)) { - if (mCommunalSettingsInteractor.get().getAutoOpenEnabled().getValue()) { - // Set the hub to show immediately when the SysUI window shows, then continue to - // lock the device. - mCommunalSceneInteractor.get().showHubFromPowerButton(); - } else { + if (!mKeyguardInteractor.showGlanceableHub()) { // If the hub is not available, go to sleep instead of locking. This can happen // because the power button behavior does not check all possible reasons the hub // might be disabled. diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt index 51b953ef290c..979c7ceb239b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt @@ -25,6 +25,7 @@ import android.view.WindowManager import com.android.internal.widget.LockPatternUtils import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dagger.qualifiers.UiBackground import com.android.systemui.keyguard.domain.interactor.KeyguardDismissTransitionInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardShowWhileAwakeInteractor import com.android.systemui.keyguard.ui.binder.KeyguardSurfaceBehindParamsApplier @@ -44,6 +45,7 @@ class WindowManagerLockscreenVisibilityManager @Inject constructor( @Main private val executor: Executor, + @UiBackground private val uiBgExecutor: Executor, private val activityTaskManagerService: IActivityTaskManager, private val keyguardStateController: KeyguardStateController, private val keyguardSurfaceBehindAnimator: KeyguardSurfaceBehindParamsApplier, @@ -144,11 +146,15 @@ constructor( isKeyguardGoingAway = true return } - // Make the surface behind the keyguard visible by calling keyguardGoingAway. The - // lockscreen is still showing as well, allowing us to animate unlocked. - Log.d(TAG, "ActivityTaskManagerService#keyguardGoingAway()") - activityTaskManagerService.keyguardGoingAway(0) + isKeyguardGoingAway = true + Log.d(TAG, "Enqueuing ATMS#keyguardGoingAway() on uiBgExecutor") + uiBgExecutor.execute { + // Make the surface behind the keyguard visible by calling keyguardGoingAway. The + // lockscreen is still showing as well, allowing us to animate unlocked. + Log.d(TAG, "ATMS#keyguardGoingAway()") + activityTaskManagerService.keyguardGoingAway(0) + } } else if (isLockscreenShowing == true) { // Re-show the lockscreen if the surface was visible and we want to make it invisible, // and the lockscreen is currently showing (this is the usual case of the going away @@ -273,32 +279,44 @@ constructor( return } - if (this.isLockscreenShowing == lockscreenShowing && this.isAodVisible == aodVisible) { + if ( + this.isLockscreenShowing == lockscreenShowing && + this.isAodVisible == aodVisible && + !this.isKeyguardGoingAway + ) { Log.d( TAG, "#setWmLockscreenState: lockscreenShowing=$lockscreenShowing and " + - "isAodVisible=$aodVisible were both unchanged, not forwarding to ATMS.", + "isAodVisible=$aodVisible were both unchanged and we're not going away, not " + + "forwarding to ATMS.", ) return } + this.isLockscreenShowing = lockscreenShowing + this.isAodVisible = aodVisible Log.d( TAG, - "ATMS#setLockScreenShown(" + - "isLockscreenShowing=$lockscreenShowing, " + - "aodVisible=$aodVisible).", + "Enqueuing ATMS#setLockScreenShown($lockscreenShowing, $aodVisible) " + + "on uiBgExecutor", ) - if (enableNewKeyguardShellTransitions) { - startKeyguardTransition(lockscreenShowing, aodVisible) - } else { - try { - activityTaskManagerService.setLockScreenShown(lockscreenShowing, aodVisible) - } catch (e: RemoteException) { - Log.e(TAG, "Remote exception", e) + uiBgExecutor.execute { + Log.d( + TAG, + "ATMS#setLockScreenShown(" + + "isLockscreenShowing=$lockscreenShowing, " + + "aodVisible=$aodVisible).", + ) + if (enableNewKeyguardShellTransitions) { + startKeyguardTransition(lockscreenShowing, aodVisible) + } else { + try { + activityTaskManagerService.setLockScreenShown(lockscreenShowing, aodVisible) + } catch (e: RemoteException) { + Log.e(TAG, "Remote exception", e) + } } } - this.isLockscreenShowing = lockscreenShowing - this.isAodVisible = aodVisible } private fun startKeyguardTransition(keyguardShowing: Boolean, aodShowing: Boolean) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/PrimaryBouncerTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/PrimaryBouncerTransitionModule.kt index 7c4dbfeba50f..7110c37e88e7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/PrimaryBouncerTransitionModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/PrimaryBouncerTransitionModule.kt @@ -20,11 +20,13 @@ import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToPrimaryBouncerTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToPrimaryBouncerTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToPrimaryBouncerTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.DreamingToPrimaryBouncerTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToPrimaryBouncerTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.LockscreenToPrimaryBouncerTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.OccludedToPrimaryBouncerTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToAodTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToDozingTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToDreamingTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToLockscreenTransitionViewModel @@ -81,6 +83,10 @@ interface PrimaryBouncerTransitionImplModule { @Binds @IntoSet + fun fromDreaming(impl: DreamingToPrimaryBouncerTransitionViewModel): PrimaryBouncerTransition + + @Binds + @IntoSet fun toAod(impl: PrimaryBouncerToAodTransitionViewModel): PrimaryBouncerTransition @Binds @@ -103,5 +109,9 @@ interface PrimaryBouncerTransitionImplModule { @Binds @IntoSet + fun toDreaming(impl: PrimaryBouncerToDreamingTransitionViewModel): PrimaryBouncerTransition + + @Binds + @IntoSet fun toOccluded(impl: PrimaryBouncerToOccludedTransitionViewModel): PrimaryBouncerTransition } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt index f53421d539fe..4fca453a184c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt @@ -93,8 +93,8 @@ constructor( // transition. scope.launch("$TAG#listenForAodToAwake") { powerInteractor.detailedWakefulness - .filterRelevantKeyguardStateAnd { wakefulness -> wakefulness.isAwake() } .debounce(50L) + .filterRelevantKeyguardStateAnd { wakefulness -> wakefulness.isAwake() } .sample( transitionInteractor.startedKeyguardTransitionStep, wakeToGoneInteractor.canWakeDirectlyToGone, @@ -140,7 +140,8 @@ constructor( val shouldTransitionToCommunal = communalSettingsInteractor.isV2FlagEnabled() && autoOpenCommunal && - !detailedWakefulness.isAwakeFromMotionOrLift() + !detailedWakefulness.isAwakeFromMotionOrLift() && + !isKeyguardOccludedLegacy if (shouldTransitionToGone) { // TODO(b/360368320): Adapt for scene framework diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index 4aaa1fab4c65..d673f22386b7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -125,7 +125,9 @@ constructor( wakefulness: WakefulnessModel, ) = if (communalSettingsInteractor.isV2FlagEnabled()) { - shouldShowCommunal && !wakefulness.isAwakeFromMotionOrLift() + shouldShowCommunal && + !wakefulness.isAwakeFromMotionOrLift() && + !keyguardInteractor.isKeyguardOccluded.value } else { isCommunalAvailable && dreamManager.canStartDreaming(false) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt index 3b1b6fcc45f2..09bf478a9338 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt @@ -316,5 +316,6 @@ constructor( val TO_LOCKSCREEN_DURATION = 1167.milliseconds val TO_AOD_DURATION = 300.milliseconds val TO_GONE_DURATION = DEFAULT_DURATION + val TO_PRIMARY_BOUNCER_DURATION = DEFAULT_DURATION } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt index 3ad862b761fc..be0cf62b0526 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt @@ -251,6 +251,8 @@ constructor( * Set at 400ms for parity with [FromLockscreenTransitionInteractor] */ val DEFAULT_DURATION = 400.milliseconds + // To lockscreen duration must be at least 500ms to allow for potential screen rotation + // during the transition while the animation begins after 500ms. val TO_LOCKSCREEN_DURATION = 1.seconds val TO_BOUNCER_DURATION = 400.milliseconds val TO_OCCLUDED_DURATION = 450.milliseconds diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt index ca6a7907a8eb..cc5ec79a1060 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt @@ -20,6 +20,8 @@ import android.animation.ValueAnimator import com.android.app.animation.Interpolators import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor +import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main @@ -48,6 +50,7 @@ constructor( keyguardInteractor: KeyguardInteractor, powerInteractor: PowerInteractor, private val communalSceneInteractor: CommunalSceneInteractor, + private val communalSettingsInteractor: CommunalSettingsInteractor, keyguardOcclusionInteractor: KeyguardOcclusionInteractor, private val keyguardShowWhileAwakeInteractor: KeyguardShowWhileAwakeInteractor, ) : @@ -77,6 +80,26 @@ constructor( } /** + * Attempt to show the glanceable hub from the gone state (eg due to power button press). + * + * This will return whether the hub was successfully shown or not. + */ + fun showGlanceableHub(): Boolean { + val isRelevantKeyguardState = + transitionInteractor.startedKeyguardTransitionStep.value.to == KeyguardState.GONE + val showGlanceableHub = + isRelevantKeyguardState && + communalSettingsInteractor.isV2FlagEnabled() && + communalSettingsInteractor.autoOpenEnabled.value && + !keyguardInteractor.isKeyguardOccluded.value + if (showGlanceableHub) { + communalSceneInteractor.snapToScene(CommunalScenes.Communal, "showGlanceableHub()") + return true + } + return false + } + + /** * A special case supported on foldables, where folding the device may put the device on an * unlocked lockscreen, but if an occluding app is already showing (like a active phone call), * then go directly to OCCLUDED. @@ -100,28 +123,36 @@ constructor( scope.launch { keyguardShowWhileAwakeInteractor.showWhileAwakeEvents .filterRelevantKeyguardState() - .sample(communalSceneInteractor.isIdleOnCommunalNotEditMode, ::Pair) - .collect { (lockReason, idleOnCommunal) -> - val to = - if (idleOnCommunal) { - KeyguardState.GLANCEABLE_HUB - } else { - KeyguardState.LOCKSCREEN - } - startTransitionTo(to, ownerReason = "lockWhileAwake: $lockReason") + .sample(communalSettingsInteractor.autoOpenEnabled, ::Pair) + .collect { (lockReason, autoOpenHub) -> + if (autoOpenHub) { + communalSceneInteractor.changeScene( + CommunalScenes.Communal, + "lockWhileAwake: $lockReason", + ) + } else { + startTransitionTo( + KeyguardState.LOCKSCREEN, + ownerReason = "lockWhileAwake: $lockReason", + ) + } } } } else { - scope.launch("$TAG#listenForGoneToLockscreenOrHubOrOccluded") { + scope.launch("$TAG#listenForGoneToLockscreenOrHubOrOccluded", mainDispatcher) { keyguardInteractor.isKeyguardShowing .filterRelevantKeyguardStateAnd { isKeyguardShowing -> isKeyguardShowing } - .sample(communalSceneInteractor.isIdleOnCommunalNotEditMode, ::Pair) - .collect { (_, isIdleOnCommunal) -> + .sample(communalSettingsInteractor.autoOpenEnabled, ::Pair) + .collect { (_, autoOpenHub) -> val to = - if (isIdleOnCommunal) { - KeyguardState.GLANCEABLE_HUB - } else if (keyguardInteractor.isKeyguardOccluded.value) { + if (keyguardInteractor.isKeyguardOccluded.value) { KeyguardState.OCCLUDED + } else if (autoOpenHub) { + communalSceneInteractor.changeScene( + CommunalScenes.Communal, + "keyguard interactor says keyguard is showing", + ) + return@collect } else { KeyguardState.LOCKSCREEN } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt index 75d6631008ca..77fc804d1e82 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt @@ -294,5 +294,6 @@ constructor( val TO_OCCLUDED_DURATION = 550.milliseconds val TO_GLANCEABLE_HUB_DURATION = DEFAULT_DURATION val TO_GONE_SURFACE_BEHIND_VISIBLE_THRESHOLD = 0.1f + val TO_DREAMING_DURATION = DEFAULT_DURATION } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt index fc79c7ff118d..50ebbe497651 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt @@ -32,7 +32,7 @@ import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.Intra import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.ShadeDisplayAware -import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow @@ -45,7 +45,7 @@ class KeyguardBlueprintInteractor constructor( private val keyguardBlueprintRepository: KeyguardBlueprintRepository, @Application private val applicationScope: CoroutineScope, - shadeInteractor: ShadeInteractor, + shadeModeInteractor: ShadeModeInteractor, @ShadeDisplayAware private val configurationInteractor: ConfigurationInteractor, private val fingerprintPropertyInteractor: FingerprintPropertyInteractor, private val smartspaceSection: SmartspaceSection, @@ -61,7 +61,7 @@ constructor( /** Current BlueprintId */ val blueprintId = - shadeInteractor.isShadeLayoutWide.map { isShadeLayoutWide -> + shadeModeInteractor.isShadeLayoutWide.map { isShadeLayoutWide -> val useSplitShade = isShadeLayoutWide && !SceneContainerFlag.isEnabled when { useSplitShade -> SplitShadeKeyguardBlueprint.ID diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt index 02e04aa279d8..21b28a24213f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt @@ -35,7 +35,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi import com.android.systemui.statusbar.notification.promoted.domain.interactor.AODPromotedNotificationInteractor import com.android.systemui.util.kotlin.combine import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated @@ -90,14 +90,14 @@ constructor( var clock: ClockController? by keyguardClockRepository.clockEventController::clock private val isAodPromotedNotificationPresent: Flow<Boolean> = - if (PromotedNotificationUiAod.isEnabled) { + if (PromotedNotificationUi.isEnabled) { aodPromotedNotificationInteractor.isPresent } else { flowOf(false) } private val areAnyNotificationsPresent: Flow<Boolean> = - if (PromotedNotificationUiAod.isEnabled) { + if (PromotedNotificationUi.isEnabled) { combine( activeNotificationsInteractor.areAnyNotificationsPresent, isAodPromotedNotificationPresent, 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 2d5ff61a5015..e625fd72e159 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 @@ -507,6 +507,11 @@ constructor( } /** Temporary shim, until [KeyguardWmStateRefactor] is enabled */ + fun showGlanceableHub(): Boolean { + return fromGoneTransitionInteractor.get().showGlanceableHub() + } + + /** Temporary shim, until [KeyguardWmStateRefactor] is enabled */ fun dismissKeyguard() { when (keyguardTransitionInteractor.transitionState.value.to) { LOCKSCREEN -> fromLockscreenTransitionInteractor.get().dismissKeyguard() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt index ef1fe49372b2..6249b8006083 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt @@ -33,9 +33,11 @@ import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockMode +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.Companion.deviceIsAsleepInState import com.android.systemui.keyguard.shared.model.KeyguardState.Companion.deviceIsAwakeInState +import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.scene.shared.model.Scenes @@ -137,19 +139,20 @@ constructor( repository.biometricUnlockState, repository.canIgnoreAuthAndReturnToGone, transitionInteractor.currentKeyguardState, - ) { - keyguardEnabled, - shouldSuppressKeyguard, - biometricUnlockState, - canIgnoreAuthAndReturnToGone, - currentState -> + transitionInteractor.startedKeyguardTransitionStep, + ) { values -> + val keyguardEnabled = values[0] as Boolean + val shouldSuppressKeyguard = values[1] as Boolean + val biometricUnlockState = values[2] as BiometricUnlockModel + val canIgnoreAuthAndReturnToGone = values[3] as Boolean + val currentState = values[4] as KeyguardState + val startedStep = values[5] as TransitionStep (!keyguardEnabled || shouldSuppressKeyguard) || BiometricUnlockMode.isWakeAndUnlock(biometricUnlockState.mode) || canIgnoreAuthAndReturnToGone || (currentState == KeyguardState.DREAMING && keyguardInteractor.isKeyguardDismissible.value) || - (currentState == KeyguardState.GONE && - transitionInteractor.getStartedState() == KeyguardState.GONE) + (currentState == KeyguardState.GONE && startedStep.to == KeyguardState.GONE) } .distinctUntilChanged() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AccessibilityActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AccessibilityActionsViewBinder.kt index 824e0228adca..c7c54e95a63b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AccessibilityActionsViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AccessibilityActionsViewBinder.kt @@ -19,21 +19,19 @@ package com.android.systemui.keyguard.ui.binder import android.os.Bundle import android.view.View +import android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED import android.view.accessibility.AccessibilityNodeInfo import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.keyguard.ui.viewmodel.AccessibilityActionsViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R import kotlinx.coroutines.DisposableHandle -import com.android.app.tracing.coroutines.launchTraced as launch /** View binder for accessibility actions placeholder on keyguard. */ object AccessibilityActionsViewBinder { - fun bind( - view: View, - viewModel: AccessibilityActionsViewModel, - ): DisposableHandle { + fun bind(view: View, viewModel: AccessibilityActionsViewModel): DisposableHandle { val disposableHandle = view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { @@ -60,9 +58,10 @@ object AccessibilityActionsViewBinder { object : View.AccessibilityDelegate() { override fun onInitializeAccessibilityNodeInfo( host: View, - info: AccessibilityNodeInfo + info: AccessibilityNodeInfo, ) { super.onInitializeAccessibilityNodeInfo(host, info) + // Add custom actions if (canOpenGlanceableHub) { val action = @@ -80,7 +79,7 @@ object AccessibilityActionsViewBinder { override fun performAccessibilityAction( host: View, action: Int, - args: Bundle? + args: Bundle?, ): Boolean { return if ( action == R.id.accessibility_action_open_communal_hub @@ -89,6 +88,20 @@ object AccessibilityActionsViewBinder { true } else super.performAccessibilityAction(host, action, args) } + + override fun sendAccessibilityEvent( + host: View, + eventType: Int, + ) { + if (eventType == TYPE_VIEW_ACCESSIBILITY_FOCUSED) { + launch { + viewModel.clearUdfpsAccessibilityOverlayMessage( + "eventType $eventType on view $host" + ) + } + } + super.sendAccessibilityEvent(host, eventType) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt index b8b032719ef8..00d41d0a7aa7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt @@ -19,8 +19,10 @@ package com.android.systemui.keyguard.ui.binder import android.util.Log import android.view.LayoutInflater import android.view.View +import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO import android.view.ViewGroup import android.view.WindowManager +import android.view.accessibility.AccessibilityEvent.TYPE_VIEW_HOVER_EXIT import android.window.OnBackInvokedCallback import android.window.OnBackInvokedDispatcher import androidx.constraintlayout.widget.ConstraintLayout @@ -47,6 +49,7 @@ import com.android.systemui.scrim.ScrimView import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch /** * When necessary, adds the alternate bouncer window above most other windows (including the @@ -235,6 +238,25 @@ constructor( udfpsA11yOverlay = UdfpsAccessibilityOverlay(view.context).apply { id = udfpsA11yOverlayViewId + importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_AUTO + } + udfpsA11yOverlay.accessibilityDelegate = + object : View.AccessibilityDelegate() { + override fun sendAccessibilityEvent( + host: View, + eventType: Int, + ) { + if (eventType == TYPE_VIEW_HOVER_EXIT) { + applicationScope.launch { + udfpsA11yOverlayViewModel + .get() + .clearUdfpsAccessibilityOverlayMessage( + "$eventType on view $host" + ) + } + } + super.sendAccessibilityEvent(host, eventType) + } } view.addView(udfpsA11yOverlay) UdfpsAccessibilityOverlayBinder.bind( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt index 1ea47ec670af..0dd7821b4929 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt @@ -29,6 +29,7 @@ import androidx.core.view.isInvisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.systemui.Flags import com.android.systemui.common.ui.view.TouchHandlingView import com.android.systemui.keyguard.ui.view.DeviceEntryIconView import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryBackgroundViewModel @@ -39,6 +40,8 @@ import com.android.systemui.plugins.FalsingManager import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.util.kotlin.DisposableHandles +import com.google.android.msdl.data.model.MSDLToken +import com.google.android.msdl.domain.MSDLPlayer import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DisposableHandle @@ -64,6 +67,7 @@ object DeviceEntryIconViewBinder { bgViewModel: DeviceEntryBackgroundViewModel, falsingManager: FalsingManager, vibratorHelper: VibratorHelper, + msdlPlayer: MSDLPlayer, overrideColor: Color? = null, ): DisposableHandle { val disposables = DisposableHandles() @@ -88,7 +92,9 @@ object DeviceEntryIconViewBinder { ) return } - vibratorHelper.performHapticFeedback(view, HapticFeedbackConstants.CONFIRM) + if (!Flags.msdlFeedback()) { + vibratorHelper.performHapticFeedback(view, HapticFeedbackConstants.CONFIRM) + } applicationScope.launch { view.clearFocus() view.clearAccessibilityFocus() @@ -165,10 +171,23 @@ object DeviceEntryIconViewBinder { view.accessibilityHintType = hint if (hint != DeviceEntryIconView.AccessibilityHintType.NONE) { view.setOnClickListener { - vibratorHelper.performHapticFeedback( - view, - HapticFeedbackConstants.CONFIRM, - ) + if (Flags.msdlFeedback()) { + val token = + if ( + hint == + DeviceEntryIconView.AccessibilityHintType.ENTER + ) { + MSDLToken.UNLOCK + } else { + MSDLToken.LONG_PRESS + } + msdlPlayer.playToken(token) + } else { + vibratorHelper.performHapticFeedback( + view, + HapticFeedbackConstants.CONFIRM, + ) + } applicationScope.launch { view.clearFocus() view.clearAccessibilityFocus() @@ -180,6 +199,16 @@ object DeviceEntryIconViewBinder { } } } + + if (Flags.msdlFeedback()) { + launch("$TAG#viewModel.isPrimaryBouncerShowing") { + viewModel.deviceDidNotEnterFromDeviceEntryIcon.collect { + // If we did not enter from the icon, we did not play device entry + // haptics. Therefore, we play the token for long-press instead. + msdlPlayer.playToken(MSDLToken.LONG_PRESS) + } + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt index fc5914b02e05..f38a2430b8fc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt @@ -128,13 +128,7 @@ object KeyguardBlueprintViewBinder { cs: ConstraintSet, constraintLayout: ConstraintLayout, ) { - val ids = - listOf( - sharedR.id.date_smartspace_view, - sharedR.id.date_smartspace_view_large, - sharedR.id.weather_smartspace_view, - sharedR.id.weather_smartspace_view_large, - ) + val ids = listOf(sharedR.id.date_smartspace_view, sharedR.id.date_smartspace_view_large) for (i in ids) { constraintLayout.getViewById(i)?.visibility = cs.getVisibility(i) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index 60460bf68c12..d90292517b01 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -51,13 +51,12 @@ import com.android.systemui.common.ui.view.onLayoutChanged import com.android.systemui.common.ui.view.onTouchListener import com.android.systemui.customization.R as customR import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor -import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.ui.view.layout.sections.AodPromotedNotificationSection import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel -import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel import com.android.systemui.keyguard.ui.viewmodel.TransitionData import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor @@ -72,7 +71,6 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shared.R as sharedR import com.android.systemui.statusbar.CrossFadeHelper import com.android.systemui.statusbar.VibratorHelper -import com.android.systemui.statusbar.phone.ScreenOffAnimationController import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import com.android.systemui.temporarydisplay.ViewPriority import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator @@ -103,10 +101,8 @@ object KeyguardRootViewBinder { configuration: ConfigurationState, occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel?, chipbarCoordinator: ChipbarCoordinator?, - screenOffAnimationController: ScreenOffAnimationController, shadeInteractor: ShadeInteractor, - clockInteractor: KeyguardClockInteractor, - clockViewModel: KeyguardClockViewModel, + smartspaceViewModel: KeyguardSmartspaceViewModel, deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor?, vibratorHelper: VibratorHelper?, falsingManager: FalsingManager?, @@ -193,7 +189,6 @@ object KeyguardRootViewBinder { childViews[largeClockId]?.translationY = y if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { childViews[largeClockDateId]?.translationY = y - childViews[largeClockWeatherId]?.translationY = y } childViews[aodPromotedNotificationId]?.translationY = y childViews[aodNotificationIconContainerId]?.translationY = y @@ -327,7 +322,7 @@ object KeyguardRootViewBinder { if (isFullyAnyExpanded) { INVISIBLE } else { - View.VISIBLE + VISIBLE } } } @@ -395,7 +390,7 @@ object KeyguardRootViewBinder { OnLayoutChange( viewModel, blueprintViewModel, - clockViewModel, + smartspaceViewModel, childViews, burnInParams, Logger(blueprintLog, TAG), @@ -453,7 +448,7 @@ object KeyguardRootViewBinder { private class OnLayoutChange( private val viewModel: KeyguardRootViewModel, private val blueprintViewModel: KeyguardBlueprintViewModel, - private val clockViewModel: KeyguardClockViewModel, + private val smartspaceViewModel: KeyguardSmartspaceViewModel, private val childViews: Map<Int, View>, private val burnInParams: MutableStateFlow<BurnInParameters>, private val logger: Logger, @@ -471,12 +466,16 @@ object KeyguardRootViewBinder { oldRight: Int, oldBottom: Int, ) { + val prevSmartspaceVisibility = smartspaceViewModel.bcSmartspaceVisibility.value + val smartspaceVisibility = childViews[bcSmartspaceId]?.visibility ?: GONE + val smartspaceVisibilityChanged = prevSmartspaceVisibility != smartspaceVisibility + // After layout, ensure the notifications are positioned correctly childViews[nsslPlaceholderId]?.let { notificationListPlaceholder -> // Do not update a second time while a blueprint transition is running val transition = blueprintViewModel.currentTransition.value val shouldAnimate = transition != null && transition.config.type.animateNotifChanges - if (prevTransition == transition && shouldAnimate) { + if (prevTransition == transition && shouldAnimate && !smartspaceVisibilityChanged) { logger.w("Skipping onNotificationContainerBoundsChanged during transition") return } @@ -485,7 +484,7 @@ object KeyguardRootViewBinder { viewModel.onNotificationContainerBoundsChanged( notificationListPlaceholder.top.toFloat(), notificationListPlaceholder.bottom.toFloat(), - animate = shouldAnimate, + animate = (shouldAnimate || smartspaceVisibilityChanged), ) } @@ -585,6 +584,7 @@ object KeyguardRootViewBinder { private val largeClockId = customR.id.lockscreen_clock_view_large private val largeClockDateId = sharedR.id.date_smartspace_view_large private val largeClockWeatherId = sharedR.id.weather_smartspace_view_large + private val bcSmartspaceId = sharedR.id.bc_smartspace_view private val smallClockId = customR.id.lockscreen_clock_view private val indicationArea = R.id.keyguard_indication_area private val startButton = R.id.start_button diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt index 220846d08de7..50ef21b3f14d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt @@ -26,6 +26,7 @@ import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.systemui.Flags import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.common.ui.binder.TextViewBinder @@ -59,7 +60,9 @@ object KeyguardSettingsViewBinder { viewModel.isVisible.distinctUntilChanged().collect { isVisible -> view.animateVisibility(visible = isVisible) if (isVisible) { - vibratorHelper.vibrate(KeyguardBottomAreaVibrations.Activated) + if (!Flags.msdlFeedback()) { + vibratorHelper.vibrate(KeyguardBottomAreaVibrations.Activated) + } val textView = view.requireViewById(R.id.text) as TextView view.setOnTouchListener( KeyguardSettingsButtonOnTouchListener(viewModel = viewModel) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt index 5ef2d6fd3256..39fe588d8b6b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt @@ -91,14 +91,9 @@ object KeyguardSmartspaceViewBinder { R.dimen.smartspace_padding_vertical ) - val smallViewIds = - listOf(sharedR.id.date_smartspace_view, sharedR.id.weather_smartspace_view) + val smallViewId = sharedR.id.date_smartspace_view - val largeViewIds = - listOf( - sharedR.id.date_smartspace_view_large, - sharedR.id.weather_smartspace_view_large, - ) + val largeViewId = sharedR.id.date_smartspace_view_large launch("$TAG#smartspaceViewModel.burnInLayerVisibility") { combine( @@ -109,10 +104,8 @@ object KeyguardSmartspaceViewBinder { .collect { (visibility, isLargeClock) -> if (isLargeClock) { // hide small clock date/weather - for (viewId in smallViewIds) { - keyguardRootView.findViewById<View>(viewId)?.let { - it.visibility = View.GONE - } + keyguardRootView.findViewById<View>(smallViewId)?.let { + it.visibility = View.GONE } } } @@ -130,10 +123,9 @@ object KeyguardSmartspaceViewBinder { ::Pair, ) .collect { (isLargeClock, clockBounds) -> - for (id in (if (isLargeClock) smallViewIds else largeViewIds)) { - keyguardRootView.findViewById<View>(id)?.let { - it.visibility = View.GONE - } + val viewId = if (isLargeClock) smallViewId else largeViewId + keyguardRootView.findViewById<View>(viewId)?.let { + it.visibility = View.GONE } if (clockBounds == VRectF.ZERO) return@collect @@ -144,26 +136,26 @@ object KeyguardSmartspaceViewBinder { sharedR.id.date_smartspace_view_large ) ?.height ?: 0 - for (id in largeViewIds) { - keyguardRootView.findViewById<View>(id)?.let { view -> - val viewHeight = view.height - val offset = (largeDateHeight - viewHeight) / 2 - view.top = - (clockBounds.bottom + yBuffer + offset).toInt() - view.bottom = view.top + viewHeight - } + + keyguardRootView.findViewById<View>(largeViewId)?.let { view -> + val viewHeight = view.height + val offset = (largeDateHeight - viewHeight) / 2 + view.top = (clockBounds.bottom + yBuffer + offset).toInt() + view.bottom = view.top + viewHeight } - } else { - for (id in smallViewIds) { - keyguardRootView.findViewById<View>(id)?.let { view -> - val viewWidth = view.width - if (view.isLayoutRtl()) { - view.right = (clockBounds.left - xBuffer).toInt() - view.left = view.right - viewWidth - } else { - view.left = (clockBounds.right + xBuffer).toInt() - view.right = view.left + viewWidth - } + } else if ( + !KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock( + keyguardRootView.resources.configuration + ) + ) { + keyguardRootView.findViewById<View>(smallViewId)?.let { view -> + val viewWidth = view.width + if (view.isLayoutRtl()) { + view.right = (clockBounds.left - xBuffer).toInt() + view.left = view.right - viewWidth + } else { + view.left = (clockBounds.right + xBuffer).toInt() + view.right = view.left + viewWidth } } } @@ -218,11 +210,6 @@ object KeyguardSmartspaceViewBinder { val dateView = constraintLayout.requireViewById<View>(sharedR.id.date_smartspace_view) addView(dateView) - if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { - val weatherView = - constraintLayout.requireViewById<View>(sharedR.id.weather_smartspace_view) - addView(weatherView) - } } } } @@ -240,11 +227,6 @@ object KeyguardSmartspaceViewBinder { val dateView = constraintLayout.requireViewById<View>(sharedR.id.date_smartspace_view) removeView(dateView) - if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { - val weatherView = - constraintLayout.requireViewById<View>(sharedR.id.weather_smartspace_view) - removeView(weatherView) - } } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index d749e3c11378..e758768aa5e4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -73,7 +73,7 @@ import com.android.systemui.plugins.clocks.ThemeConfig import com.android.systemui.plugins.clocks.WeatherData import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag -import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import com.android.systemui.shared.clocks.ClockRegistry import com.android.systemui.shared.clocks.shared.model.ClockPreviewConstants import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots @@ -113,7 +113,7 @@ constructor( private val udfpsOverlayInteractor: UdfpsOverlayInteractor, private val indicationController: KeyguardIndicationController, @Assisted bundle: Bundle, - private val shadeInteractor: ShadeInteractor, + private val shadeModeInteractor: ShadeModeInteractor, private val secureSettings: SecureSettings, private val defaultShortcutsSection: DefaultShortcutsSection, private val keyguardQuickAffordanceViewBinder: KeyguardQuickAffordanceViewBinder, @@ -415,10 +415,7 @@ constructor( setUpClock(previewContext, rootView) if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { setUpSmartspace(previewContext, keyguardRootView) - KeyguardPreviewSmartspaceViewBinder.bind( - keyguardRootView, - smartspaceViewModel, - ) + KeyguardPreviewSmartspaceViewBinder.bind(keyguardRootView, smartspaceViewModel) } KeyguardPreviewClockViewBinder.bind( keyguardRootView, @@ -591,7 +588,7 @@ constructor( private fun getPreviewShadeLayoutWide(display: Display): Boolean { return if (display.displayId == 0) { - shadeInteractor.isShadeLayoutWide.value + shadeModeInteractor.isShadeLayoutWide.value } else { // For the unfolded preview in a folded screen; it's landscape by default // For the folded preview in an unfolded screen; it's portrait by default diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt index e268050234ab..bca0bedc7350 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt @@ -34,12 +34,12 @@ import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.res.R import com.android.systemui.shade.ShadeDisplayAware -import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi import com.android.systemui.statusbar.phone.NotificationIconContainer import com.android.systemui.statusbar.ui.SystemBarUtilsState import com.android.systemui.util.ui.value @@ -56,7 +56,7 @@ constructor( private val nicAodIconViewStore: AlwaysOnDisplayNotificationIconViewStore, private val systemBarUtilsState: SystemBarUtilsState, private val rootViewModel: KeyguardRootViewModel, - private val shadeInteractor: ShadeInteractor, + private val shadeModeInteractor: ShadeModeInteractor, ) : KeyguardSection() { private var nicBindingDisposable: DisposableHandle? = null @@ -99,10 +99,10 @@ constructor( context.resources.getDimensionPixelSize(customR.dimen.status_view_margin_horizontal) val height = context.resources.getDimensionPixelSize(R.dimen.notification_shelf_height) val isVisible = rootViewModel.isNotifIconContainerVisible.value - val isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value + val isShadeLayoutWide = shadeModeInteractor.isShadeLayoutWide.value constraintSet.apply { - if (PromotedNotificationUiAod.isEnabled) { + if (PromotedNotificationUi.isEnabled) { connect(nicId, TOP, AodPromotedNotificationSection.viewId, BOTTOM, bottomMargin) } else { connect(nicId, TOP, R.id.smart_space_barrier_bottom, BOTTOM, bottomMargin) @@ -111,7 +111,7 @@ constructor( setGoneMargin(nicId, BOTTOM, bottomMargin) setVisibility(nicId, if (isVisible.value) VISIBLE else GONE) - if (PromotedNotificationUiAod.isEnabled && isShadeLayoutWide) { + if (PromotedNotificationUi.isEnabled && isShadeLayoutWide) { // Don't create a start constraint, so the icons can hopefully right-align. } else { connect(nicId, START, PARENT_ID, START, horizontalMargin) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt index 2110c4027667..f75b53017500 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt @@ -28,10 +28,10 @@ import androidx.constraintlayout.widget.ConstraintSet.TOP import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.res.R import com.android.systemui.shade.ShadeDisplayAware -import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import com.android.systemui.statusbar.notification.promoted.AODPromotedNotification import com.android.systemui.statusbar.notification.promoted.PromotedNotificationLogger -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi import com.android.systemui.statusbar.notification.promoted.ui.viewmodel.AODPromotedNotificationViewModel import javax.inject.Inject @@ -40,7 +40,7 @@ class AodPromotedNotificationSection constructor( @ShadeDisplayAware private val context: Context, private val viewModelFactory: AODPromotedNotificationViewModel.Factory, - private val shadeInteractor: ShadeInteractor, + private val shadeModeInteractor: ShadeModeInteractor, private val logger: PromotedNotificationLogger, ) : KeyguardSection() { var view: ComposeView? = null @@ -50,7 +50,7 @@ constructor( } override fun addViews(constraintLayout: ConstraintLayout) { - if (!PromotedNotificationUiAod.isEnabled) { + if (!PromotedNotificationUi.isEnabled) { return } @@ -67,7 +67,7 @@ constructor( } override fun bindData(constraintLayout: ConstraintLayout) { - if (!PromotedNotificationUiAod.isEnabled) { + if (!PromotedNotificationUi.isEnabled) { return } @@ -79,7 +79,7 @@ constructor( } override fun applyConstraints(constraintSet: ConstraintSet) { - if (!PromotedNotificationUiAod.isEnabled) { + if (!PromotedNotificationUi.isEnabled) { return } @@ -90,7 +90,7 @@ constructor( context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start_icons) constraintSet.apply { - val isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value + val isShadeLayoutWide = shadeModeInteractor.isShadeLayoutWide.value if (isShadeLayoutWide) { // When in split shade, align with top of smart space: @@ -119,7 +119,7 @@ constructor( } override fun removeViews(constraintLayout: ConstraintLayout) { - if (!PromotedNotificationUiAod.isEnabled) { + if (!PromotedNotificationUi.isEnabled) { return } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt index 8a33c6471326..9c6f46570b1d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt @@ -121,18 +121,22 @@ constructor( setAlpha(getNonTargetClockFace(clock).views, 0F) if (!keyguardClockViewModel.isLargeClockVisible.value) { - if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { + if ( + KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock( + context.resources.configuration + ) + ) { connect( sharedR.id.bc_smartspace_view, TOP, - customR.id.lockscreen_clock_view, + sharedR.id.date_smartspace_view, BOTTOM, ) } else { connect( sharedR.id.bc_smartspace_view, TOP, - sharedR.id.date_smartspace_view, + customR.id.lockscreen_clock_view, BOTTOM, ) } @@ -187,6 +191,8 @@ constructor( val guideline = if (keyguardClockViewModel.clockShouldBeCentered.value) PARENT_ID else R.id.split_shade_guideline + val dateWeatherBelowSmallClock = + KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(context.resources.configuration) constraints.apply { connect(customR.id.lockscreen_clock_view_large, START, PARENT_ID, START) connect(customR.id.lockscreen_clock_view_large, END, guideline, END) @@ -254,11 +260,7 @@ constructor( 0 } - if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { - clockInteractor.setNotificationStackDefaultTop( - (smallClockBottom + marginBetweenSmartspaceAndNotification).toFloat() - ) - } else { + if (dateWeatherBelowSmallClock) { val dateWeatherSmartspaceHeight = getDimen(context, DATE_WEATHER_VIEW_HEIGHT).toFloat() clockInteractor.setNotificationStackDefaultTop( @@ -266,6 +268,10 @@ constructor( dateWeatherSmartspaceHeight + marginBetweenSmartspaceAndNotification ) + } else { + clockInteractor.setNotificationStackDefaultTop( + (smallClockBottom + marginBetweenSmartspaceAndNotification).toFloat() + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt index 9c8f04b419fb..754b3d72c13d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt @@ -47,6 +47,7 @@ import com.android.systemui.res.R import com.android.systemui.shade.NotificationPanelView import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.VibratorHelper +import com.google.android.msdl.domain.MSDLPlayer import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -69,6 +70,7 @@ constructor( private val deviceEntryBackgroundViewModel: Lazy<DeviceEntryBackgroundViewModel>, private val falsingManager: Lazy<FalsingManager>, private val vibratorHelper: Lazy<VibratorHelper>, + private val msdlPlayer: Lazy<MSDLPlayer>, @LongPressTouchLog private val logBuffer: LogBuffer, @KeyguardBlueprintLog blueprintLogBuffer: LogBuffer, ) : KeyguardSection() { @@ -101,6 +103,7 @@ constructor( deviceEntryBackgroundViewModel.get(), falsingManager.get(), vibratorHelper.get(), + msdlPlayer.get(), ) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt index d0b5f743c277..d9652b590678 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt @@ -20,6 +20,7 @@ import android.content.Context import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver.OnGlobalLayoutListener +import android.widget.LinearLayout import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet @@ -57,10 +58,8 @@ constructor( private val keyguardRootViewModel: KeyguardRootViewModel, ) : KeyguardSection() { private var smartspaceView: View? = null - private var weatherView: View? = null private var dateView: ViewGroup? = null - private var weatherViewLargeClock: View? = null - private var dateViewLargeClock: View? = null + private var dateViewLargeClock: ViewGroup? = null private var smartspaceVisibilityListener: OnGlobalLayoutListener? = null private var pastVisibility: Int = -1 @@ -77,34 +76,47 @@ constructor( override fun addViews(constraintLayout: ConstraintLayout) { if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return smartspaceView = smartspaceController.buildAndConnectView(constraintLayout) - weatherView = smartspaceController.buildAndConnectWeatherView(constraintLayout, false) dateView = smartspaceController.buildAndConnectDateView(constraintLayout, false) as? ViewGroup + var weatherViewLargeClock: View? = null + val weatherView: View? = + smartspaceController.buildAndConnectWeatherView(constraintLayout, false) if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { weatherViewLargeClock = smartspaceController.buildAndConnectWeatherView(constraintLayout, true) dateViewLargeClock = - smartspaceController.buildAndConnectDateView(constraintLayout, true) + smartspaceController.buildAndConnectDateView(constraintLayout, true) as? ViewGroup } pastVisibility = smartspaceView?.visibility ?: View.GONE constraintLayout.addView(smartspaceView) if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { dateView?.visibility = View.GONE - weatherView?.visibility = View.GONE dateViewLargeClock?.visibility = View.GONE - weatherViewLargeClock?.visibility = View.GONE - constraintLayout.addView(dateView) - constraintLayout.addView(weatherView) - constraintLayout.addView(weatherViewLargeClock) constraintLayout.addView(dateViewLargeClock) - } else { if (keyguardSmartspaceViewModel.isDateWeatherDecoupled) { - constraintLayout.addView(dateView) // Place weather right after the date, before the extras (alarm and dnd) - val index = if (dateView?.childCount == 0) 0 else 1 - dateView?.addView(weatherView, index) + val index = if (dateViewLargeClock?.childCount == 0) 0 else 1 + dateViewLargeClock?.addView(weatherViewLargeClock, index) + } + + if ( + KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock( + context.resources.configuration, + keyguardClockViewModel.hasCustomWeatherDataDisplay.value, + ) + ) { + (dateView as? LinearLayout)?.orientation = LinearLayout.HORIZONTAL + } else { + (dateView as? LinearLayout)?.orientation = LinearLayout.VERTICAL } } + + if (keyguardSmartspaceViewModel.isDateWeatherDecoupled) { + constraintLayout.addView(dateView) + // Place weather right after the date, before the extras (alarm and dnd) + val index = if (dateView?.childCount == 0) 0 else 1 + dateView?.addView(weatherView, index) + } keyguardUnlockAnimationController.lockscreenSmartspace = smartspaceView smartspaceVisibilityListener = OnGlobalLayoutListener { smartspaceView?.let { @@ -136,10 +148,15 @@ constructor( val dateWeatherPaddingStart = KeyguardSmartspaceViewModel.getDateWeatherStartMargin(context) val smartspaceHorizontalPadding = KeyguardSmartspaceViewModel.getSmartspaceHorizontalMargin(context) + val dateWeatherBelowSmallClock = + KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock( + context.resources.configuration, + keyguardClockViewModel.hasCustomWeatherDataDisplay.value, + ) constraintSet.apply { constrainHeight(sharedR.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT) constrainWidth(sharedR.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT) - if (!com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { + if (dateWeatherBelowSmallClock) { connect( sharedR.id.date_smartspace_view, ConstraintSet.START, @@ -167,7 +184,7 @@ constructor( smartspaceHorizontalPadding, ) if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) { - if (!com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { + if (dateWeatherBelowSmallClock) { clear(sharedR.id.date_smartspace_view, ConstraintSet.TOP) connect( sharedR.id.date_smartspace_view, @@ -179,12 +196,27 @@ constructor( } else { clear(sharedR.id.date_smartspace_view, ConstraintSet.BOTTOM) if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { - connect( - sharedR.id.bc_smartspace_view, - ConstraintSet.TOP, - customR.id.lockscreen_clock_view, - ConstraintSet.BOTTOM, - ) + if (dateWeatherBelowSmallClock) { + connect( + sharedR.id.date_smartspace_view, + ConstraintSet.TOP, + customR.id.lockscreen_clock_view, + ConstraintSet.BOTTOM, + ) + connect( + sharedR.id.bc_smartspace_view, + ConstraintSet.TOP, + sharedR.id.date_smartspace_view, + ConstraintSet.BOTTOM, + ) + } else { + connect( + sharedR.id.bc_smartspace_view, + ConstraintSet.TOP, + customR.id.lockscreen_clock_view, + ConstraintSet.BOTTOM, + ) + } } else { connect( sharedR.id.date_smartspace_view, @@ -203,7 +235,6 @@ constructor( if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { if (keyguardClockViewModel.isLargeClockVisible.value) { - setVisibility(sharedR.id.weather_smartspace_view, GONE) setVisibility(sharedR.id.date_smartspace_view, GONE) constrainHeight( sharedR.id.date_smartspace_view_large, @@ -238,118 +269,79 @@ constructor( connect( sharedR.id.date_smartspace_view_large, ConstraintSet.END, - sharedR.id.weather_smartspace_view_large, - ConstraintSet.START, - ) - - connect( - sharedR.id.weather_smartspace_view_large, - ConstraintSet.BOTTOM, - sharedR.id.date_smartspace_view_large, - ConstraintSet.BOTTOM, - ) - - connect( - sharedR.id.weather_smartspace_view_large, - ConstraintSet.TOP, - sharedR.id.date_smartspace_view_large, - ConstraintSet.TOP, - ) - - connect( - sharedR.id.weather_smartspace_view_large, - ConstraintSet.START, - sharedR.id.date_smartspace_view_large, - ConstraintSet.END, - ) - - connect( - sharedR.id.weather_smartspace_view_large, - ConstraintSet.END, customR.id.lockscreen_clock_view_large, ConstraintSet.END, ) - - setHorizontalChainStyle( - sharedR.id.weather_smartspace_view_large, - ConstraintSet.CHAIN_PACKED, - ) setHorizontalChainStyle( sharedR.id.date_smartspace_view_large, ConstraintSet.CHAIN_PACKED, ) } else { - setVisibility(sharedR.id.weather_smartspace_view_large, GONE) - setVisibility(sharedR.id.date_smartspace_view_large, GONE) - constrainHeight(sharedR.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT) - constrainWidth(sharedR.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT) - constrainHeight(sharedR.id.weather_smartspace_view, ConstraintSet.WRAP_CONTENT) - constrainWidth(sharedR.id.weather_smartspace_view, ConstraintSet.WRAP_CONTENT) + if (dateWeatherBelowSmallClock) { + connect( + sharedR.id.date_smartspace_view, + ConstraintSet.START, + ConstraintSet.PARENT_ID, + ConstraintSet.START, + dateWeatherPaddingStart, + ) + } else { + setVisibility(sharedR.id.date_smartspace_view_large, GONE) + constrainHeight(sharedR.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT) + constrainWidth(sharedR.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT) + connect( + sharedR.id.date_smartspace_view, + ConstraintSet.START, + customR.id.lockscreen_clock_view, + ConstraintSet.END, + context.resources.getDimensionPixelSize( + R.dimen.smartspace_padding_horizontal + ), + ) + connect( + sharedR.id.date_smartspace_view, + ConstraintSet.TOP, + customR.id.lockscreen_clock_view, + ConstraintSet.TOP, + ) + connect( + sharedR.id.date_smartspace_view, + ConstraintSet.BOTTOM, + customR.id.lockscreen_clock_view, + ConstraintSet.BOTTOM, + ) + } + } + } - connect( - sharedR.id.date_smartspace_view, - ConstraintSet.START, - customR.id.lockscreen_clock_view, - ConstraintSet.END, - context.resources.getDimensionPixelSize( - R.dimen.smartspace_padding_horizontal - ), - ) - connect( - sharedR.id.date_smartspace_view, - ConstraintSet.TOP, - customR.id.lockscreen_clock_view, - ConstraintSet.TOP, - ) - connect( - sharedR.id.date_smartspace_view, - ConstraintSet.BOTTOM, - sharedR.id.weather_smartspace_view, - ConstraintSet.TOP, - ) - connect( - sharedR.id.weather_smartspace_view, - ConstraintSet.START, - sharedR.id.date_smartspace_view, - ConstraintSet.START, - ) - connect( - sharedR.id.weather_smartspace_view, - ConstraintSet.TOP, - sharedR.id.date_smartspace_view, - ConstraintSet.BOTTOM, + if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { + if (dateWeatherBelowSmallClock) { + createBarrier( + R.id.smart_space_barrier_bottom, + Barrier.BOTTOM, + 0, + *intArrayOf(sharedR.id.bc_smartspace_view, sharedR.id.date_smartspace_view), ) - connect( - sharedR.id.weather_smartspace_view, - ConstraintSet.BOTTOM, - customR.id.lockscreen_clock_view, - ConstraintSet.BOTTOM, + createBarrier( + R.id.smart_space_barrier_top, + Barrier.TOP, + 0, + *intArrayOf(sharedR.id.bc_smartspace_view, sharedR.id.date_smartspace_view), ) - - setVerticalChainStyle( - sharedR.id.weather_smartspace_view, - ConstraintSet.CHAIN_PACKED, + } else { + createBarrier( + R.id.smart_space_barrier_bottom, + Barrier.BOTTOM, + 0, + sharedR.id.bc_smartspace_view, ) - setVerticalChainStyle( - sharedR.id.date_smartspace_view, - ConstraintSet.CHAIN_PACKED, + createBarrier( + R.id.smart_space_barrier_top, + Barrier.TOP, + 0, + sharedR.id.bc_smartspace_view, ) } - } - - if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { - createBarrier( - R.id.smart_space_barrier_bottom, - Barrier.BOTTOM, - 0, - sharedR.id.bc_smartspace_view, - ) - createBarrier( - R.id.smart_space_barrier_top, - Barrier.TOP, - 0, - sharedR.id.bc_smartspace_view, - ) } else { createBarrier( R.id.smart_space_barrier_bottom, @@ -373,13 +365,7 @@ constructor( val list = if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { - listOf( - smartspaceView, - dateView, - weatherView, - weatherViewLargeClock, - dateViewLargeClock, - ) + listOf(smartspaceView, dateView, dateViewLargeClock) } else { listOf(smartspaceView, dateView) } @@ -424,10 +410,8 @@ constructor( if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { if (keyguardClockViewModel.isLargeClockVisible.value) { - setVisibility(sharedR.id.weather_smartspace_view, GONE) setVisibility(sharedR.id.date_smartspace_view, GONE) } else { - setVisibility(sharedR.id.weather_smartspace_view_large, GONE) setVisibility(sharedR.id.date_smartspace_view_large, GONE) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt index 434d7eadd742..d830a8456d66 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt @@ -299,14 +299,12 @@ class ClockSizeTransition( } if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { addTarget(sharedR.id.date_smartspace_view_large) - addTarget(sharedR.id.weather_smartspace_view_large) } } else { logger.i("Adding small clock") addTarget(customR.id.lockscreen_clock_view) - if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { + if (!viewModel.dateWeatherBelowSmallClock()) { addTarget(sharedR.id.date_smartspace_view) - addTarget(sharedR.id.weather_smartspace_view) } } } @@ -386,7 +384,7 @@ class ClockSizeTransition( duration = if (isLargeClock) STATUS_AREA_MOVE_UP_MILLIS else STATUS_AREA_MOVE_DOWN_MILLIS interpolator = Interpolators.EMPHASIZED - if (!com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { + if (viewModel.dateWeatherBelowSmallClock()) { addTarget(sharedR.id.date_smartspace_view) } addTarget(sharedR.id.bc_smartspace_view) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/DefaultClockSteppingTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/DefaultClockSteppingTransition.kt index 0874b6da180e..9faca7567279 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/DefaultClockSteppingTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/DefaultClockSteppingTransition.kt @@ -32,7 +32,6 @@ class DefaultClockSteppingTransition(private val clock: ClockController) : Trans addTarget(clock.largeClock.view) if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { addTarget(sharedR.id.date_smartspace_view_large) - addTarget(sharedR.id.weather_smartspace_view_large) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModel.kt index 38f5d3e76c7c..678872d0d64d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor @@ -33,7 +34,8 @@ class AccessibilityActionsViewModel constructor( private val communalInteractor: CommunalInteractor, keyguardInteractor: KeyguardInteractor, - keyguardTransitionInteractor: KeyguardTransitionInteractor, + val keyguardTransitionInteractor: KeyguardTransitionInteractor, + private val udfpsOverlayInteractor: UdfpsOverlayInteractor, ) { val isCommunalAvailable = communalInteractor.isCommunalAvailable @@ -44,7 +46,7 @@ constructor( keyguardTransitionInteractor.transitionValue(KeyguardState.LOCKSCREEN).map { it == 1f }, - keyguardInteractor.statusBarState + keyguardInteractor.statusBarState, ) { transitionFinishedOnLockscreen, statusBarState -> transitionFinishedOnLockscreen && statusBarState == StatusBarState.KEYGUARD } @@ -55,4 +57,12 @@ constructor( newScene = CommunalScenes.Communal, loggingReason = "accessibility", ) + + /** + * Clears the content description to prevent the view from storing stale UDFPS directional + * guidance messages for accessibility. + */ + suspend fun clearUdfpsAccessibilityOverlayMessage(reason: String) { + udfpsOverlayInteractor.clearUdfpsAccessibilityOverlayMessage(reason) + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt index 9b4bd67f227e..729edcf7e5ba 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt @@ -44,6 +44,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf @@ -253,6 +254,13 @@ constructor( val isLongPressEnabled: Flow<Boolean> = isInteractive + val deviceDidNotEnterFromDeviceEntryIcon = + deviceEntrySourceInteractor.attemptEnterDeviceFromDeviceEntryIcon + .map { keyguardInteractor.isKeyguardDismissible.value } + .filterNot { it } // only emit events if the keyguard is not dismissible + // map to Unit + .map {} + suspend fun onUserInteraction() { if (SceneContainerFlag.isEnabled) { deviceEntryInteractor.attemptDeviceEntry() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerTransitionViewModel.kt new file mode 100644 index 000000000000..8771f02326fa --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerTransitionViewModel.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor +import com.android.systemui.keyguard.shared.model.Edge +import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING +import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER +import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow +import com.android.systemui.keyguard.ui.transitions.BlurConfig +import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition +import com.android.systemui.scene.shared.model.Overlays +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow + +@SysUISingleton +class DreamingToPrimaryBouncerTransitionViewModel +@Inject +constructor(blurConfig: BlurConfig, animationFlow: KeyguardTransitionAnimationFlow) : + PrimaryBouncerTransition { + private val transitionAnimation = + animationFlow + .setup( + duration = FromDreamingTransitionInteractor.TO_PRIMARY_BOUNCER_DURATION, + edge = Edge.create(from = DREAMING, to = Overlays.Bouncer), + ) + .setupWithoutSceneContainer(edge = Edge.create(from = DREAMING, to = PRIMARY_BOUNCER)) + + override val windowBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) + + override val notificationBlurRadius: Flow<Float> = emptyFlow() +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt index bcbe66642d11..fd5783ef7f8e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt @@ -19,7 +19,10 @@ package com.android.systemui.keyguard.ui.viewmodel import android.util.LayoutDirection import com.android.app.animation.Interpolators.EMPHASIZED import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.dagger.GlanceableHubBlurComponent import com.android.systemui.keyguard.domain.interactor.FromGlanceableHubTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION import com.android.systemui.keyguard.shared.model.Edge @@ -34,21 +37,32 @@ import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.ShadeDisplayAware import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn /** * Breaks down GLANCEABLE_HUB->LOCKSCREEN transition into discrete steps for corresponding views to * consume. */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class GlanceableHubToLockscreenTransitionViewModel @Inject constructor( + @Application applicationScope: CoroutineScope, @ShadeDisplayAware configurationInteractor: ConfigurationInteractor, animationFlow: KeyguardTransitionAnimationFlow, + communalSceneInteractor: CommunalSceneInteractor, + communalSettingsInteractor: CommunalSettingsInteractor, private val blurFactory: GlanceableHubBlurComponent.Factory, ) : GlanceableHubTransition, DeviceEntryIconTransition { private val transitionAnimation = @@ -59,18 +73,45 @@ constructor( ) .setupWithoutSceneContainer(edge = Edge.create(from = GLANCEABLE_HUB, to = LOCKSCREEN)) + // Whether screen rotation will happen with the transition. Only emit when idle so ongoing + // animation won't be interrupted when orientation is updated during the transition. + private val willRotateToPortraitInTransition: StateFlow<Boolean> = + if (!communalSettingsInteractor.isV2FlagEnabled()) { + flowOf(false) + } else { + communalSceneInteractor.isIdleOnCommunal.combineTransform( + communalSceneInteractor.willRotateToPortrait + ) { isIdle, willRotate -> + if (isIdle) emit(willRotate) + } + } + .stateIn(applicationScope, SharingStarted.Eagerly, false) + override val windowBlurRadius: Flow<Float> = blurFactory.create(transitionAnimation).getBlurProvider().exitBlurRadius val keyguardAlpha: Flow<Float> = - transitionAnimation.sharedFlow( - duration = 167.milliseconds, - startTime = 167.milliseconds, - onStep = { it }, - onFinish = { 1f }, - onCancel = { 0f }, - name = "GLANCEABLE_HUB->LOCKSCREEN: keyguardAlpha", - ) + willRotateToPortraitInTransition.flatMapLatest { willRotate -> + transitionAnimation.sharedFlow( + duration = 167.milliseconds, + // If will rotate, start later to leave time for screen rotation. + startTime = if (willRotate) 500.milliseconds else 167.milliseconds, + onStep = { step -> + if (willRotate) { + if (!communalSceneInteractor.rotatedToPortrait.value) { + 0f + } else { + 1f + } + } else { + step + } + }, + onFinish = { 1f }, + onCancel = { 0f }, + name = "GLANCEABLE_HUB->LOCKSCREEN: keyguardAlpha", + ) + } // Show UMO as long as keyguard is not visible. val showUmo: Flow<Boolean> = keyguardAlpha.map { alpha -> alpha == 0f } @@ -84,7 +125,14 @@ constructor( .flatMapLatest { translatePx: Int -> transitionAnimation.sharedFlowWithState( duration = TO_LOCKSCREEN_DURATION, - onStep = { value -> -translatePx + value * translatePx }, + onStep = { value -> + // do not animate translation-x if screen rotation will happen + if (willRotateToPortraitInTransition.value) { + 0f + } else { + -translatePx + value * translatePx + } + }, interpolator = EMPHASIZED, // Move notifications back to their original position since they can be // accessed from the shade, and also keyguard elements in case the animation diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModel.kt index 6d95ade59211..4bc722bc6695 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModel.kt @@ -30,15 +30,15 @@ import kotlinx.coroutines.flow.Flow @SysUISingleton class GoneToGlanceableHubTransitionViewModel @Inject -constructor( - animationFlow: KeyguardTransitionAnimationFlow, -) : DeviceEntryIconTransition { +constructor(animationFlow: KeyguardTransitionAnimationFlow) : DeviceEntryIconTransition { private val transitionAnimation = animationFlow .setup(duration = TO_GLANCEABLE_HUB_DURATION, edge = Edge.INVALID) .setupWithoutSceneContainer(edge = Edge.create(GONE, GLANCEABLE_HUB)) + val keyguardAlpha = transitionAnimation.immediatelyTransitionTo(0f) + override val deviceEntryParentViewAlpha: Flow<Float> = transitionAnimation.sharedFlow( duration = 167.milliseconds, 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 dcbf7b5a9335..cf6845354f44 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 @@ -180,6 +180,9 @@ constructor( val largeClockTextSize: Flow<Int> = configurationInteractor.dimensionPixelSize(customR.dimen.large_clock_text_size) + fun dateWeatherBelowSmallClock() = + KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(context.resources.configuration) + enum class ClockLayout { LARGE_CLOCK, SMALL_CLOCK, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index 830afeac7b96..a5051657c2f0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -116,6 +116,7 @@ constructor( private val goneToDozingTransitionViewModel: GoneToDozingTransitionViewModel, private val goneToDreamingTransitionViewModel: GoneToDreamingTransitionViewModel, private val goneToLockscreenTransitionViewModel: GoneToLockscreenTransitionViewModel, + private val goneToGlanceableHubTransitionViewModel: GoneToGlanceableHubTransitionViewModel, private val lockscreenToAodTransitionViewModel: LockscreenToAodTransitionViewModel, private val lockscreenToDozingTransitionViewModel: LockscreenToDozingTransitionViewModel, private val lockscreenToDreamingTransitionViewModel: LockscreenToDreamingTransitionViewModel, @@ -277,6 +278,7 @@ constructor( primaryBouncerToAodTransitionViewModel.lockscreenAlpha, primaryBouncerToGoneTransitionViewModel.lockscreenAlpha, primaryBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState), + goneToGlanceableHubTransitionViewModel.keyguardAlpha, ) .onStart { emit(0f) }, ) { hideKeyguard, alpha -> diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt index f8425c16c341..a00d0ced2c07 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt @@ -17,12 +17,14 @@ package com.android.systemui.keyguard.ui.viewmodel import android.content.Context +import android.content.res.Configuration +import android.util.Log import com.android.systemui.customization.R as customR import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor import com.android.systemui.res.R -import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -40,7 +42,7 @@ constructor( smartspaceController: LockscreenSmartspaceController, keyguardClockViewModel: KeyguardClockViewModel, smartspaceInteractor: KeyguardSmartspaceInteractor, - shadeInteractor: ShadeInteractor, + shadeModeInteractor: ShadeModeInteractor, ) { /** Whether the smartspace section is available in the build. */ val isSmartspaceEnabled: Boolean = smartspaceController.isEnabled @@ -91,9 +93,46 @@ constructor( /* trigger clock and smartspace constraints change when smartspace appears */ val bcSmartspaceVisibility: StateFlow<Int> = smartspaceInteractor.bcSmartspaceVisibility - val isShadeLayoutWide: StateFlow<Boolean> = shadeInteractor.isShadeLayoutWide + val isShadeLayoutWide: StateFlow<Boolean> = shadeModeInteractor.isShadeLayoutWide companion object { + private const val TAG = "KeyguardSmartspaceVM" + + fun dateWeatherBelowSmallClock( + configuration: Configuration, + customDateWeather: Boolean = false, + ): Boolean { + return if ( + com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout() && + !customDateWeather + ) { + // font size to display size + // These values come from changing the font size and display size on a non-foldable. + // Visually looked at which configs cause the date/weather to push off of the screen + val breakingPairs = + listOf( + 0.85f to 320, // tiny font size but large display size + 1f to 346, + 1.15f to 346, + 1.5f to 376, + 1.8f to 411, // large font size but tiny display size + ) + val screenWidthDp = configuration.screenWidthDp + val fontScale = configuration.fontScale + var fallBelow = false + for ((font, width) in breakingPairs) { + if (fontScale >= font && screenWidthDp <= width) { + fallBelow = true + break + } + } + Log.d(TAG, "Width: $screenWidthDp, Font: $fontScale, BelowClock: $fallBelow") + return fallBelow + } else { + true + } + } + fun getDateWeatherStartMargin(context: Context): Int { return context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start) + context.resources.getDimensionPixelSize(customR.dimen.status_view_margin_horizontal) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModel.kt new file mode 100644 index 000000000000..9de25fcac64a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModel.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor +import com.android.systemui.keyguard.shared.model.Edge +import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING +import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER +import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow +import com.android.systemui.keyguard.ui.transitions.BlurConfig +import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition +import com.android.systemui.scene.shared.model.Overlays +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow + +@SysUISingleton +class PrimaryBouncerToDreamingTransitionViewModel +@Inject +constructor(blurConfig: BlurConfig, animationFlow: KeyguardTransitionAnimationFlow) : + PrimaryBouncerTransition { + private val transitionAnimation = + animationFlow + .setup( + duration = FromPrimaryBouncerTransitionInteractor.TO_DREAMING_DURATION, + edge = Edge.create(from = Overlays.Bouncer, to = DREAMING), + ) + .setupWithoutSceneContainer(edge = Edge.create(from = PRIMARY_BOUNCER, to = DREAMING)) + + override val windowBlurRadius: Flow<Float> = + transitionAnimation.sharedFlow( + onStart = { blurConfig.maxBlurRadiusPx }, + onStep = { + transitionProgressToBlurRadius( + blurConfig.maxBlurRadiusPx, + endBlurRadius = blurConfig.minBlurRadiusPx, + transitionProgress = it, + ) + }, + onFinish = { blurConfig.minBlurRadiusPx }, + ) + + override val notificationBlurRadius: Flow<Float> = emptyFlow() +} diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt index c1658e1f1694..5e9e930812a6 100644 --- a/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt @@ -23,7 +23,7 @@ import android.hardware.SensorEventListener import android.hardware.SensorManager import android.util.Log import com.android.systemui.Dumpable -import com.android.systemui.lowlightclock.dagger.LowLightModule.LIGHT_SENSOR +import com.android.systemui.lowlightclock.dagger.LowLightModule.Companion.LIGHT_SENSOR import com.android.systemui.util.sensors.AsyncSensorManager import java.io.PrintWriter import java.util.Optional diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java deleted file mode 100644 index e5eec64ac615..000000000000 --- a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.systemui.lowlightclock; - -import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT; -import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR; -import static com.android.systemui.dreams.dagger.DreamModule.LOW_LIGHT_DREAM_SERVICE; -import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_ON; -import static com.android.systemui.lowlightclock.dagger.LowLightModule.LOW_LIGHT_PRECONDITIONS; - -import android.content.ComponentName; -import android.content.pm.PackageManager; - -import androidx.annotation.Nullable; - -import com.android.dream.lowlight.LowLightDreamManager; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.SystemUser; -import com.android.systemui.keyguard.ScreenLifecycle; -import com.android.systemui.shared.condition.Condition; -import com.android.systemui.shared.condition.Monitor; -import com.android.systemui.util.condition.ConditionalCoreStartable; - -import dagger.Lazy; - -import java.util.Set; -import java.util.concurrent.Executor; - -import javax.inject.Inject; -import javax.inject.Named; - -/** - * Tracks environment (low-light or not) in order to correctly show or hide a low-light clock while - * dreaming. - */ -public class LowLightMonitor extends ConditionalCoreStartable implements Monitor.Callback, - ScreenLifecycle.Observer { - private static final String TAG = "LowLightMonitor"; - - private final Lazy<LowLightDreamManager> mLowLightDreamManager; - private final Monitor mConditionsMonitor; - private final Lazy<Set<Condition>> mLowLightConditions; - private Monitor.Subscription.Token mSubscriptionToken; - private ScreenLifecycle mScreenLifecycle; - private final LowLightLogger mLogger; - - private final ComponentName mLowLightDreamService; - - private final PackageManager mPackageManager; - - private final Executor mExecutor; - - @Inject - public LowLightMonitor(Lazy<LowLightDreamManager> lowLightDreamManager, - @SystemUser Monitor conditionsMonitor, - @Named(LOW_LIGHT_PRECONDITIONS) Lazy<Set<Condition>> lowLightConditions, - ScreenLifecycle screenLifecycle, - LowLightLogger lowLightLogger, - @Nullable @Named(LOW_LIGHT_DREAM_SERVICE) ComponentName lowLightDreamService, - PackageManager packageManager, - @Background Executor backgroundExecutor) { - super(conditionsMonitor); - mLowLightDreamManager = lowLightDreamManager; - mConditionsMonitor = conditionsMonitor; - mLowLightConditions = lowLightConditions; - mScreenLifecycle = screenLifecycle; - mLogger = lowLightLogger; - mLowLightDreamService = lowLightDreamService; - mPackageManager = packageManager; - mExecutor = backgroundExecutor; - } - - @Override - public void onConditionsChanged(boolean allConditionsMet) { - mExecutor.execute(() -> { - mLogger.d(TAG, "Low light enabled: " + allConditionsMet); - - mLowLightDreamManager.get().setAmbientLightMode(allConditionsMet - ? AMBIENT_LIGHT_MODE_LOW_LIGHT : AMBIENT_LIGHT_MODE_REGULAR); - }); - } - - @Override - public void onScreenTurnedOn() { - mExecutor.execute(() -> { - if (mSubscriptionToken == null) { - mLogger.d(TAG, "Screen turned on. Subscribing to low light conditions."); - - mSubscriptionToken = mConditionsMonitor.addSubscription( - new Monitor.Subscription.Builder(this) - .addConditions(mLowLightConditions.get()) - .build()); - } - }); - } - - - @Override - public void onScreenTurnedOff() { - mExecutor.execute(() -> { - if (mSubscriptionToken != null) { - mLogger.d(TAG, "Screen turned off. Removing subscription to low light conditions."); - - mConditionsMonitor.removeSubscription(mSubscriptionToken); - mSubscriptionToken = null; - } - }); - } - - @Override - protected void onStart() { - mExecutor.execute(() -> { - if (mLowLightDreamService != null) { - // Note that the dream service is disabled by default. This prevents the dream from - // appearing in settings on devices that don't have it explicitly excluded (done in - // the settings overlay). Therefore, the component is enabled if it is to be used - // here. - mPackageManager.setComponentEnabledSetting( - mLowLightDreamService, - PackageManager.COMPONENT_ENABLED_STATE_ENABLED, - PackageManager.DONT_KILL_APP - ); - } else { - // If there is no low light dream service, do not observe conditions. - return; - } - - mScreenLifecycle.addObserver(this); - if (mScreenLifecycle.getScreenState() == SCREEN_ON) { - onScreenTurnedOn(); - } - }); - - } -} diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.kt new file mode 100644 index 000000000000..137226332e38 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.lowlightclock + +import android.content.ComponentName +import android.content.pm.PackageManager +import com.android.dream.lowlight.LowLightDreamManager +import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.SystemUser +import com.android.systemui.dreams.dagger.DreamModule +import com.android.systemui.lowlightclock.dagger.LowLightModule +import com.android.systemui.shared.condition.Condition +import com.android.systemui.shared.condition.Monitor +import com.android.systemui.util.condition.ConditionalCoreStartable +import com.android.systemui.util.kotlin.BooleanFlowOperators.not +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import dagger.Lazy +import javax.inject.Inject +import javax.inject.Named +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch + +/** + * Tracks environment (low-light or not) in order to correctly show or hide a low-light clock while + * dreaming. + */ +class LowLightMonitor +@Inject +constructor( + private val lowLightDreamManager: Lazy<LowLightDreamManager>, + @param:SystemUser private val conditionsMonitor: Monitor, + @param:Named(LowLightModule.LOW_LIGHT_PRECONDITIONS) + private val lowLightConditions: Lazy<Set<Condition>>, + displayStateInteractor: DisplayStateInteractor, + private val logger: LowLightLogger, + @param:Named(DreamModule.LOW_LIGHT_DREAM_SERVICE) + private val lowLightDreamService: ComponentName?, + private val packageManager: PackageManager, + @Background private val scope: CoroutineScope, +) : ConditionalCoreStartable(conditionsMonitor) { + private val isScreenOn = not(displayStateInteractor.isDefaultDisplayOff).distinctUntilChanged() + + private val isLowLight = conflatedCallbackFlow { + val token = + conditionsMonitor.addSubscription( + Monitor.Subscription.Builder { trySend(it) } + .addConditions(lowLightConditions.get()) + .build() + ) + + awaitClose { conditionsMonitor.removeSubscription(token) } + } + + @OptIn(ExperimentalCoroutinesApi::class) + override fun onStart() { + scope.launch { + if (lowLightDreamService != null) { + // Note that the dream service is disabled by default. This prevents the dream from + // appearing in settings on devices that don't have it explicitly excluded (done in + // the settings overlay). Therefore, the component is enabled if it is to be used + // here. + packageManager.setComponentEnabledSetting( + lowLightDreamService, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + PackageManager.DONT_KILL_APP, + ) + } else { + // If there is no low light dream service, do not observe conditions. + return@launch + } + + isScreenOn + .flatMapLatest { + if (it) { + isLowLight + } else { + flowOf(false) + } + } + .distinctUntilChanged() + .collect { + logger.d(TAG, "Low light enabled: $it") + lowLightDreamManager + .get() + .setAmbientLightMode( + if (it) LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT + else LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR + ) + } + } + } + + companion object { + private const val TAG = "LowLightMonitor" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java deleted file mode 100644 index f8072f2f79b4..000000000000 --- a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.lowlightclock.dagger; - -import android.annotation.Nullable; -import android.content.res.Resources; -import android.hardware.Sensor; - -import com.android.dream.lowlight.dagger.LowLightDreamModule; -import com.android.systemui.CoreStartable; -import com.android.systemui.communal.DeviceInactiveCondition; -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.log.LogBuffer; -import com.android.systemui.log.LogBufferFactory; -import com.android.systemui.lowlightclock.AmbientLightModeMonitor; -import com.android.systemui.lowlightclock.DirectBootCondition; -import com.android.systemui.lowlightclock.ForceLowLightCondition; -import com.android.systemui.lowlightclock.LowLightCondition; -import com.android.systemui.lowlightclock.LowLightDisplayController; -import com.android.systemui.lowlightclock.LowLightMonitor; -import com.android.systemui.lowlightclock.ScreenSaverEnabledCondition; -import com.android.systemui.res.R; -import com.android.systemui.shared.condition.Condition; - -import dagger.Binds; -import dagger.BindsOptionalOf; -import dagger.Module; -import dagger.Provides; -import dagger.multibindings.ClassKey; -import dagger.multibindings.IntoMap; -import dagger.multibindings.IntoSet; - -import javax.inject.Named; - -@Module(includes = LowLightDreamModule.class) -public abstract class LowLightModule { - public static final String Y_TRANSLATION_ANIMATION_OFFSET = - "y_translation_animation_offset"; - public static final String Y_TRANSLATION_ANIMATION_DURATION_MILLIS = - "y_translation_animation_duration_millis"; - public static final String ALPHA_ANIMATION_IN_START_DELAY_MILLIS = - "alpha_animation_in_start_delay_millis"; - public static final String ALPHA_ANIMATION_DURATION_MILLIS = - "alpha_animation_duration_millis"; - public static final String LOW_LIGHT_PRECONDITIONS = "low_light_preconditions"; - public static final String LIGHT_SENSOR = "low_light_monitor_light_sensor"; - - - /** - * Provides a {@link LogBuffer} for logs related to low-light features. - */ - @Provides - @SysUISingleton - @LowLightLog - public static LogBuffer provideLowLightLogBuffer(LogBufferFactory factory) { - return factory.create("LowLightLog", 250); - } - - @Binds - @IntoSet - @Named(LOW_LIGHT_PRECONDITIONS) - abstract Condition bindScreenSaverEnabledCondition(ScreenSaverEnabledCondition condition); - - @Provides - @IntoSet - @Named(LOW_LIGHT_PRECONDITIONS) - static Condition provideLowLightCondition(LowLightCondition lowLightCondition, - DirectBootCondition directBootCondition) { - // Start lowlight if we are either in lowlight or in direct boot. The ordering of the - // conditions matters here since we don't want to start the lowlight condition if - // we are in direct boot mode. - return directBootCondition.or(lowLightCondition); - } - - @Binds - @IntoSet - @Named(LOW_LIGHT_PRECONDITIONS) - abstract Condition bindForceLowLightCondition(ForceLowLightCondition condition); - - @Binds - @IntoSet - @Named(LOW_LIGHT_PRECONDITIONS) - abstract Condition bindDeviceInactiveCondition(DeviceInactiveCondition condition); - - @BindsOptionalOf - abstract LowLightDisplayController bindsLowLightDisplayController(); - - @BindsOptionalOf - @Nullable - @Named(LIGHT_SENSOR) - abstract Sensor bindsLightSensor(); - - @BindsOptionalOf - abstract AmbientLightModeMonitor.DebounceAlgorithm bindsDebounceAlgorithm(); - - /** - * - */ - @Provides - @Named(Y_TRANSLATION_ANIMATION_OFFSET) - static int providesAnimationInOffset(@Main Resources resources) { - return resources.getDimensionPixelOffset( - R.dimen.low_light_clock_translate_animation_offset); - } - - /** - * - */ - @Provides - @Named(Y_TRANSLATION_ANIMATION_DURATION_MILLIS) - static long providesAnimationDurationMillis(@Main Resources resources) { - return resources.getInteger(R.integer.low_light_clock_translate_animation_duration_ms); - } - - /** - * - */ - @Provides - @Named(ALPHA_ANIMATION_IN_START_DELAY_MILLIS) - static long providesAlphaAnimationInStartDelayMillis(@Main Resources resources) { - return resources.getInteger(R.integer.low_light_clock_alpha_animation_in_start_delay_ms); - } - - /** - * - */ - @Provides - @Named(ALPHA_ANIMATION_DURATION_MILLIS) - static long providesAlphaAnimationDurationMillis(@Main Resources resources) { - return resources.getInteger(R.integer.low_light_clock_alpha_animation_duration_ms); - } - /** Inject into LowLightMonitor. */ - @Binds - @IntoMap - @ClassKey(LowLightMonitor.class) - abstract CoreStartable bindLowLightMonitor(LowLightMonitor lowLightMonitor); -} diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.kt new file mode 100644 index 000000000000..6b3254e928ec --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.kt @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.lowlightclock.dagger + +import android.content.res.Resources +import android.hardware.Sensor +import com.android.dream.lowlight.dagger.LowLightDreamModule +import com.android.systemui.CoreStartable +import com.android.systemui.communal.DeviceInactiveCondition +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LogBufferFactory +import com.android.systemui.lowlightclock.AmbientLightModeMonitor.DebounceAlgorithm +import com.android.systemui.lowlightclock.DirectBootCondition +import com.android.systemui.lowlightclock.ForceLowLightCondition +import com.android.systemui.lowlightclock.LowLightCondition +import com.android.systemui.lowlightclock.LowLightDisplayController +import com.android.systemui.lowlightclock.LowLightMonitor +import com.android.systemui.lowlightclock.ScreenSaverEnabledCondition +import com.android.systemui.res.R +import com.android.systemui.shared.condition.Condition +import dagger.Binds +import dagger.BindsOptionalOf +import dagger.Module +import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import dagger.multibindings.IntoSet +import javax.inject.Named + +@Module(includes = [LowLightDreamModule::class]) +abstract class LowLightModule { + @Binds + @IntoSet + @Named(LOW_LIGHT_PRECONDITIONS) + abstract fun bindScreenSaverEnabledCondition(condition: ScreenSaverEnabledCondition): Condition + + @Binds + @IntoSet + @Named(LOW_LIGHT_PRECONDITIONS) + abstract fun bindForceLowLightCondition(condition: ForceLowLightCondition): Condition + + @Binds + @IntoSet + @Named(LOW_LIGHT_PRECONDITIONS) + abstract fun bindDeviceInactiveCondition(condition: DeviceInactiveCondition): Condition + + @BindsOptionalOf abstract fun bindsLowLightDisplayController(): LowLightDisplayController + + @BindsOptionalOf @Named(LIGHT_SENSOR) abstract fun bindsLightSensor(): Sensor + + @BindsOptionalOf abstract fun bindsDebounceAlgorithm(): DebounceAlgorithm + + /** Inject into LowLightMonitor. */ + @Binds + @IntoMap + @ClassKey(LowLightMonitor::class) + abstract fun bindLowLightMonitor(lowLightMonitor: LowLightMonitor): CoreStartable + + companion object { + const val Y_TRANSLATION_ANIMATION_OFFSET: String = "y_translation_animation_offset" + const val Y_TRANSLATION_ANIMATION_DURATION_MILLIS: String = + "y_translation_animation_duration_millis" + const val ALPHA_ANIMATION_IN_START_DELAY_MILLIS: String = + "alpha_animation_in_start_delay_millis" + const val ALPHA_ANIMATION_DURATION_MILLIS: String = "alpha_animation_duration_millis" + const val LOW_LIGHT_PRECONDITIONS: String = "low_light_preconditions" + const val LIGHT_SENSOR: String = "low_light_monitor_light_sensor" + + /** Provides a [LogBuffer] for logs related to low-light features. */ + @JvmStatic + @Provides + @SysUISingleton + @LowLightLog + fun provideLowLightLogBuffer(factory: LogBufferFactory): LogBuffer { + return factory.create("LowLightLog", 250) + } + + @Provides + @IntoSet + @Named(LOW_LIGHT_PRECONDITIONS) + fun provideLowLightCondition( + lowLightCondition: LowLightCondition, + directBootCondition: DirectBootCondition, + ): Condition { + // Start lowlight if we are either in lowlight or in direct boot. The ordering of the + // conditions matters here since we don't want to start the lowlight condition if + // we are in direct boot mode. + return directBootCondition.or(lowLightCondition) + } + + /** */ + @JvmStatic + @Provides + @Named(Y_TRANSLATION_ANIMATION_OFFSET) + fun providesAnimationInOffset(@Main resources: Resources): Int { + return resources.getDimensionPixelOffset( + R.dimen.low_light_clock_translate_animation_offset + ) + } + + /** */ + @JvmStatic + @Provides + @Named(Y_TRANSLATION_ANIMATION_DURATION_MILLIS) + fun providesAnimationDurationMillis(@Main resources: Resources): Long { + return resources + .getInteger(R.integer.low_light_clock_translate_animation_duration_ms) + .toLong() + } + + /** */ + @JvmStatic + @Provides + @Named(ALPHA_ANIMATION_IN_START_DELAY_MILLIS) + fun providesAlphaAnimationInStartDelayMillis(@Main resources: Resources): Long { + return resources + .getInteger(R.integer.low_light_clock_alpha_animation_in_start_delay_ms) + .toLong() + } + + /** */ + @JvmStatic + @Provides + @Named(ALPHA_ANIMATION_DURATION_MILLIS) + fun providesAlphaAnimationDurationMillis(@Main resources: Resources): Long { + return resources + .getInteger(R.integer.low_light_clock_alpha_animation_duration_ms) + .toLong() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt index c2efc7559487..ab4467e87a3e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt @@ -875,11 +875,7 @@ class LegacyMediaDataManagerImpl( desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT - val progress = - if (mediaFlags.isResumeProgressEnabled()) { - MediaDataUtils.getDescriptionProgress(desc.extras) - } else null - + val progress = MediaDataUtils.getDescriptionProgress(desc.extras) val mediaAction = getResumeMediaAction(resumeAction) val lastActive = systemClock.elapsedRealtime() val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt index a7c5a36b804a..1a4687b59dbd 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt @@ -339,11 +339,7 @@ constructor( desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT - val progress = - if (mediaFlags.isResumeProgressEnabled()) { - MediaDataUtils.getDescriptionProgress(desc.extras) - } else null - + val progress = MediaDataUtils.getDescriptionProgress(desc.extras) val mediaAction = getResumeMediaAction(resumeAction) return MediaDataLoaderResult( appName = appName, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt index ca4a65953cba..7dfa69efc155 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt @@ -719,11 +719,7 @@ class MediaDataProcessor( desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT - val progress = - if (mediaFlags.isResumeProgressEnabled()) { - MediaDataUtils.getDescriptionProgress(desc.extras) - } else null - + val progress = MediaDataUtils.getDescriptionProgress(desc.extras) val mediaAction = getResumeMediaAction(resumeAction) val lastActive = systemClock.elapsedRealtime() val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt index df0e1adee968..2e0e3a78c5b7 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt @@ -24,7 +24,6 @@ import android.media.session.MediaController import android.media.session.PlaybackState import android.os.BadParcelableException import android.util.Log -import com.android.systemui.Flags.mediaControlsPostsOptimization import com.android.systemui.biometrics.Utils.toBitmap import com.android.systemui.media.controls.shared.model.MediaData @@ -45,7 +44,7 @@ fun isSameMediaData( new: MediaData, old: MediaData?, ): Boolean { - if (old == null || !mediaControlsPostsOptimization()) return false + if (old == null) return false return new.userId == old.userId && new.app == old.app && diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt index 5b65531cdd55..f81745704d2b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt @@ -157,6 +157,7 @@ open class SeekBarObserver(private val holder: MediaViewHolder) : return DateUtils.formatElapsedTime(milliseconds / DateUtils.SECOND_IN_MILLIS) } + @UiThread fun updateContentDescription( elapsedTimeDescription: CharSequence, durationDescription: CharSequence, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java index f69985ee5364..9cf7356a0ab2 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java @@ -399,7 +399,9 @@ public class MediaControlPanel { } private void setSeekbarContentDescription(CharSequence elapsedTime, CharSequence duration) { - mSeekBarObserver.updateContentDescription(elapsedTime, duration); + mMainExecutor.execute(() -> { + mSeekBarObserver.updateContentDescription(elapsedTime, duration); + }); } /** diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt index e87d5de56177..8c683e8f9749 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt @@ -236,10 +236,12 @@ constructor( durationDescription: CharSequence, ) { if (!SceneContainerFlag.isEnabled) return - seekBarObserver.updateContentDescription( - elapsedTimeDescription, - durationDescription, - ) + mainExecutor.execute { + seekBarObserver.updateContentDescription( + elapsedTimeDescription, + durationDescription, + ) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt index 78a8cf8e9432..6175b6e47b52 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt @@ -36,7 +36,6 @@ import androidx.annotation.WorkerThread import androidx.core.view.GestureDetectorCompat import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import com.android.systemui.Flags import com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.media.NotificationMediaManager @@ -151,7 +150,6 @@ constructor( } override fun onMetadataChanged(metadata: MediaMetadata?) { - if (!Flags.mediaControlsPostsOptimization()) return val (enabled, duration) = getEnabledStateAndDuration(metadata) if (_data.duration != duration) { _data = _data.copy(enabled = enabled, duration = duration) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt index 172998e09266..8ad10ba2a240 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt @@ -46,9 +46,6 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlagsClass */ fun isRetainingPlayersEnabled() = featureFlags.isEnabled(FlagsClassic.MEDIA_RETAIN_SESSIONS) - /** Check whether to get progress information for resume players */ - fun isResumeProgressEnabled() = featureFlags.isEnabled(FlagsClassic.MEDIA_RESUME_PROGRESS) - /** Check whether we allow remote media to generate resume controls */ fun isRemoteResumeAllowed() = featureFlags.isEnabled(FlagsClassic.MEDIA_REMOTE_RESUME) } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaItem.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaItem.java index 7b1c62e2a0e5..78e66235112a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaItem.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaItem.java @@ -37,16 +37,21 @@ public class MediaItem { @MediaItemType private final int mMediaItemType; private final boolean mIsFirstDeviceInGroup; + private final boolean mIsExpandableDivider; + private final boolean mHasTopSeparator; @Retention(RetentionPolicy.SOURCE) @IntDef({ MediaItemType.TYPE_DEVICE, MediaItemType.TYPE_GROUP_DIVIDER, - MediaItemType.TYPE_PAIR_NEW_DEVICE}) + MediaItemType.TYPE_PAIR_NEW_DEVICE, + MediaItemType.TYPE_DEVICE_GROUP + }) public @interface MediaItemType { int TYPE_DEVICE = 0; int TYPE_GROUP_DIVIDER = 1; int TYPE_PAIR_NEW_DEVICE = 2; + int TYPE_DEVICE_GROUP = 3; } /** @@ -70,6 +75,18 @@ public class MediaItem { } /** + * Returns a new {@link MediaItemType#TYPE_DEVICE_GROUP} {@link MediaItem}. This items controls + * the volume of the group session. + */ + public static MediaItem createDeviceGroupMediaItem() { + return new MediaItem( + /* device */ null, + /* title */ null, + /* type */ MediaItemType.TYPE_DEVICE_GROUP, + /* misFirstDeviceInGroup */ false); + } + + /** * Returns a new {@link MediaItemType#TYPE_PAIR_NEW_DEVICE} {@link MediaItem} with both {@link * #getMediaDevice() media device} and title set to {@code null}. */ @@ -93,15 +110,58 @@ public class MediaItem { /* misFirstDeviceInGroup */ false); } + /** + * Returns a new {@link MediaItemType#TYPE_GROUP_DIVIDER} {@link MediaItem} with the specified + * title and a {@code null} {@link #getMediaDevice() media device}. This item needs to be + * rendered with a separator above it. + */ + public static MediaItem createGroupDividerWithSeparatorMediaItem(@Nullable String title) { + return new MediaItem( + /* device */ null, + title, + MediaItemType.TYPE_GROUP_DIVIDER, + /* isFirstDeviceInGroup */ false, + /* isExpandableDivider */ false, + /* hasTopSeparator */ true); + } + + /** + * Returns a new {@link MediaItemType#TYPE_GROUP_DIVIDER} {@link MediaItem} with the specified + * title and a {@code null} {@link #getMediaDevice() media device}. The item serves as a toggle + * for expanding/collapsing the group of devices. + */ + public static MediaItem createExpandableGroupDividerMediaItem(@Nullable String title) { + return new MediaItem( + /* device */ null, + title, + MediaItemType.TYPE_GROUP_DIVIDER, + /* isFirstDeviceInGroup */ false, + /* isExpandableDivider */ true, + /* hasTopSeparator */ false); + } + private MediaItem( @Nullable MediaDevice device, @Nullable String title, @MediaItemType int type, boolean isFirstDeviceInGroup) { + this(device, title, type, isFirstDeviceInGroup, /* isExpandableDivider */ + false, /* hasTopSeparator */ false); + } + + private MediaItem( + @Nullable MediaDevice device, + @Nullable String title, + @MediaItemType int type, + boolean isFirstDeviceInGroup, + boolean isExpandableDivider, + boolean hasTopSeparator) { this.mMediaDeviceOptional = Optional.ofNullable(device); this.mTitle = title; this.mMediaItemType = type; this.mIsFirstDeviceInGroup = isFirstDeviceInGroup; + this.mIsExpandableDivider = isExpandableDivider; + this.mHasTopSeparator = hasTopSeparator; } public Optional<MediaDevice> getMediaDevice() { @@ -133,4 +193,14 @@ public class MediaItem { public boolean isFirstDeviceInGroup() { return mIsFirstDeviceInGroup; } + + /** Returns whether a group divider has a button that expands group device list */ + public boolean isExpandableDivider() { + return mIsExpandableDivider; + } + + /** Returns whether a group divider has a border at the top */ + public boolean hasTopSeparator() { + return mHasTopSeparator; + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.kt new file mode 100644 index 000000000000..4c34250c9653 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.kt @@ -0,0 +1,688 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.media.dialog + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.Typeface +import android.graphics.drawable.AnimatedVectorDrawable +import android.graphics.drawable.Drawable +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.ProgressBar +import android.widget.TextView +import androidx.annotation.VisibleForTesting +import androidx.appcompat.content.res.AppCompatResources +import androidx.recyclerview.widget.RecyclerView +import com.android.settingslib.media.InputMediaDevice +import com.android.settingslib.media.MediaDevice +import com.android.systemui.FontStyles.GSF_TITLE_MEDIUM_EMPHASIZED +import com.android.systemui.FontStyles.GSF_TITLE_SMALL +import com.android.systemui.media.dialog.MediaItem.MediaItemType.TYPE_DEVICE +import com.android.systemui.media.dialog.MediaItem.MediaItemType.TYPE_DEVICE_GROUP +import com.android.systemui.media.dialog.MediaItem.MediaItemType.TYPE_GROUP_DIVIDER +import com.android.systemui.media.dialog.MediaOutputAdapterBase.ConnectionState.CONNECTED +import com.android.systemui.media.dialog.MediaOutputAdapterBase.ConnectionState.CONNECTING +import com.android.systemui.media.dialog.MediaOutputAdapterBase.ConnectionState.DISCONNECTED +import com.android.systemui.res.R +import com.android.systemui.util.kotlin.getOrNull +import com.google.android.material.slider.Slider + +/** A RecyclerView adapter for the legacy UI media output dialog device list. */ +class MediaOutputAdapter(controller: MediaSwitchingController) : + MediaOutputAdapterBase(controller) { + private val mGroupSelectedItems = mController.selectedMediaDevice.size > 1 + + /** Refreshes the RecyclerView dataset and forces re-render. */ + override fun updateItems() { + val newList = + mController.getMediaItemList(false /* addConnectNewDeviceButton */).toMutableList() + + addSeparatorForTheFirstGroupDivider(newList) + coalesceSelectedDevices(newList) + + mMediaItemList.clear() + mMediaItemList.addAll(newList) + + notifyDataSetChanged() + } + + private fun addSeparatorForTheFirstGroupDivider(newList: MutableList<MediaItem>) { + for ((i, item) in newList.withIndex()) { + if (item.mediaItemType == TYPE_GROUP_DIVIDER) { + newList[i] = MediaItem.createGroupDividerWithSeparatorMediaItem(item.title) + break + } + } + } + + /** + * If there are 2+ selected devices, adds an "Connected speakers" expandable group divider and + * displays a single session control instead of individual device controls. + */ + private fun coalesceSelectedDevices(newList: MutableList<MediaItem>) { + val selectedDevices = newList.filter { this.isSelectedDevice(it) } + + if (mGroupSelectedItems && selectedDevices.size > 1) { + newList.removeAll(selectedDevices.toSet()) + if (mController.isGroupListCollapsed) { + newList.add(0, MediaItem.createDeviceGroupMediaItem()) + } else { + newList.addAll(0, selectedDevices) + } + newList.add(0, mController.connectedSpeakersExpandableGroupDivider) + } + } + + private fun isSelectedDevice(mediaItem: MediaItem): Boolean { + return mediaItem.mediaDevice.getOrNull()?.let { device -> + isDeviceIncluded(mController.selectedMediaDevice, device) + } ?: false + } + + override fun getItemId(position: Int): Long { + if (position >= mMediaItemList.size) { + Log.e(TAG, "Item position exceeds list size: $position") + return RecyclerView.NO_ID + } + val currentMediaItem = mMediaItemList[position] + return when (currentMediaItem.mediaItemType) { + TYPE_DEVICE -> + currentMediaItem.mediaDevice.getOrNull()?.id?.hashCode()?.toLong() + ?: RecyclerView.NO_ID + TYPE_GROUP_DIVIDER -> currentMediaItem.title.hashCode().toLong() + TYPE_DEVICE_GROUP -> currentMediaItem.hashCode().toLong() + else -> RecyclerView.NO_ID + } + } + + override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val context = viewGroup.context + return when (viewType) { + TYPE_GROUP_DIVIDER -> { + val holderView = + LayoutInflater.from(context) + .inflate(R.layout.media_output_list_item_group_divider, viewGroup, false) + MediaGroupDividerViewHolder(holderView, context) + } + + TYPE_DEVICE, + TYPE_DEVICE_GROUP -> { + val holderView = + LayoutInflater.from(context) + .inflate(R.layout.media_output_list_item_device, viewGroup, false) + MediaDeviceViewHolder(holderView, context) + } + + else -> throw IllegalArgumentException("Invalid view type: $viewType") + } + } + + override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) { + require(position < itemCount) { "Invalid position: $position, list size: $itemCount" } + val currentMediaItem = mMediaItemList[position] + when (currentMediaItem.mediaItemType) { + TYPE_GROUP_DIVIDER -> + (viewHolder as MediaGroupDividerViewHolder).onBind( + groupDividerTitle = currentMediaItem.title, + isExpandableDivider = currentMediaItem.isExpandableDivider, + hasTopSeparator = currentMediaItem.hasTopSeparator(), + ) + + TYPE_DEVICE -> + (viewHolder as MediaDeviceViewHolder).onBindDevice( + mediaItem = currentMediaItem, + position = position, + ) + + TYPE_DEVICE_GROUP -> (viewHolder as MediaDeviceViewHolder).onBindDeviceGroup() + else -> + throw IllegalArgumentException( + "Invalid item type ${currentMediaItem.mediaItemType} for position: $position" + ) + } + } + + val controller: MediaSwitchingController + get() = mController + + /** ViewHolder for binding device view. */ + inner class MediaDeviceViewHolder(view: View, context: Context?) : + MediaDeviceViewHolderBase(view, context) { + @VisibleForTesting val mMainContent: LinearLayout = view.requireViewById(R.id.main_content) + + @VisibleForTesting val mItemLayout: LinearLayout = view.requireViewById(R.id.item_layout) + + @VisibleForTesting val mTitleText: TextView = view.requireViewById(R.id.title) + + @VisibleForTesting val mSubTitleText: TextView = view.requireViewById(R.id.subtitle) + + @VisibleForTesting val mTitleIcon: ImageView = view.requireViewById(R.id.title_icon) + + @VisibleForTesting + val mLoadingIndicator: ProgressBar = view.requireViewById(R.id.loading_indicator) + + @VisibleForTesting val mStatusIcon: ImageView = view.requireViewById(R.id.status_icon) + + @VisibleForTesting val mGroupButton: ImageButton = view.requireViewById(R.id.group_button) + + @VisibleForTesting val mDivider: View = view.requireViewById(R.id.divider) + + @VisibleForTesting + val mOngoingSessionButton: ImageButton = view.requireViewById(R.id.ongoing_session_button) + + @VisibleForTesting var mSlider: Slider = view.requireViewById(R.id.volume_seekbar) + private var mLatestUpdateVolume = NO_VOLUME_SET + + private val mInactivePadding = + mContext.resources.getDimension(R.dimen.media_output_item_content_vertical_margin) + private val mActivePadding = + mContext.resources.getDimension( + R.dimen.media_output_item_content_vertical_margin_active + ) + private val mSubtitleAlpha = + mContext.resources.getFloat(R.dimen.media_output_item_subtitle_alpha) + + fun onBindDevice(mediaItem: MediaItem, position: Int) { + resetViewState() + renderItem(mediaItem, position) + } + + fun onBindDeviceGroup() { + resetViewState() + renderDeviceGroupItem() + } + + private fun resetViewState() { + mItemLayout.visibility = VISIBLE + mGroupButton.visibility = GONE + mOngoingSessionButton.visibility = GONE + mStatusIcon.visibility = GONE + mLoadingIndicator.visibility = GONE + mDivider.visibility = GONE + mSubTitleText.visibility = GONE + mMainContent.setOnClickListener(null) + } + + override fun renderDeviceItem( + hideGroupItem: Boolean, + device: MediaDevice, + connectionState: ConnectionState, + restrictVolumeAdjustment: Boolean, + groupStatus: GroupStatus?, + ongoingSessionStatus: OngoingSessionStatus?, + clickListener: View.OnClickListener?, + deviceDisabled: Boolean, + subtitle: String?, + deviceStatusIcon: Drawable?, + ) { + val fixedVolumeConnected = connectionState == CONNECTED && restrictVolumeAdjustment + val colorTheme = ColorTheme(fixedVolumeConnected, deviceDisabled) + + updateTitle(device.name, connectionState, colorTheme) + updateTitleIcon(device, connectionState, restrictVolumeAdjustment, colorTheme) + updateSubtitle(subtitle, colorTheme) + updateSeekBar(device, connectionState, restrictVolumeAdjustment, colorTheme) + updateEndArea(device, connectionState, groupStatus, ongoingSessionStatus, colorTheme) + updateLoadingIndicator(connectionState, colorTheme) + updateDeviceStatusIcon(deviceStatusIcon, colorTheme) + updateContentBackground(fixedVolumeConnected, colorTheme) + updateContentClickListener(clickListener) + } + + override fun renderDeviceGroupItem() { + mTitleIcon.visibility = GONE + val colorTheme = ColorTheme() + updateTitle( + title = mController.sessionName ?: "", + connectionState = CONNECTED, + colorTheme = colorTheme, + ) + updateGroupSeekBar(colorTheme) + } + + private fun updateTitle( + title: CharSequence, + connectionState: ConnectionState, + colorTheme: ColorTheme, + ) { + mTitleText.text = title + val fontFamilyName: String = + if (connectionState == CONNECTED) GSF_TITLE_MEDIUM_EMPHASIZED else GSF_TITLE_SMALL + mTitleText.typeface = Typeface.create(fontFamilyName, Typeface.NORMAL) + mTitleText.setTextColor(colorTheme.titleColor) + mTitleText.alpha = colorTheme.contentAlpha + } + + private fun updateContentBackground(fixedVolumeConnected: Boolean, colorTheme: ColorTheme) { + if (fixedVolumeConnected) { + mMainContent.backgroundTintList = + ColorStateList.valueOf(colorTheme.containerRestrictedVolumeBackground) + mMainContent.background = + AppCompatResources.getDrawable( + mContext, + R.drawable.media_output_dialog_item_fixed_volume_background, + ) + } else { + mMainContent.background = null + mMainContent.setBackgroundColor(Color.TRANSPARENT) + } + } + + private fun updateContentPadding(verticalPadding: Float) { + mMainContent.setPadding(0, verticalPadding.toInt(), 0, verticalPadding.toInt()) + } + + private fun updateLayoutForSlider(showSlider: Boolean) { + updateContentPadding(if (showSlider) mActivePadding else mInactivePadding) + mSlider.visibility = if (showSlider) VISIBLE else GONE + mSlider.alpha = if (showSlider) 1f else 0f + } + + private fun updateSeekBar( + device: MediaDevice, + connectionState: ConnectionState, + restrictVolumeAdjustment: Boolean, + colorTheme: ColorTheme, + ) { + val showSlider = connectionState == CONNECTED && !restrictVolumeAdjustment + if (showSlider) { + updateLayoutForSlider(showSlider = true) + initSeekbar( + volumeChangeCallback = { volume: Int -> + mController.adjustVolume(device, volume) + }, + settleCallback = { mController.logInteractionAdjustVolume(device) }, + deviceDrawable = mController.getDeviceIconDrawable(device), + isInputDevice = device is InputMediaDevice, + isVolumeControlAllowed = mController.isVolumeControlEnabled(device), + currentVolume = device.currentVolume, + maxVolume = device.maxVolume, + colorTheme = colorTheme, + ) + } else { + updateLayoutForSlider(showSlider = false) + } + } + + private fun updateGroupSeekBar(colorTheme: ColorTheme) { + mSlider.visibility = VISIBLE + updateContentPadding(mActivePadding) + val groupDrawable = + AppCompatResources.getDrawable( + mContext, + com.android.settingslib.R.drawable.ic_media_group_device, + ) + initSeekbar( + volumeChangeCallback = { volume: Int -> mController.adjustSessionVolume(volume) }, + deviceDrawable = groupDrawable, + isVolumeControlAllowed = mController.isVolumeControlEnabledForSession, + currentVolume = mController.sessionVolume, + maxVolume = mController.sessionVolumeMax, + colorTheme = colorTheme, + ) + } + + private fun updateSubtitle(subtitle: String?, colorTheme: ColorTheme) { + if (subtitle.isNullOrEmpty()) { + mSubTitleText.visibility = GONE + } else { + mSubTitleText.text = subtitle + mSubTitleText.setTextColor(colorTheme.subtitleColor) + mSubTitleText.alpha = mSubtitleAlpha * colorTheme.contentAlpha + mSubTitleText.visibility = VISIBLE + } + } + + private fun updateLoadingIndicator( + connectionState: ConnectionState, + colorTheme: ColorTheme, + ) { + if (connectionState == CONNECTING) { + mLoadingIndicator.visibility = VISIBLE + mLoadingIndicator.indeterminateDrawable.setTintList( + ColorStateList.valueOf(colorTheme.statusIconColor) + ) + } else { + mLoadingIndicator.visibility = GONE + } + } + + private fun initializeSeekbarVolume(currentVolume: Int) { + tryResolveVolumeUserRequest(currentVolume) + if (!isDragging && hasNoPendingVolumeRequests()) { + mSlider.value = currentVolume.toFloat() + } + } + + private fun tryResolveVolumeUserRequest(currentVolume: Int) { + if (currentVolume == mLatestUpdateVolume) { + mLatestUpdateVolume = NO_VOLUME_SET + } + } + + private fun hasNoPendingVolumeRequests(): Boolean { + return mLatestUpdateVolume == NO_VOLUME_SET + } + + private fun setLatestVolumeRequest(volume: Int) { + mLatestUpdateVolume = volume + } + + private fun initSeekbar( + volumeChangeCallback: (Int) -> Unit, + settleCallback: () -> Unit = {}, + deviceDrawable: Drawable?, + isInputDevice: Boolean = false, + isVolumeControlAllowed: Boolean, + currentVolume: Int, + maxVolume: Int, + colorTheme: ColorTheme, + ) { + if (maxVolume == 0) { + Log.e(TAG, "Invalid maxVolume value") + // Slider doesn't allow valueFrom == valueTo, return to prevent crash. + return + } + + mSlider.isEnabled = isVolumeControlAllowed + mSlider.valueFrom = 0f + mSlider.valueTo = maxVolume.toFloat() + mSlider.stepSize = 1f + mSlider.thumbTintList = ColorStateList.valueOf(colorTheme.sliderActiveColor) + mSlider.trackActiveTintList = ColorStateList.valueOf(colorTheme.sliderActiveColor) + mSlider.trackInactiveTintList = ColorStateList.valueOf(colorTheme.sliderInactiveColor) + mSlider.trackIconActiveColor = ColorStateList.valueOf(colorTheme.sliderActiveIconColor) + mSlider.trackIconInactiveColor = + ColorStateList.valueOf(colorTheme.sliderInactiveIconColor) + val muteDrawable = getMuteDrawable(isInputDevice) + updateSliderIconsVisibility( + deviceDrawable = deviceDrawable, + muteDrawable = muteDrawable, + isMuted = currentVolume == 0, + ) + initializeSeekbarVolume(currentVolume) + + mSlider.clearOnChangeListeners() // Prevent adding multiple listeners + mSlider.addOnChangeListener { _: Slider, value: Float, fromUser: Boolean -> + if (fromUser) { + val seekBarVolume = value.toInt() + updateSliderIconsVisibility( + deviceDrawable = deviceDrawable, + muteDrawable = muteDrawable, + isMuted = seekBarVolume == 0, + ) + if (seekBarVolume != currentVolume) { + setLatestVolumeRequest(seekBarVolume) + volumeChangeCallback(seekBarVolume) + } + } + } + + mSlider.clearOnSliderTouchListeners() // Prevent adding multiple listeners + mSlider.addOnSliderTouchListener( + object : Slider.OnSliderTouchListener { + override fun onStartTrackingTouch(slider: Slider) { + setIsDragging(true) + } + + override fun onStopTrackingTouch(slider: Slider) { + setIsDragging(false) + settleCallback() + } + } + ) + } + + private fun getMuteDrawable(isInputDevice: Boolean): Drawable? { + return AppCompatResources.getDrawable( + mContext, + if (isInputDevice) R.drawable.ic_mic_off + else R.drawable.media_output_icon_volume_off, + ) + } + + private fun updateSliderIconsVisibility( + deviceDrawable: Drawable?, + muteDrawable: Drawable?, + isMuted: Boolean, + ) { + mSlider.trackIconInactiveStart = if (isMuted) muteDrawable else null + // A workaround for the slider glitch that sometimes shows the active icon in inactive + // state. + mSlider.trackIconActiveStart = if (isMuted) null else deviceDrawable + } + + private fun updateTitleIcon( + device: MediaDevice, + connectionState: ConnectionState, + restrictVolumeAdjustment: Boolean, + colorTheme: ColorTheme, + ) { + if (connectionState == CONNECTED && !restrictVolumeAdjustment) { + mTitleIcon.visibility = GONE + } else { + mTitleIcon.imageTintList = ColorStateList.valueOf(colorTheme.iconColor) + val drawable = mController.getDeviceIconDrawable(device) + mTitleIcon.setImageDrawable(drawable) + mTitleIcon.visibility = VISIBLE + mTitleIcon.alpha = colorTheme.contentAlpha + } + } + + private fun updateDeviceStatusIcon(deviceStatusIcon: Drawable?, colorTheme: ColorTheme) { + if (deviceStatusIcon == null) { + mStatusIcon.visibility = GONE + } else { + mStatusIcon.setImageDrawable(deviceStatusIcon) + mStatusIcon.alpha = colorTheme.contentAlpha + mStatusIcon.imageTintList = ColorStateList.valueOf(colorTheme.statusIconColor) + mStatusIcon.visibility = VISIBLE + } + } + + private fun updateEndArea( + device: MediaDevice, + connectionState: ConnectionState, + groupStatus: GroupStatus?, + ongoingSessionStatus: OngoingSessionStatus?, + colorTheme: ColorTheme, + ) { + var showDivider = false + + if (ongoingSessionStatus != null) { + showDivider = true + mOngoingSessionButton.visibility = VISIBLE + updateOngoingSessionButton(device, ongoingSessionStatus.host, colorTheme) + } + + if (groupStatus != null && shouldShowGroupCheckbox(groupStatus)) { + showDivider = true + mGroupButton.visibility = VISIBLE + updateGroupButton(device, groupStatus, colorTheme) + } + + mDivider.visibility = + if (showDivider && connectionState == DISCONNECTED) VISIBLE else GONE + mDivider.setBackgroundColor(mController.colorScheme.getOutline()) + } + + private fun shouldShowGroupCheckbox(groupStatus: GroupStatus): Boolean { + val disabled = groupStatus.selected && !groupStatus.deselectable + return !disabled + } + + private fun updateOngoingSessionButton( + device: MediaDevice, + isHost: Boolean, + colorTheme: ColorTheme, + ) { + val iconDrawableId = + if (isHost) R.drawable.media_output_status_edit_session + else R.drawable.ic_sound_bars_anim + mOngoingSessionButton.setOnClickListener { v: View? -> + mController.tryToLaunchInAppRoutingIntent(device.id, v) + } + val drawable = AppCompatResources.getDrawable(mContext, iconDrawableId) + mOngoingSessionButton.setImageDrawable(drawable) + mOngoingSessionButton.imageTintList = ColorStateList.valueOf(colorTheme.iconColor) + if (drawable is AnimatedVectorDrawable) { + drawable.start() + } + } + + private fun updateGroupButton( + device: MediaDevice, + groupStatus: GroupStatus, + colorTheme: ColorTheme, + ) { + mGroupButton.contentDescription = + mContext.getString( + if (groupStatus.selected) R.string.accessibility_remove_device_from_group + else R.string.accessibility_add_device_to_group + ) + mGroupButton.setImageResource( + if (groupStatus.selected) R.drawable.ic_check_circle_filled + else R.drawable.ic_add_circle_rounded + ) + mGroupButton.setOnClickListener { + onGroupActionTriggered(!groupStatus.selected, device) + } + mGroupButton.imageTintList = ColorStateList.valueOf(colorTheme.iconColor) + } + + private fun updateContentClickListener(listener: View.OnClickListener?) { + mMainContent.setOnClickListener(listener) + if (listener == null) { + mMainContent.isClickable = false // clickable is not removed automatically. + } + } + + override fun disableSeekBar() { + mSlider.isEnabled = false + } + } + + inner class MediaGroupDividerViewHolder(itemView: View, val mContext: Context) : + RecyclerView.ViewHolder(itemView) { + private val mTopSeparator: View = itemView.requireViewById(R.id.top_separator) + private val mTitleText: TextView = itemView.requireViewById(R.id.title) + @VisibleForTesting + val mExpandButton: ViewGroup = itemView.requireViewById(R.id.expand_button) + private val mExpandButtonIcon: ImageView = itemView.requireViewById(R.id.expand_button_icon) + + fun onBind( + groupDividerTitle: String?, + isExpandableDivider: Boolean, + hasTopSeparator: Boolean, + ) { + mTitleText.text = groupDividerTitle + mTitleText.setTextColor(mController.colorScheme.getPrimary()) + if (hasTopSeparator) { + mTopSeparator.visibility = VISIBLE + mTopSeparator.setBackgroundColor(mController.colorScheme.getOutlineVariant()) + } else { + mTopSeparator.visibility = GONE + } + updateExpandButton(isExpandableDivider) + } + + private fun updateExpandButton(isExpandableDivider: Boolean) { + if (!isExpandableDivider) { + mExpandButton.visibility = GONE + return + } + val isCollapsed = mController.isGroupListCollapsed + mExpandButtonIcon.setImageDrawable( + AppCompatResources.getDrawable( + mContext, + if (isCollapsed) R.drawable.ic_expand_more_rounded + else R.drawable.ic_expand_less_rounded, + ) + ) + mExpandButtonIcon.contentDescription = + mContext.getString( + if (isCollapsed) R.string.accessibility_expand_group + else R.string.accessibility_collapse_group + ) + mExpandButton.visibility = VISIBLE + mExpandButton.setOnClickListener { toggleGroupList() } + mExpandButtonIcon.backgroundTintList = + ColorStateList.valueOf(mController.colorScheme.getOnSurface()) + .withAlpha((255 * 0.1).toInt()) + mExpandButtonIcon.imageTintList = + ColorStateList.valueOf(mController.colorScheme.getOnSurface()) + } + + private fun toggleGroupList() { + mController.isGroupListCollapsed = !mController.isGroupListCollapsed + updateItems() + } + } + + private inner class ColorTheme( + isConnectedWithFixedVolume: Boolean = false, + deviceDisabled: Boolean = false, + ) { + private val colorScheme: MediaOutputColorScheme = mController.colorScheme + + val titleColor = + if (isConnectedWithFixedVolume) { + colorScheme.getOnPrimary() + } else { + colorScheme.getOnSurface() + } + val subtitleColor = + if (isConnectedWithFixedVolume) { + colorScheme.getOnPrimary() + } else { + colorScheme.getOnSurfaceVariant() + } + val iconColor = + if (isConnectedWithFixedVolume) { + colorScheme.getOnPrimary() + } else { + colorScheme.getOnSurface() + } + val statusIconColor = + if (isConnectedWithFixedVolume) { + colorScheme.getOnPrimary() + } else { + colorScheme.getOnSurfaceVariant() + } + val sliderActiveColor = colorScheme.getPrimary() + val sliderActiveIconColor = colorScheme.getOnPrimary() + val sliderInactiveColor = colorScheme.getSecondaryContainer() + val sliderInactiveIconColor = colorScheme.getOnSurface() + val containerRestrictedVolumeBackground = colorScheme.getPrimary() + val contentAlpha = if (deviceDisabled) DEVICE_DISABLED_ALPHA else DEVICE_ACTIVE_ALPHA + } + + companion object { + private const val TAG = "MediaOutputAdapter" + private const val DEVICE_DISABLED_ALPHA = 0.5f + private const val DEVICE_ACTIVE_ALPHA = 1f + private const val NO_VOLUME_SET = -1 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java index e3990d25f94e..d46cca2736da 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java @@ -19,6 +19,7 @@ package com.android.systemui.media.dialog; import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_GO_TO_APP; import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_NONE; import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER; +import static com.android.media.flags.Flags.enableOutputSwitcherRedesign; import android.content.Context; import android.graphics.drawable.Drawable; @@ -46,11 +47,11 @@ import java.util.concurrent.CopyOnWriteArrayList; * manipulate the layout directly. */ public abstract class MediaOutputAdapterBase extends RecyclerView.Adapter<RecyclerView.ViewHolder> { - record OngoingSessionStatus(boolean host) {} + public record OngoingSessionStatus(boolean host) {} - record GroupStatus(Boolean selected, Boolean deselectable) {} + public record GroupStatus(Boolean selected, Boolean deselectable) {} - enum ConnectionState { + public enum ConnectionState { CONNECTED, CONNECTING, DISCONNECTED, @@ -138,7 +139,7 @@ public abstract class MediaOutputAdapterBase extends RecyclerView.Adapter<Recycl return mMediaItemList.size(); } - abstract class MediaDeviceViewHolderBase extends RecyclerView.ViewHolder { + public abstract class MediaDeviceViewHolderBase extends RecyclerView.ViewHolder { Context mContext; @@ -211,7 +212,8 @@ public abstract class MediaOutputAdapterBase extends RecyclerView.Adapter<Recycl clickListener = v -> cancelMuteAwaitConnection(); } else if (device.getState() == MediaDeviceState.STATE_GROUPING) { connectionState = ConnectionState.CONNECTING; - } else if (mShouldGroupSelectedMediaItems && hasMultipleSelectedDevices() + } else if (!enableOutputSwitcherRedesign() && mShouldGroupSelectedMediaItems + && hasMultipleSelectedDevices() && isSelected) { if (mediaItem.isFirstDeviceInGroup()) { isDeviceGroup = true; diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java index 49d09cf64c8e..7f9370ca671d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java @@ -19,16 +19,23 @@ package com.android.systemui.media.dialog; import static android.view.WindowInsets.Type.navigationBars; import static android.view.WindowInsets.Type.statusBars; +import static com.android.media.flags.Flags.enableOutputSwitcherRedesign; +import static com.android.systemui.FontStyles.GSF_LABEL_LARGE; +import static com.android.systemui.FontStyles.GSF_TITLE_MEDIUM_EMPHASIZED; +import static com.android.systemui.FontStyles.GSF_TITLE_SMALL; + import android.annotation.NonNull; import android.app.WallpaperColors; import android.bluetooth.BluetoothLeBroadcast; import android.bluetooth.BluetoothLeBroadcastMetadata; import android.content.Context; import android.content.SharedPreferences; +import android.content.res.ColorStateList; import android.content.res.Configuration; import android.graphics.ColorFilter; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; +import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.os.Bundle; @@ -49,6 +56,7 @@ import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.VisibleForTesting; +import androidx.appcompat.content.res.AppCompatResources; import androidx.core.graphics.drawable.IconCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -57,6 +65,8 @@ import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.res.R; import com.android.systemui.statusbar.phone.SystemUIDialog; +import com.google.android.material.button.MaterialButton; + import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -71,7 +81,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog private static final int HANDLE_BROADCAST_FAILED_DELAY = 3000; protected final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); - private final RecyclerView.LayoutManager mLayoutManager; + private final LinearLayoutManager mLayoutManager; final Context mContext; final MediaSwitchingController mMediaSwitchingController; @@ -93,8 +103,12 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog private ImageView mBroadcastIcon; private RecyclerView mDevicesRecyclerView; private ViewGroup mDeviceListLayout; + private ViewGroup mQuickAccessShelf; + private MaterialButton mConnectDeviceButton; private LinearLayout mMediaMetadataSectionLayout; private Button mDoneButton; + private ViewGroup mDialogFooter; + private View mFooterSpacer; private Button mStopButton; private WallpaperColors mWallpaperColors; private boolean mShouldLaunchLeBroadcastDialog; @@ -229,7 +243,11 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog mHeaderTitle = mDialogView.requireViewById(R.id.header_title); mHeaderSubtitle = mDialogView.requireViewById(R.id.header_subtitle); mHeaderIcon = mDialogView.requireViewById(R.id.header_icon); + mQuickAccessShelf = mDialogView.requireViewById(R.id.quick_access_shelf); + mConnectDeviceButton = mDialogView.requireViewById(R.id.connect_device); mDevicesRecyclerView = mDialogView.requireViewById(R.id.list_result); + mDialogFooter = mDialogView.requireViewById(R.id.dialog_footer); + mFooterSpacer = mDialogView.requireViewById(R.id.footer_spacer); mMediaMetadataSectionLayout = mDialogView.requireViewById(R.id.media_metadata_section); mDeviceListLayout = mDialogView.requireViewById(R.id.device_list); mDoneButton = mDialogView.requireViewById(R.id.done); @@ -252,6 +270,49 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog } mDismissing = false; + + if (enableOutputSwitcherRedesign()) { + // Reduce radius of dialog background. + mDialogView.setBackground(AppCompatResources.getDrawable(mContext, + R.drawable.media_output_dialog_background_reduced_radius)); + // Set non-transparent footer background to change it color on scroll. + mDialogFooter.setBackground(AppCompatResources.getDrawable(mContext, + R.drawable.media_output_dialog_footer_background)); + // Right-align the footer buttons. + LinearLayout.LayoutParams layoutParams = + (LinearLayout.LayoutParams) mFooterSpacer.getLayoutParams(); + layoutParams.width = (int) mContext.getResources().getDimension( + R.dimen.media_output_dialog_button_gap); + mFooterSpacer.setLayoutParams(layoutParams); + layoutParams.weight = 0; + // Update font family to Google Sans Flex. + Typeface buttonTypeface = Typeface.create(GSF_LABEL_LARGE, Typeface.NORMAL); + mDoneButton.setTypeface(buttonTypeface); + mStopButton.setTypeface(buttonTypeface); + mHeaderTitle + .setTypeface(Typeface.create(GSF_TITLE_MEDIUM_EMPHASIZED, Typeface.NORMAL)); + mHeaderSubtitle + .setTypeface(Typeface.create(GSF_TITLE_SMALL, Typeface.NORMAL)); + // Reduce the size of the app icon. + float appIconSize = mContext.getResources().getDimension( + R.dimen.media_output_dialog_app_icon_size); + float appIconBottomMargin = mContext.getResources().getDimension( + R.dimen.media_output_dialog_app_icon_bottom_margin); + ViewGroup.MarginLayoutParams params = + (ViewGroup.MarginLayoutParams) mAppResourceIcon.getLayoutParams(); + params.bottomMargin = (int) appIconBottomMargin; + params.width = (int) appIconSize; + params.height = (int) appIconSize; + mAppResourceIcon.setLayoutParams(params); + // Change footer background color on scroll. + mDevicesRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + changeFooterColorForScroll(); + } + }); + } } @Override @@ -366,6 +427,18 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog } } + if (enableOutputSwitcherRedesign()) { + if (mMediaSwitchingController.getConnectNewDeviceItem() != null) { + mQuickAccessShelf.setVisibility(View.VISIBLE); + mConnectDeviceButton.setVisibility(View.VISIBLE); + mConnectDeviceButton.setOnClickListener( + mMediaSwitchingController::launchBluetoothPairing); + } else { + mQuickAccessShelf.setVisibility(View.GONE); + mConnectDeviceButton.setVisibility(View.GONE); + } + } + // Show when remote media session is available or // when the device supports BT LE audio + media is playing mStopButton.setVisibility(getStopButtonVisibility()); @@ -390,21 +463,48 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog } private void updateButtonBackgroundColorFilter() { - ColorFilter buttonColorFilter = - new PorterDuffColorFilter( - mMediaSwitchingController.getColorSchemeLegacy().getColorButtonBackground(), - PorterDuff.Mode.SRC_IN); - mDoneButton.getBackground().setColorFilter(buttonColorFilter); - mStopButton.getBackground().setColorFilter(buttonColorFilter); - mDoneButton.setTextColor( - mMediaSwitchingController.getColorSchemeLegacy().getColorPositiveButtonText()); + if (enableOutputSwitcherRedesign()) { + mDoneButton.getBackground().setTint( + mMediaSwitchingController.getColorScheme().getPrimary()); + mDoneButton.setTextColor(mMediaSwitchingController.getColorScheme().getOnPrimary()); + mStopButton.getBackground().setTint( + mMediaSwitchingController.getColorScheme().getOutlineVariant()); + mStopButton.setTextColor(mMediaSwitchingController.getColorScheme().getPrimary()); + mConnectDeviceButton.setTextColor( + mMediaSwitchingController.getColorScheme().getOnSurfaceVariant()); + mConnectDeviceButton.setStrokeColor(ColorStateList.valueOf( + mMediaSwitchingController.getColorScheme().getOutlineVariant())); + mConnectDeviceButton.setIconTint(ColorStateList.valueOf( + mMediaSwitchingController.getColorScheme().getPrimary())); + } else { + ColorFilter buttonColorFilter = new PorterDuffColorFilter( + mMediaSwitchingController.getColorSchemeLegacy().getColorButtonBackground(), + PorterDuff.Mode.SRC_IN); + mDoneButton.getBackground().setColorFilter(buttonColorFilter); + mStopButton.getBackground().setColorFilter(buttonColorFilter); + mDoneButton.setTextColor( + mMediaSwitchingController.getColorSchemeLegacy().getColorPositiveButtonText()); + } } private void updateDialogBackgroundColor() { - getDialogView().getBackground().setTint( - mMediaSwitchingController.getColorSchemeLegacy().getColorDialogBackground()); - mDeviceListLayout.setBackgroundColor( - mMediaSwitchingController.getColorSchemeLegacy().getColorDialogBackground()); + int backgroundColor = enableOutputSwitcherRedesign() + ? mMediaSwitchingController.getColorScheme().getSurfaceContainer() + : mMediaSwitchingController.getColorSchemeLegacy().getColorDialogBackground(); + getDialogView().getBackground().setTint(backgroundColor); + mDeviceListLayout.setBackgroundColor(backgroundColor); + } + + private void changeFooterColorForScroll() { + int totalItemCount = mLayoutManager.getItemCount(); + int lastVisibleItemPosition = + mLayoutManager.findLastCompletelyVisibleItemPosition(); + boolean hasBottomScroll = + totalItemCount > 0 && lastVisibleItemPosition != totalItemCount - 1; + mDialogFooter.getBackground().setTint( + hasBottomScroll + ? mMediaSwitchingController.getColorScheme().getSurfaceContainerHigh() + : mMediaSwitchingController.getColorScheme().getSurfaceContainer()); } public void handleLeBroadcastStarted() { diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorScheme.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorScheme.kt new file mode 100644 index 000000000000..21b92cc11406 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorScheme.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.dialog + +import android.content.Context +import com.android.systemui.monet.ColorScheme +import com.android.systemui.res.R + +abstract class MediaOutputColorScheme { + companion object Factory { + @JvmStatic + fun fromDynamicColors(dynamicScheme: ColorScheme): MediaOutputColorScheme { + return MediaOutputColorSchemeDynamic(dynamicScheme) + } + + @JvmStatic + fun fromSystemColors(context: Context): MediaOutputColorScheme { + return MediaOutputColorSchemeSystem(context) + } + } + + abstract fun getPrimary(): Int + + abstract fun getOnPrimary(): Int + + abstract fun getSecondary(): Int + + abstract fun getSecondaryContainer(): Int + + abstract fun getSurfaceContainer(): Int + + abstract fun getSurfaceContainerHigh(): Int + + abstract fun getOnSurface(): Int + + abstract fun getOnSurfaceVariant(): Int + + abstract fun getOutline(): Int + + abstract fun getOutlineVariant(): Int +} + +class MediaOutputColorSchemeDynamic(dynamicScheme: ColorScheme) : MediaOutputColorScheme() { + private val mMaterialScheme = dynamicScheme.materialScheme + + override fun getPrimary() = mMaterialScheme.primary + + override fun getOnPrimary() = mMaterialScheme.onPrimary + + override fun getSecondary() = mMaterialScheme.secondary + + override fun getSecondaryContainer() = mMaterialScheme.secondaryContainer + + override fun getSurfaceContainer() = mMaterialScheme.surfaceContainer + + override fun getSurfaceContainerHigh() = mMaterialScheme.surfaceContainerHigh + + override fun getOnSurface() = mMaterialScheme.onSurface + + override fun getOnSurfaceVariant() = mMaterialScheme.onSurfaceVariant + + override fun getOutline() = mMaterialScheme.outline + + override fun getOutlineVariant() = mMaterialScheme.outlineVariant +} + +class MediaOutputColorSchemeSystem(private val mContext: Context) : MediaOutputColorScheme() { + override fun getPrimary() = mContext.getColor(R.color.media_dialog_primary) + + override fun getOnPrimary() = mContext.getColor(R.color.media_dialog_on_primary) + + override fun getSecondary() = mContext.getColor(R.color.media_dialog_secondary) + + override fun getSecondaryContainer() = + mContext.getColor(R.color.media_dialog_secondary_container) + + override fun getSurfaceContainer() = mContext.getColor(R.color.media_dialog_surface_container) + + override fun getSurfaceContainerHigh() = + mContext.getColor(R.color.media_dialog_surface_container_high) + + override fun getOnSurface() = mContext.getColor(R.color.media_dialog_on_surface) + + override fun getOnSurfaceVariant() = mContext.getColor(R.color.media_dialog_on_surface_variant) + + override fun getOutline() = mContext.getColor(R.color.media_dialog_outline) + + override fun getOutlineVariant() = mContext.getColor(R.color.media_dialog_outline_variant) +} diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java index 163ff248b9df..225ad724ce71 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java @@ -17,6 +17,7 @@ package com.android.systemui.media.dialog; import static com.android.settingslib.flags.Flags.legacyLeAudioSharing; +import static com.android.media.flags.Flags.enableOutputSwitcherRedesign; import android.content.Context; import android.os.Bundle; @@ -57,8 +58,10 @@ public class MediaOutputDialog extends MediaOutputBaseDialog { super(context, broadcastSender, mediaSwitchingController, includePlaybackAndAppMetadata); mDialogTransitionAnimator = dialogTransitionAnimator; mUiEventLogger = uiEventLogger; - mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mainExecutor, - backgroundExecutor); + mAdapter = enableOutputSwitcherRedesign() + ? new MediaOutputAdapter(mMediaSwitchingController) + : new MediaOutputAdapterLegacy(mMediaSwitchingController, mainExecutor, + backgroundExecutor); if (!aboveStatusbar) { getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java index bf1f971c0f8c..0b4a9321618d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java @@ -171,7 +171,9 @@ public class MediaSwitchingController private FeatureFlags mFeatureFlags; private UserTracker mUserTracker; private VolumePanelGlobalStateInteractor mVolumePanelGlobalStateInteractor; + @NonNull private MediaOutputColorScheme mMediaOutputColorScheme; @NonNull private MediaOutputColorSchemeLegacy mMediaOutputColorSchemeLegacy; + private boolean mIsGroupListCollapsed = true; public enum BroadcastNotifyDialog { ACTION_FIRST_LAUNCH, @@ -229,6 +231,7 @@ public class MediaSwitchingController mOutputMediaItemListProxy = new OutputMediaItemListProxy(context); mDialogTransitionAnimator = dialogTransitionAnimator; mNearbyMediaDevicesManager = nearbyMediaDevicesManager; + mMediaOutputColorScheme = MediaOutputColorScheme.fromSystemColors(mContext); mMediaOutputColorSchemeLegacy = MediaOutputColorSchemeLegacy.fromSystemColors(mContext); if (enableInputRouting()) { @@ -499,7 +502,7 @@ public class MediaSwitchingController return getNotificationIcon(); } - IconCompat getDeviceIconCompat(MediaDevice device) { + Drawable getDeviceIconDrawable(MediaDevice device) { Drawable drawable = device.getIcon(); if (drawable == null) { if (DEBUG) { @@ -509,7 +512,19 @@ public class MediaSwitchingController // Use default Bluetooth device icon to handle getIcon() is null case. drawable = mContext.getDrawable(com.android.internal.R.drawable.ic_bt_headphones_a2dp); } - return BluetoothUtils.createIconWithDrawable(drawable); + return drawable; + } + + IconCompat getDeviceIconCompat(MediaDevice device) { + return BluetoothUtils.createIconWithDrawable(getDeviceIconDrawable(device)); + } + + public void setGroupListCollapsed(boolean isCollapsed) { + mIsGroupListCollapsed = isCollapsed; + } + + public boolean isGroupListCollapsed() { + return mIsGroupListCollapsed; } boolean isActiveItem(MediaDevice device) { @@ -560,10 +575,16 @@ public class MediaSwitchingController void updateCurrentColorScheme(WallpaperColors wallpaperColors, boolean isDarkTheme) { ColorScheme currentColorScheme = new ColorScheme(wallpaperColors, isDarkTheme); + mMediaOutputColorScheme = MediaOutputColorScheme.fromDynamicColors( + currentColorScheme); mMediaOutputColorSchemeLegacy = MediaOutputColorSchemeLegacy.fromDynamicColors( currentColorScheme, isDarkTheme); } + MediaOutputColorScheme getColorScheme() { + return mMediaOutputColorScheme; + } + MediaOutputColorSchemeLegacy getColorSchemeLegacy() { return mMediaOutputColorSchemeLegacy; } @@ -609,8 +630,7 @@ public class MediaSwitchingController devices, getSelectedMediaDevice(), connectedMediaDevice, - needToHandleMutingExpectedDevice, - getConnectNewDeviceItem()); + needToHandleMutingExpectedDevice); } else { List<MediaItem> updatedMediaItems = buildMediaItems( @@ -701,7 +721,6 @@ public class MediaSwitchingController } } dividerItems.forEach(finalMediaItems::add); - attachConnectNewDeviceItemIfNeeded(finalMediaItems); return finalMediaItems; } } @@ -765,7 +784,6 @@ public class MediaSwitchingController finalMediaItems.add(MediaItem.createDeviceMediaItem(device)); } } - attachConnectNewDeviceItemIfNeeded(finalMediaItems); return finalMediaItems; } @@ -789,8 +807,14 @@ public class MediaSwitchingController } } + @NonNull + MediaItem getConnectedSpeakersExpandableGroupDivider() { + return MediaItem.createExpandableGroupDividerMediaItem( + mContext.getString(R.string.media_output_group_title_connected_speakers)); + } + @Nullable - private MediaItem getConnectNewDeviceItem() { + MediaItem getConnectNewDeviceItem() { boolean isSelectedDeviceNotAGroup = getSelectedMediaDevice().size() == 1; if (enableInputRouting()) { // When input routing is enabled, there are expected to be at least 2 total selected @@ -879,6 +903,15 @@ public class MediaSwitchingController }); } + private List<MediaItem> getOutputDeviceList(boolean addConnectDeviceButton) { + List<MediaItem> mediaItems = new ArrayList<>( + mOutputMediaItemListProxy.getOutputMediaItemList()); + if (addConnectDeviceButton) { + attachConnectNewDeviceItemIfNeeded(mediaItems); + } + return mediaItems; + } + private void addInputDevices(List<MediaItem> mediaItems) { mediaItems.add( MediaItem.createGroupDividerMediaItem( @@ -886,22 +919,34 @@ public class MediaSwitchingController mediaItems.addAll(mInputMediaItemList); } - private void addOutputDevices(List<MediaItem> mediaItems) { + private void addOutputDevices(List<MediaItem> mediaItems, boolean addConnectDeviceButton) { mediaItems.add( MediaItem.createGroupDividerMediaItem( mContext.getString(R.string.media_output_group_title))); - mediaItems.addAll(mOutputMediaItemListProxy.getOutputMediaItemList()); + mediaItems.addAll(getOutputDeviceList(addConnectDeviceButton)); } + /** + * Returns a list of media items to be rendered in the device list. For backward compatibility + * reasons, adds a "Connect a device" button by default. + */ public List<MediaItem> getMediaItemList() { + return getMediaItemList(true /* addConnectDeviceButton */); + } + + /** + * Returns a list of media items to be rendered in the device list. + * @param addConnectDeviceButton Whether to add a "Connect a device" button to the list. + */ + public List<MediaItem> getMediaItemList(boolean addConnectDeviceButton) { // If input routing is not enabled, only return output media items. if (!enableInputRouting()) { - return mOutputMediaItemListProxy.getOutputMediaItemList(); + return getOutputDeviceList(addConnectDeviceButton); } // If input routing is enabled, return both output and input media items. List<MediaItem> mediaItems = new ArrayList<>(); - addOutputDevices(mediaItems); + addOutputDevices(mediaItems, addConnectDeviceButton); addInputDevices(mediaItems); return mediaItems; } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java b/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java index 45ca2c6ee8e5..c15ef82f0378 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java @@ -44,7 +44,6 @@ public class OutputMediaItemListProxy { private final List<MediaItem> mSelectedMediaItems; private final List<MediaItem> mSuggestedMediaItems; private final List<MediaItem> mSpeakersAndDisplaysMediaItems; - @Nullable private MediaItem mConnectNewDeviceMediaItem; public OutputMediaItemListProxy(Context context) { mContext = context; @@ -88,9 +87,6 @@ public class OutputMediaItemListProxy { R.string.media_output_group_title_speakers_and_displays))); finalMediaItems.addAll(mSpeakersAndDisplaysMediaItems); } - if (mConnectNewDeviceMediaItem != null) { - finalMediaItems.add(mConnectNewDeviceMediaItem); - } return finalMediaItems; } @@ -99,8 +95,7 @@ public class OutputMediaItemListProxy { List<MediaDevice> devices, List<MediaDevice> selectedDevices, @Nullable MediaDevice connectedMediaDevice, - boolean needToHandleMutingExpectedDevice, - @Nullable MediaItem connectNewDeviceMediaItem) { + boolean needToHandleMutingExpectedDevice) { Set<String> selectedOrConnectedMediaDeviceIds = selectedDevices.stream().map(MediaDevice::getId).collect(Collectors.toSet()); if (connectedMediaDevice != null) { @@ -177,7 +172,6 @@ public class OutputMediaItemListProxy { mSuggestedMediaItems.addAll(updatedSuggestedMediaItems); mSpeakersAndDisplaysMediaItems.clear(); mSpeakersAndDisplaysMediaItems.addAll(updatedSpeakersAndDisplaysMediaItems); - mConnectNewDeviceMediaItem = connectNewDeviceMediaItem; // The cached mOutputMediaItemList is cleared upon any update to individual media item // lists. This ensures getOutputMediaItemList() computes and caches a fresh list on the next @@ -197,10 +191,6 @@ public class OutputMediaItemListProxy { mSelectedMediaItems.removeIf((MediaItem::isMutingExpectedDevice)); mSuggestedMediaItems.removeIf((MediaItem::isMutingExpectedDevice)); mSpeakersAndDisplaysMediaItems.removeIf((MediaItem::isMutingExpectedDevice)); - if (mConnectNewDeviceMediaItem != null - && mConnectNewDeviceMediaItem.isMutingExpectedDevice()) { - mConnectNewDeviceMediaItem = null; - } } mOutputMediaItemList.removeIf((MediaItem::isMutingExpectedDevice)); } @@ -211,7 +201,6 @@ public class OutputMediaItemListProxy { mSelectedMediaItems.clear(); mSuggestedMediaItems.clear(); mSpeakersAndDisplaysMediaItems.clear(); - mConnectNewDeviceMediaItem = null; } mOutputMediaItemList.clear(); } @@ -221,8 +210,7 @@ public class OutputMediaItemListProxy { if (Flags.fixOutputMediaItemListIndexOutOfBoundsException()) { return mSelectedMediaItems.isEmpty() && mSuggestedMediaItems.isEmpty() - && mSpeakersAndDisplaysMediaItems.isEmpty() - && (mConnectNewDeviceMediaItem == null); + && mSpeakersAndDisplaysMediaItems.isEmpty(); } else { return mOutputMediaItemList.isEmpty(); } diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt index d6d185195c51..063ff15054e8 100644 --- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt @@ -267,7 +267,11 @@ private fun CardCarouselContent( } if (behavior.isCarouselDismissible) { - SwipeToDismiss(content = { PagerContent() }, onDismissed = onDismissed) + SwipeToDismiss( + content = { PagerContent() }, + isSwipingEnabled = isSwipingEnabled, + onDismissed = onDismissed, + ) } else { val overscrollEffect = rememberOffsetOverscrollEffect() SwipeToReveal( diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToDismiss.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToDismiss.kt index b80bf4143252..f044257bb343 100644 --- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToDismiss.kt +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToDismiss.kt @@ -17,99 +17,187 @@ package com.android.systemui.media.remedia.ui.compose import androidx.compose.animation.core.Animatable -import androidx.compose.foundation.OverscrollEffect +import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.absoluteOffset +import androidx.compose.foundation.overscroll import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.input.pointer.PointerType +import androidx.compose.ui.layout.layout import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.util.fastCoerceIn import androidx.compose.ui.util.fastRoundToInt +import com.android.compose.gesture.NestedDraggable +import com.android.compose.gesture.effect.rememberOffsetOverscrollEffect +import com.android.compose.gesture.nestedDraggable +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch /** Swipe to dismiss that supports nested scrolling. */ @Composable fun SwipeToDismiss( - content: @Composable (overscrollEffect: OverscrollEffect?) -> Unit, + content: @Composable () -> Unit, + isSwipingEnabled: Boolean, onDismissed: () -> Unit, modifier: Modifier = Modifier, ) { - val scope = rememberCoroutineScope() - val offsetAnimatable = remember { Animatable(0f) } + val overscrollEffect = rememberOffsetOverscrollEffect() - // This is the width of the revealed content UI box. It's not a state because it's not - // observed in any composition and is an object with a value to avoid the extra cost - // associated with boxing and unboxing an int. - val revealedContentBoxWidth = remember { + // This is the width of the content UI box. It's not a state because it's not observed in any + // composition and is an object with a value to avoid the extra cost associated with boxing and + // unboxing an int. + val contentBoxWidth = remember { object { var value = 0 } } - val nestedScrollConnection = remember { - object : NestedScrollConnection { - override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { - return if (offsetAnimatable.value > 0f && available.x < 0f) { - scope.launch { offsetAnimatable.snapTo(offsetAnimatable.value + available.x) } - Offset(available.x, 0f) - } else if (offsetAnimatable.value < 0f && available.x > 0f) { - scope.launch { offsetAnimatable.snapTo(offsetAnimatable.value + available.x) } - Offset(available.x, 0f) - } else { - Offset.Zero - } - } - - override fun onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource, - ): Offset { - return if (available.x > 0f) { - scope.launch { offsetAnimatable.snapTo(offsetAnimatable.value + available.x) } - Offset(available.x, 0f) - } else if (available.x < 0f) { - scope.launch { offsetAnimatable.snapTo(offsetAnimatable.value + available.x) } - Offset(available.x, 0f) - } else { - Offset.Zero - } - } + // In order to support the drag to dismiss, infrastructure has to be put in place where a + // NestedDraggable helps by consuming the unconsumed drags and flings and applying the offset. + // + // This is the NestedDraggalbe controller. + val dragController = + rememberDismissibleContentDragController( + maxBound = { contentBoxWidth.value.toFloat() }, + onDismissed = onDismissed, + ) - override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { - scope.launch { - offsetAnimatable.animateTo( - if (offsetAnimatable.value >= revealedContentBoxWidth.value / 2f) { - revealedContentBoxWidth.value * 2f - } else if (offsetAnimatable.value <= -revealedContentBoxWidth.value / 2f) { - -revealedContentBoxWidth.value * 2f - } else { - 0f - } - ) - if (offsetAnimatable.value != 0f) { - onDismissed() + Box( + modifier = + modifier + .layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + contentBoxWidth.value = placeable.measuredWidth + layout(placeable.measuredWidth, placeable.measuredHeight) { + placeable.place(0, 0) } } - return super.onPostFling(consumed, available) - } + .nestedDraggable( + enabled = isSwipingEnabled, + draggable = + remember { + object : NestedDraggable { + override fun onDragStarted( + position: Offset, + sign: Float, + pointersDown: Int, + pointerType: PointerType?, + ): NestedDraggable.Controller { + return dragController + } + + override fun shouldConsumeNestedPostScroll(sign: Float): Boolean { + return dragController.shouldConsumePostScrolls(sign) + } + + override fun shouldConsumeNestedPreScroll(sign: Float): Boolean { + return dragController.shouldConsumePreScrolls(sign) + } + } + }, + orientation = Orientation.Horizontal, + ) + .overscroll(overscrollEffect) + .absoluteOffset { IntOffset(dragController.offset.fastRoundToInt(), y = 0) } + ) { + content() + } +} + +@Composable +private fun rememberDismissibleContentDragController( + maxBound: () -> Float, + onDismissed: () -> Unit, +): DismissibleContentDragController { + val scope = rememberCoroutineScope() + return remember { + DismissibleContentDragController( + scope = scope, + maxBound = maxBound, + onDismissed = onDismissed, + ) + } +} + +private class DismissibleContentDragController( + private val scope: CoroutineScope, + private val maxBound: () -> Float, + private val onDismissed: () -> Unit, +) : NestedDraggable.Controller { + private val offsetAnimatable = Animatable(0f) + private var lastTarget = 0f + private var range = 0f..1f + private var shouldConsumePreScrolls by mutableStateOf(false) + + override val autoStopNestedDrags: Boolean + get() = true + + val offset: Float + get() = offsetAnimatable.value + + fun shouldConsumePreScrolls(sign: Float): Boolean { + if (!shouldConsumePreScrolls) return false + + if (lastTarget > 0f && sign < 0f) { + range = 0f..maxBound() + return true } + + if (lastTarget < 0f && sign > 0f) { + range = -maxBound()..0f + return true + } + + return false } - Box( - modifier = - modifier - .onSizeChanged { revealedContentBoxWidth.value = it.width } - .nestedScroll(nestedScrollConnection) - .offset { IntOffset(x = offsetAnimatable.value.fastRoundToInt(), y = 0) } - ) { - content(null) + fun shouldConsumePostScrolls(sign: Float): Boolean { + val max = maxBound() + if (sign > 0f && lastTarget < max) { + range = 0f..maxBound() + return true + } + + if (sign < 0f && lastTarget > -max) { + range = -maxBound()..0f + return true + } + + return false + } + + override fun onDrag(delta: Float): Float { + val previousTarget = lastTarget + lastTarget = (lastTarget + delta).fastCoerceIn(range.start, range.endInclusive) + val newTarget = lastTarget + scope.launch { offsetAnimatable.snapTo(newTarget) } + return lastTarget - previousTarget + } + + override suspend fun onDragStopped(velocity: Float, awaitFling: suspend () -> Unit): Float { + val rangeMiddle = range.start + (range.endInclusive - range.start) / 2f + lastTarget = + when { + lastTarget >= rangeMiddle -> range.endInclusive + else -> range.start + } + + shouldConsumePreScrolls = lastTarget != 0f + val newTarget = lastTarget + + scope.launch { + offsetAnimatable.animateTo(newTarget) + if (newTarget != 0f) { + onDismissed() + } + } + return velocity } } diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToReveal.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToReveal.kt index 770762c7a29f..1d7b79d9a07a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToReveal.kt +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToReveal.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.absoluteOffset -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.overscroll import androidx.compose.foundation.withoutVisualEffect import androidx.compose.runtime.Composable @@ -82,7 +81,7 @@ fun SwipeToReveal( // overscroll visual effect. // // This is the NestedDraggalbe controller. - val revealedContentDragController = rememberRevealedContentDragController { + val revealedContentDragController = rememberDismissibleContentDragController { revealedContentBoxWidth.value.toFloat() } @@ -186,7 +185,7 @@ fun SwipeToReveal( } @Composable -private fun rememberRevealedContentDragController( +private fun rememberDismissibleContentDragController( maxBound: () -> Float ): RevealedContentDragController { val scope = rememberCoroutineScope() diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionUtils.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionUtils.kt index 88cbc3867744..a8d0e0573d89 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionUtils.kt @@ -18,6 +18,7 @@ package com.android.systemui.mediaprojection.permission import android.content.Context import android.media.projection.MediaProjectionConfig +import com.android.media.projection.flags.Flags import com.android.systemui.res.R /** Various utility methods related to media projection permissions. */ @@ -28,13 +29,27 @@ object MediaProjectionPermissionUtils { mediaProjectionConfig: MediaProjectionConfig?, overrideDisableSingleAppOption: Boolean, ): String? { - // The single app option should only be disabled if the client has setup a - // MediaProjection with MediaProjectionConfig#createConfigForDefaultDisplay AND - // it hasn't been overridden by the OVERRIDE_DISABLE_SINGLE_APP_OPTION per-app override. + val singleAppOptionDisabled = !overrideDisableSingleAppOption && - mediaProjectionConfig?.regionToCapture == - MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY + if (Flags.appContentSharing()) { + // The single app option should only be disabled if the client has setup a + // MediaProjection with MediaProjection.isChoiceAppEnabled == false (e.g by + // creating it + // with MediaProjectionConfig#createConfigForDefaultDisplay AND + // it hasn't been overridden by the OVERRIDE_DISABLE_SINGLE_APP_OPTION per-app + // override. + mediaProjectionConfig?.isSourceEnabled( + MediaProjectionConfig.PROJECTION_SOURCE_APP + ) == false + } else { + // The single app option should only be disabled if the client has setup a + // MediaProjection with MediaProjectionConfig#createConfigForDefaultDisplay AND + // it hasn't been overridden by the OVERRIDE_DISABLE_SINGLE_APP_OPTION per-app + // override. + mediaProjectionConfig?.regionToCapture == + MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY + } return if (singleAppOptionDisabled) { context.getString( R.string.media_projection_entry_app_permission_dialog_single_app_disabled, diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java index efed260b4c99..3f1401cd08b9 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java @@ -239,6 +239,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, @Override public void onDisplayAddSystemDecorations(int displayId) { CommandQueue.Callbacks.super.onDisplayAddSystemDecorations(displayId); + mEdgeBackGestureHandler.onDisplayAddSystemDecorations(displayId); if (mLauncherProxyService.getProxy() == null) { return; } @@ -253,6 +254,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, @Override public void onDisplayRemoved(int displayId) { CommandQueue.Callbacks.super.onDisplayRemoved(displayId); + mEdgeBackGestureHandler.onDisplayRemoveSystemDecorations(displayId); if (mLauncherProxyService.getProxy() == null) { return; } @@ -267,6 +269,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, @Override public void onDisplayRemoveSystemDecorations(int displayId) { CommandQueue.Callbacks.super.onDisplayRemoveSystemDecorations(displayId); + mEdgeBackGestureHandler.onDisplayRemoveSystemDecorations(displayId); if (mLauncherProxyService.getProxy() == null) { return; } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index 6cda192c4198..b74135a39ee6 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -22,6 +22,7 @@ import static android.view.MotionEvent.TOOL_TYPE_FINGER; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION; import static com.android.systemui.Flags.edgebackGestureHandlerGetRunningTasksBackground; +import static com.android.window.flags.Flags.enableMultidisplayTrackpadBackGesture; import static com.android.systemui.Flags.predictiveBackDelayWmTransition; import static com.android.systemui.classifier.Classifier.BACK_GESTURE; import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadThreeFingerSwipe; @@ -115,6 +116,8 @@ import kotlinx.coroutines.Job; import java.io.PrintWriter; import java.util.ArrayDeque; import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.Optional; @@ -279,8 +282,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private boolean mIsTrackpadThreeFingerSwipe; private boolean mIsButtonForcedVisible; - private InputMonitorCompat mInputMonitor; - private InputChannelCompat.InputEventReceiver mInputEventReceiver; + private final Map<Integer, InputMonitorResource> mInputMonitorResources = new HashMap<>(); private NavigationEdgeBackPlugin mEdgeBackPlugin; private BackAnimation mBackAnimation; @@ -665,14 +667,44 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mIsNavBarShownTransiently = isTransient; } - private void disposeInputChannel() { - if (mInputEventReceiver != null) { - mInputEventReceiver.dispose(); - mInputEventReceiver = null; + /** + * Called when a new display gets connected + * + * @param displayId The id associated with the connected display. + */ + public void onDisplayAddSystemDecorations(int displayId) { + if (enableMultidisplayTrackpadBackGesture() && mIsEnabled) { + mUiThreadContext.runWithScissors(() -> { + removeAndDisposeInputMonitorResource(displayId); + mInputMonitorResources.put(displayId, new InputMonitorResource(displayId)); + }); } - if (mInputMonitor != null) { - mInputMonitor.dispose(); - mInputMonitor = null; + } + + /** + * Called when a display gets disconnected + * + * @param displayId The id associated with the disconnected display. + */ + public void onDisplayRemoveSystemDecorations(int displayId) { + if (enableMultidisplayTrackpadBackGesture()) { + mUiThreadContext.runWithScissors(() -> removeAndDisposeInputMonitorResource(displayId)); + } + } + + private void removeAndDisposeInputMonitorResource(int displayId) { + InputMonitorResource inputMonitor = mInputMonitorResources.remove(displayId); + if (inputMonitor != null) { + inputMonitor.dispose(); + } + } + + private void disposeInputChannels() { + Iterator<Map.Entry<Integer, InputMonitorResource>> iterator = + mInputMonitorResources.entrySet().iterator(); + while (iterator.hasNext()) { + iterator.next().getValue().dispose(); + iterator.remove(); } } @@ -691,7 +723,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack return; } mIsEnabled = isEnabled; - disposeInputChannel(); + disposeInputChannels(); if (mEdgeBackPlugin != null) { mEdgeBackPlugin.onDestroy(); @@ -746,9 +778,8 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } // Register input event receiver - mInputMonitor = new InputMonitorCompat("edge-swipe", mDisplayId); - mInputEventReceiver = mInputMonitor.getInputReceiver(mUiThreadContext.getLooper(), - mUiThreadContext.getChoreographer(), this::onInputEvent); + mInputMonitorResources.put(mDisplayId, new InputMonitorResource(mDisplayId)); + //TODO(b/382774299): Register input monitor on connected displays (if any) // Add a nav bar panel window resetEdgeBackPlugin(); @@ -950,9 +981,10 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack return true; } - private boolean isValidTrackpadBackGesture(boolean isTrackpadEvent) { - if (!isTrackpadEvent) { - return false; + private boolean isValidTrackpadBackGesture(int displayId) { + if (enableMultidisplayTrackpadBackGesture() && displayId != mDisplayId) { + //TODO(b/382774299): Handle exclude regions on connected displays + return true; } // for trackpad gestures, unless the whole screen is excluded region, 3-finger swipe // gestures are allowed even if the cursor is in the excluded region. @@ -969,14 +1001,15 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private boolean isWithinTouchRegion(MotionEvent ev) { // If the point is inside the PiP or Nav bar overlay excluded bounds, then ignore the back - // gesture + // gesture. Also ignore (for now) if it's not on the main display. + // TODO(b/382130680): Implement back gesture handling on connected displays int x = (int) ev.getX(); int y = (int) ev.getY(); final boolean isInsidePip = mIsInPip && mPipExcludedBounds.contains(x, y); final boolean isInDesktopExcludeRegion = desktopExcludeRegionContains(x, y) && isEdgeResizePermitted(ev); if (isInsidePip || isInDesktopExcludeRegion - || mNavBarOverlayExcludedBounds.contains(x, y)) { + || mNavBarOverlayExcludedBounds.contains(x, y) || ev.getDisplayId() != mDisplayId) { return false; } @@ -1076,7 +1109,11 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack // Verify if this is in within the touch region and we aren't in immersive mode, and // either the bouncer is showing or the notification panel is hidden - mInputEventReceiver.setBatchingEnabled(false); + InputMonitorResource inputMonitorResource = + mInputMonitorResources.get(ev.getDisplayId()); + if (inputMonitorResource != null) { + inputMonitorResource.mInputEventReceiver.setBatchingEnabled(false); + } if (mIsTrackpadThreeFingerSwipe) { // Since trackpad gestures don't have zones, this will be determined later by the // direction of the gesture. {@code mIsOnLeftEdge} is set to false to begin with. @@ -1099,7 +1136,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack boolean trackpadGesturesEnabled = (mSysUiFlags & SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED) == 0; mAllowGesture = isBackAllowedCommon && trackpadGesturesEnabled - && isValidTrackpadBackGesture(true /* isTrackpadEvent */); + && isValidTrackpadBackGesture(ev.getDisplayId()); } else { mAllowGesture = isBackAllowedCommon && !mUsingThreeButtonNav && isWithinInsets && isWithinTouchRegion(ev) && !isButtonPressFromTrackpad(ev); @@ -1210,12 +1247,14 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } private void pilferPointers() { - if (mInputMonitor != null) { + //TODO(b/382774299): Pilfer pointers on the correct display + InputMonitorResource inputMonitorResource = mInputMonitorResources.get(mDisplayId); + if (inputMonitorResource != null) { // Capture inputs - mInputMonitor.pilferPointers(); + inputMonitorResource.mInputMonitorCompat.pilferPointers(); // Notify FalsingManager that an intentional gesture has occurred. mFalsingManager.isFalseTouch(BACK_GESTURE); - mInputEventReceiver.setBatchingEnabled(true); + inputMonitorResource.mInputEventReceiver.setBatchingEnabled(true); } } @@ -1344,6 +1383,11 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack if (mEdgeBackPlugin != null) { mEdgeBackPlugin.dump(pw); } + pw.println(" mInputMonitorResources=" + mInputMonitorResources); + for (Map.Entry<Integer, InputMonitorResource> inputMonitorResource : + mInputMonitorResources.entrySet()) { + inputMonitorResource.getValue().dump("\t", pw); + } } private void updateTopActivityPackageName() { @@ -1376,6 +1420,33 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } } + private class InputMonitorResource { + private final int mDisplayId; + private final InputMonitorCompat mInputMonitorCompat; + private final InputChannelCompat.InputEventReceiver mInputEventReceiver; + + private InputMonitorResource(int displayId) { + this.mDisplayId = displayId; + mInputMonitorCompat = new InputMonitorCompat("edge-swipe", displayId); + mInputEventReceiver = mInputMonitorCompat.getInputReceiver(mUiThreadContext.getLooper(), + mUiThreadContext.getChoreographer(), EdgeBackGestureHandler.this::onInputEvent); + } + + public void dispose() { + mInputEventReceiver.dispose(); + mInputMonitorCompat.dispose(); + } + + public void dump(String prefix, PrintWriter writer) { + writer.println(prefix + this); + } + + @Override + public String toString() { + return "InputMonitorResource (displayId=" + mDisplayId + ")"; + } + } + private static class LogArray extends ArrayDeque<String> { private final int mLength; diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java index ad0acbdaf702..8bc3203ea51e 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java @@ -502,10 +502,6 @@ public class NavigationBar extends ViewController<NavigationBarView> implements boolean mHasBlurs; @Override - public void onWallpaperZoomOutChanged(float zoomOut) { - } - - @Override public void onBlurRadiusChanged(int radius) { boolean hasBlurs = radius != 0; if (hasBlurs == mHasBlurs) { diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt index 18b4b7d2b5cf..2a7fb5467173 100644 --- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt @@ -25,14 +25,12 @@ import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import com.android.systemui.statusbar.disableflags.domain.interactor.DisableFlagsInteractor -import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flowOf @@ -52,28 +50,10 @@ constructor( private val shadeInteractor: ShadeInteractor, disableFlagsInteractor: DisableFlagsInteractor, mediaCarouselInteractor: MediaCarouselInteractor, - activeNotificationsInteractor: ActiveNotificationsInteractor, ) : ExclusiveActivatable() { private val hydrator = Hydrator("NotificationsShadeOverlayContentViewModel.hydrator") - val showClock: Boolean by - hydrator.hydratedStateOf( - traceName = "showClock", - initialValue = - shouldShowClock( - isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value, - areAnyNotificationsPresent = - activeNotificationsInteractor.areAnyNotificationsPresentValue, - ), - source = - combine( - shadeInteractor.isShadeLayoutWide, - activeNotificationsInteractor.areAnyNotificationsPresent, - this::shouldShowClock, - ), - ) - val showMedia: Boolean by hydrator.hydratedStateOf( traceName = "showMedia", @@ -112,13 +92,6 @@ constructor( shadeInteractor.collapseNotificationsShade(loggingReason = "shade scrim clicked") } - private fun shouldShowClock( - isShadeLayoutWide: Boolean, - areAnyNotificationsPresent: Boolean, - ): Boolean { - return !isShadeLayoutWide && areAnyNotificationsPresent - } - @AssistedFactory interface Factory { fun create(): NotificationsShadeOverlayContentViewModel diff --git a/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt b/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt index faa77e51ec24..e38a0a78fbfd 100644 --- a/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt @@ -17,14 +17,16 @@ package com.android.systemui.power.data.repository +import android.annotation.SuppressLint import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.PowerManager +import com.android.keyguard.UserActivityNotifier +import com.android.systemui.Flags import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.power.shared.model.DozeScreenStateModel @@ -33,6 +35,7 @@ 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.util.time.SystemClock +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -102,6 +105,7 @@ constructor( @Application private val applicationContext: Context, private val systemClock: SystemClock, dispatcher: BroadcastDispatcher, + private val userActivityNotifier: UserActivityNotifier, ) : PowerRepository { override val dozeScreenState = MutableStateFlow(DozeScreenStateModel.UNKNOWN) @@ -163,12 +167,22 @@ constructor( ) } + @SuppressLint("MissingPermission") override fun userTouch(noChangeLights: Boolean) { - manager.userActivity( - systemClock.uptimeMillis(), - PowerManager.USER_ACTIVITY_EVENT_TOUCH, - if (noChangeLights) PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS else 0, - ) + val pmFlags = if (noChangeLights) PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS else 0 + if (Flags.bouncerUiRevamp()) { + userActivityNotifier.notifyUserActivity( + timeOfActivity = systemClock.uptimeMillis(), + event = PowerManager.USER_ACTIVITY_EVENT_TOUCH, + flags = pmFlags, + ) + } else { + manager.userActivity( + systemClock.uptimeMillis(), + PowerManager.USER_ACTIVITY_EVENT_TOUCH, + pmFlags, + ) + } } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt index 699778f3b6f9..bd7e7832751a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt @@ -162,6 +162,7 @@ fun LargeTileContent( colors = colors, accessibilityUiState = accessibilityUiState, isVisible = isVisible, + modifier = Modifier.weight(1f), ) if (sideDrawable != null) { @@ -289,6 +290,8 @@ private fun TileLabel( ) { var textSize by remember { mutableIntStateOf(0) } + val iterations = if (isVisible()) TILE_MARQUEE_ITERATIONS else 0 + BasicText( text = text, color = color, @@ -321,14 +324,10 @@ private fun TileLabel( ) } } - .thenIf(isVisible()) { - // Only apply the marquee when the label is visible, which is needed for the - // always composed QS - Modifier.basicMarquee( - iterations = TILE_MARQUEE_ITERATIONS, - initialDelayMillis = TILE_INITIAL_DELAY_MILLIS, - ) - }, + .basicMarquee( + iterations = iterations, + initialDelayMillis = TILE_INITIAL_DELAY_MILLIS, + ), ) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt index f8eaa6c3bcfb..b8cb2c4844e4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt @@ -22,8 +22,11 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.VisibilityThreshold import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -108,6 +111,7 @@ import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.CustomAccessibilityAction import androidx.compose.ui.semantics.contentDescription @@ -118,6 +122,7 @@ import androidx.compose.ui.text.style.Hyphens import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastMap import com.android.compose.gesture.effect.rememberOffsetOverscrollEffectFactory @@ -157,9 +162,9 @@ import com.android.systemui.qs.panels.ui.model.AvailableTileGridCell import com.android.systemui.qs.panels.ui.model.GridCell import com.android.systemui.qs.panels.ui.model.SpacerGridCell import com.android.systemui.qs.panels.ui.model.TileGridCell -import com.android.systemui.qs.panels.ui.viewmodel.BounceableTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.shared.model.TileCategory import com.android.systemui.qs.shared.model.groupAndSort import com.android.systemui.res.R import kotlin.math.abs @@ -220,7 +225,6 @@ private fun EditModeTopBar(onStopEditing: () -> Unit, onReset: (() -> Unit)?) { ) } -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun DefaultEditTileGrid( listState: EditTileListState, @@ -526,11 +530,7 @@ private fun CurrentTilesGrid( var gridContentOffset by remember { mutableStateOf(Offset(0f, 0f)) } val coroutineScope = rememberCoroutineScope() - val cells = - remember(listState.tiles) { - listState.tiles.fastMap { Pair(it, BounceableTileViewModel()) } - } - + val cells = listState.tiles val primaryColor = MaterialTheme.colorScheme.primary TileLazyGrid( state = gridState, @@ -561,11 +561,11 @@ private fun CurrentTilesGrid( .testTag(CURRENT_TILES_GRID_TEST_TAG), ) { EditTiles( - cells, - listState, - selectionState, - coroutineScope, - largeTilesSpan, + cells = cells, + dragAndDropState = listState, + selectionState = selectionState, + coroutineScope = coroutineScope, + largeTilesSpan = largeTilesSpan, onRemoveTile = onRemoveTile, ) { resizingOperation -> when (resizingOperation) { @@ -618,11 +618,9 @@ private fun AvailableTileGrid( } .padding(16.dp), ) { - Text( - text = category.label.load() ?: "", - style = MaterialTheme.typography.titleMediumEmphasized, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.fillMaxWidth().padding(start = 8.dp, bottom = 16.dp), + CategoryHeader( + category, + modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), ) tiles.chunked(columns).forEach { row -> Row( @@ -662,7 +660,7 @@ private fun GridCell.key(index: Int): Any { /** * Adds a list of [GridCell] to the lazy grid * - * @param cells the pairs of [GridCell] to [BounceableTileViewModel] + * @param cells the list of [GridCell] * @param dragAndDropState the [DragAndDropState] for this grid * @param selectionState the [MutableSelectionState] for this grid * @param coroutineScope the [CoroutineScope] to be used for the tiles @@ -671,7 +669,7 @@ private fun GridCell.key(index: Int): Any { * @param onResize the callback when a tile has a new [ResizeOperation] */ fun LazyGridScope.EditTiles( - cells: List<Pair<GridCell, BounceableTileViewModel>>, + cells: List<GridCell>, dragAndDropState: DragAndDropState, selectionState: MutableSelectionState, coroutineScope: CoroutineScope, @@ -681,11 +679,11 @@ fun LazyGridScope.EditTiles( ) { items( count = cells.size, - key = { cells[it].first.key(it) }, - span = { cells[it].first.span }, + key = { cells[it].key(it) }, + span = { cells[it].span }, contentType = { TileType }, ) { index -> - when (val cell = cells[index].first) { + when (val cell = cells[index]) { is TileGridCell -> if (dragAndDropState.isMoving(cell.tile.tileSpec)) { // If the tile is being moved, replace it with a visible spacer @@ -708,7 +706,15 @@ fun LazyGridScope.EditTiles( onRemoveTile = onRemoveTile, coroutineScope = coroutineScope, largeTilesSpan = largeTilesSpan, - modifier = Modifier.animateItem(), + modifier = + Modifier.animateItem( + placementSpec = + spring( + stiffness = Spring.StiffnessMediumLow, + dampingRatio = Spring.DampingRatioLowBouncy, + visibilityThreshold = IntOffset.VisibilityThreshold, + ) + ), ) } is SpacerGridCell -> @@ -853,6 +859,26 @@ private fun TileGridCell( @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable +private fun CategoryHeader(category: TileCategory, modifier: Modifier = Modifier) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = spacedBy(8.dp), + modifier = modifier, + ) { + Icon( + painter = painterResource(category.iconId), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = category.label.load() ?: "", + style = MaterialTheme.typography.titleMediumEmphasized, + color = MaterialTheme.colorScheme.onSurface, + ) + } +} + +@Composable private fun AvailableTileGridCell( cell: AvailableTileGridCell, dragAndDropState: DragAndDropState, diff --git a/packages/SystemUI/src/com/android/systemui/qs/shared/model/TileCategory.kt b/packages/SystemUI/src/com/android/systemui/qs/shared/model/TileCategory.kt index 59cb7d3d5345..c8225e7a3509 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/shared/model/TileCategory.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/shared/model/TileCategory.kt @@ -20,14 +20,35 @@ import com.android.systemui.common.shared.model.Text import com.android.systemui.res.R /** Categories for tiles. This can be used to sort tiles in edit mode. */ -enum class TileCategory(val label: Text) { - CONNECTIVITY(Text.Resource(R.string.qs_edit_mode_category_connectivity)), - UTILITIES(Text.Resource(R.string.qs_edit_mode_category_utilities)), - DISPLAY(Text.Resource(R.string.qs_edit_mode_category_display)), - PRIVACY(Text.Resource(R.string.qs_edit_mode_category_privacy)), - ACCESSIBILITY(Text.Resource(R.string.qs_edit_mode_category_accessibility)), - PROVIDED_BY_APP(Text.Resource(R.string.qs_edit_mode_category_providedByApps)), - UNKNOWN(Text.Resource(R.string.qs_edit_mode_category_unknown)), +enum class TileCategory(val label: Text, val iconId: Int) { + CONNECTIVITY( + Text.Resource(R.string.qs_edit_mode_category_connectivity), + R.drawable.ic_qs_category_connectivty, + ), + UTILITIES( + Text.Resource(R.string.qs_edit_mode_category_utilities), + R.drawable.ic_qs_category_utilities, + ), + DISPLAY( + Text.Resource(R.string.qs_edit_mode_category_display), + R.drawable.ic_qs_category_display, + ), + PRIVACY( + Text.Resource(R.string.qs_edit_mode_category_privacy), + R.drawable.ic_qs_category_privacy, + ), + ACCESSIBILITY( + Text.Resource(R.string.qs_edit_mode_category_accessibility), + R.drawable.ic_qs_category_accessibility, + ), + PROVIDED_BY_APP( + Text.Resource(R.string.qs_edit_mode_category_providedByApps), + R.drawable.ic_qs_category_provided_by_apps, + ), + UNKNOWN( + Text.Resource(R.string.qs_edit_mode_category_unknown), + R.drawable.ic_qs_category_unknown, + ), } interface CategoryAndName { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetAdapter.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetAdapter.java index b21c3e4e44e1..6236fff87f63 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetAdapter.java @@ -196,11 +196,16 @@ public class InternetAdapter extends RecyclerView.Adapter<InternetAdapter.Intern if (mJob == null) { mJob = WifiUtils.checkWepAllowed(mContext, mCoroutineScope, wifiEntry.getSsid(), WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG, intent -> { - mInternetDetailsContentController.startActivityForDialog(intent); + mInternetDetailsContentController + .startActivityForDialog(intent); return null; }, () -> { wifiConnect(wifiEntry, view); return null; + }, intent -> { + mInternetDetailsContentController + .startActivityForDialogDismissDialogFirst(intent, view); + return null; }); } return; diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java index 945e051606b9..2497daebdd6d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java @@ -784,6 +784,17 @@ public class InternetDetailsContentController implements AccessPointController.A mActivityStarter.startActivity(intent, false /* dismissShade */); } + // Closes the dialog first, as the WEP dialog is in a different process and can have weird + // interactions otherwise. + void startActivityForDialogDismissDialogFirst(Intent intent, View view) { + ActivityTransitionAnimator.Controller controller = + mDialogTransitionAnimator.createActivityTransitionController(view); + if (mCallback != null) { + mCallback.dismissDialog(); + } + mActivityStarter.startActivity(intent, false /* dismissShade */, controller); + } + void launchNetworkSetting(View view) { startActivity(getSettingsIntent(), view); } 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 06fc8610c97b..daaa2db54775 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 @@ -332,6 +332,7 @@ constructor( /** Switches between scenes based on ever-changing application state. */ private fun automaticallySwitchScenes() { handleBouncerImeVisibility() + handleBouncerHiding() handleSimUnlock() handleDeviceUnlockStatus() handlePowerState() @@ -352,6 +353,24 @@ constructor( } } + private fun handleBouncerHiding() { + applicationScope.launch { + repeatWhen( + condition = + authenticationInteractor + .get() + .authenticationMethod + .map { !it.isSecure } + .distinctUntilChanged() + ) { + sceneInteractor.hideOverlay( + overlay = Overlays.Bouncer, + loggingReason = "Authentication method changed to a non-secure one.", + ) + } + } + } + private fun handleSimUnlock() { applicationScope.launch { simBouncerInteractor @@ -434,6 +453,12 @@ constructor( } } + if (powerInteractor.detailedWakefulness.value.isAsleep()) { + // The logic below is for when the device becomes unlocked. That must be a + // no-op if the device is not awake. + return@mapNotNull null + } + if ( isOnPrimaryBouncer && deviceUnlockStatus.deviceUnlockSource == DeviceUnlockSource.TrustAgent @@ -833,7 +858,7 @@ constructor( } .collect { val loggingReason = "Falsing detected." - switchToScene(Scenes.Lockscreen, loggingReason) + switchToScene(targetSceneKey = Scenes.Lockscreen, loggingReason = loggingReason) } } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt index 73c71f6088e1..452ea3f719fa 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt @@ -90,7 +90,7 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer: fun logSceneChangeRejection( from: ContentKey?, to: ContentKey?, - originalChangeReason: String, + originalChangeReason: String?, rejectionReason: String, ) { logBuffer.log( @@ -112,8 +112,10 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer: "scene " } ) - append("change $str1 because \"$str2\" ") - append("(original change reason: \"$str3\")") + append("change $str1 because \"$str2\"") + if (str3 != null) { + append(" (original change reason: \"$str3\")") + } } }, ) @@ -136,8 +138,11 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer: logBuffer.log( tag = TAG, level = LogLevel.INFO, - messageInitializer = { str1 = transitionState.currentScene.toString() }, - messagePrinter = { "Scene transition idle on: $str1" }, + messageInitializer = { + str1 = transitionState.currentScene.toString() + str2 = transitionState.currentOverlays.joinToString() + }, + messagePrinter = { "Scene transition idle on: $str1, overlays: $str2" }, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt index a81fcec94989..d8bb84af6023 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt @@ -219,15 +219,24 @@ constructor( * it being a false touch. */ fun canChangeScene(toScene: SceneKey): Boolean { - return isInteractionAllowedByFalsing(toScene).also { - // A scene change is guaranteed; log it. - logger.logSceneChanged( - from = currentScene.value, - to = toScene, - sceneState = null, - reason = "user interaction", - isInstant = false, - ) + return isInteractionAllowedByFalsing(toScene).also { sceneChangeAllowed -> + if (sceneChangeAllowed) { + // A scene change is guaranteed; log it. + logger.logSceneChanged( + from = currentScene.value, + to = toScene, + sceneState = null, + reason = "user interaction", + isInstant = false, + ) + } else { + logger.logSceneChangeRejection( + from = currentScene.value, + to = toScene, + originalChangeReason = null, + rejectionReason = "Falsing: false touch detected", + ) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ScrollCaptureController.java index f4c77da674b0..742067a98057 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ScrollCaptureController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ScrollCaptureController.java @@ -24,6 +24,8 @@ import android.provider.Settings; import android.util.Log; import android.view.ScrollCaptureResponse; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.concurrent.futures.CallbackToFutureAdapter; import androidx.concurrent.futures.CallbackToFutureAdapter.Completer; @@ -68,11 +70,15 @@ public class ScrollCaptureController { private final UiEventLogger mEventLogger; private final ScrollCaptureClient mClient; + @Nullable private Completer<LongScreenshot> mCaptureCompleter; + @Nullable private ListenableFuture<Session> mSessionFuture; private Session mSession; + @Nullable private ListenableFuture<CaptureResult> mTileFuture; + @Nullable private ListenableFuture<Void> mEndFuture; private String mWindowOwner; private volatile boolean mCancelled; @@ -148,8 +154,9 @@ public class ScrollCaptureController { } @Inject - ScrollCaptureController(Context context, @Background Executor bgExecutor, - ScrollCaptureClient client, ImageTileSet imageTileSet, UiEventLogger logger) { + ScrollCaptureController(@NonNull Context context, @Background Executor bgExecutor, + @NonNull ScrollCaptureClient client, @NonNull ImageTileSet imageTileSet, + @NonNull UiEventLogger logger) { mContext = context; mBgExecutor = bgExecutor; mClient = client; @@ -214,7 +221,9 @@ public class ScrollCaptureController { } catch (InterruptedException | ExecutionException e) { // Failure to start, propagate to caller Log.e(TAG, "session start failed!"); - mCaptureCompleter.setException(e); + if (mCaptureCompleter != null) { + mCaptureCompleter.setException(e); + } mEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_FAILURE, 0, mWindowOwner); } } @@ -235,7 +244,9 @@ public class ScrollCaptureController { Log.e(TAG, "requestTile cancelled"); } catch (InterruptedException | ExecutionException e) { Log.e(TAG, "requestTile failed!", e); - mCaptureCompleter.setException(e); + if (mCaptureCompleter != null) { + mCaptureCompleter.setException(e); + } } }, mBgExecutor); } @@ -350,7 +361,9 @@ public class ScrollCaptureController { } // Provide result to caller and complete the top-level future // Caller is responsible for releasing this resource (ImageReader/HardwareBuffers) - mCaptureCompleter.set(new LongScreenshot(mSession, mImageTileSet)); + if (mCaptureCompleter != null) { + mCaptureCompleter.set(new LongScreenshot(mSession, mImageTileSet)); + } }, mContext.getMainExecutor()); } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index 362b5db012e1..913aacb53e12 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -20,7 +20,6 @@ import android.content.Context import android.content.res.Configuration import android.graphics.Rect import android.os.PowerManager -import android.os.SystemClock import android.util.ArraySet import android.view.GestureDetector import android.view.MotionEvent @@ -43,6 +42,7 @@ import androidx.lifecycle.repeatOnLifecycle import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.theme.PlatformTheme import com.android.internal.annotations.VisibleForTesting +import com.android.keyguard.UserActivityNotifier import com.android.systemui.Flags import com.android.systemui.ambient.touch.TouchMonitor import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent @@ -53,6 +53,7 @@ import com.android.systemui.communal.ui.compose.CommunalContainer import com.android.systemui.communal.ui.compose.CommunalContent import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.util.CommunalColors +import com.android.systemui.communal.util.UserTouchActivityNotifier import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor @@ -100,7 +101,9 @@ constructor( private val notificationStackScrollLayoutController: NotificationStackScrollLayoutController, private val keyguardMediaController: KeyguardMediaController, private val lockscreenSmartspaceController: LockscreenSmartspaceController, + private val userTouchActivityNotifier: UserTouchActivityNotifier, @CommunalTouchLog logBuffer: LogBuffer, + private val userActivityNotifier: UserActivityNotifier, ) : LifecycleOwner { private val logger = Logger(logBuffer, TAG) @@ -644,8 +647,8 @@ constructor( // result in broken states. return true } + var handled = hubShowing try { - var handled = false if (!touchTakenByKeyguardGesture) { communalContainerWrapper?.dispatchTouchEvent(ev) { if (it) { @@ -653,13 +656,11 @@ constructor( } } } - return handled || hubShowing + return handled } finally { - powerManager.userActivity( - SystemClock.uptimeMillis(), - PowerManager.USER_ACTIVITY_EVENT_TOUCH, - 0, - ) + if (handled) { + userTouchActivityNotifier.notifyActivity(ev) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/OWNERS b/packages/SystemUI/src/com/android/systemui/shade/OWNERS index 89454b84a528..47ca531b8502 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/shade/OWNERS @@ -1,5 +1,4 @@ justinweir@google.com -syeonlee@google.com nicomazz@google.com burakov@google.com @@ -8,16 +7,16 @@ per-file *Notification* = file:../statusbar/notification/OWNERS per-file NotificationsQuickSettingsContainer.java = kozynski@google.com, asc@google.com per-file NotificationsQSContainerController.kt = kozynski@google.com, asc@google.com -per-file *ShadeHeader* = syeonlee@google.com, kozynski@google.com, asc@google.com +per-file *ShadeHeader* = kozynski@google.com, asc@google.com per-file *Interactor* = set noparent -per-file *Interactor* = justinweir@google.com, syeonlee@google.com, nijamkin@google.com, nicomazz@google.com, burakov@google.com +per-file *Interactor* = justinweir@google.com, nijamkin@google.com, nicomazz@google.com, burakov@google.com per-file *Repository* = set noparent -per-file *Repository* = justinweir@google.com, syeonlee@google.com, nijamkin@google.com, nicomazz@google.com, burakov@google.com +per-file *Repository* = justinweir@google.com, nijamkin@google.com, nicomazz@google.com, burakov@google.com -per-file NotificationShadeWindow* = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, syeonlee@google.com, nicomazz@google.com, burakov@google.com +per-file NotificationShadeWindow* = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, nicomazz@google.com, burakov@google.com per-file NotificationPanelUnfoldAnimationController.kt = alexflo@google.com, jeffdq@google.com, juliacr@google.com -per-file NotificationPanelView.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, syeonlee@google.com, nicomazz@google.com, burakov@google.com -per-file NotificationPanelViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, syeonlee@google.com, nicomazz@google.com, burakov@google.com +per-file NotificationPanelView.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, nicomazz@google.com, burakov@google.com +per-file NotificationPanelViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, nicomazz@google.com, burakov@google.com diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt index b211f0729318..82d361797f96 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt @@ -101,6 +101,7 @@ constructor( shadeInteractor.collapseQuickSettingsShade( loggingReason = "ShadeControllerSceneImpl.instantCollapseShade", transitionKey = Instant, + bypassNotificationsShade = true, ) } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt index 446d4b450edc..0132390f9ce8 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt @@ -23,6 +23,7 @@ import android.view.WindowManager import android.view.WindowManager.LayoutParams import android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE import android.window.WindowContext +import com.android.app.tracing.TrackGroupUtils.trackGroup import com.android.systemui.CoreStartable import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.common.ui.ConfigurationStateImpl @@ -34,6 +35,8 @@ import com.android.systemui.common.ui.view.ChoreographerUtils import com.android.systemui.common.ui.view.ChoreographerUtilsImpl import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LogBufferFactory import com.android.systemui.res.R import com.android.systemui.scene.ui.view.WindowRootView import com.android.systemui.shade.data.repository.MutableShadeDisplaysRepository @@ -266,6 +269,20 @@ object ShadeDisplayAwareModule { @Provides @ShadeOnDefaultDisplayWhenLocked fun provideShadeOnDefaultDisplayWhenLocked(): Boolean = true + + /** Provides a [LogBuffer] for use by classes related to shade movement */ + @Provides + @SysUISingleton + @ShadeDisplayLog + fun provideShadeDisplayLogLogBuffer(factory: LogBufferFactory): LogBuffer { + val logBufferName = "ShadeDisplayLog" + return factory.create( + logBufferName, + maxSize = 400, + alwaysLogToLogcat = true, + systraceTrackName = trackGroup("shade", logBufferName), + ) + } } /** Module that should be included only if the shade window [WindowRootView] is available. */ @@ -298,3 +315,6 @@ object ShadeDisplayAwareWithShadeWindowModule { * how well this solution behaves from the performance point of view. */ @Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class ShadeOnDefaultDisplayWhenLocked + +/** A [com.android.systemui.log.LogBuffer] for changes to the shade display. */ +@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class ShadeDisplayLog diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt index bd7796118038..1ec83835ab43 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt @@ -24,6 +24,8 @@ import com.android.systemui.CoreStartable import com.android.systemui.common.ui.data.repository.ConfigurationRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel import com.android.systemui.shade.data.repository.ShadeDisplaysRepository import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.domain.interactor.ShadeModeInteractor @@ -42,12 +44,13 @@ constructor( private val shadeDisplaysRepository: Lazy<ShadeDisplaysRepository>, @ShadeDisplayAware private val configurationRepository: ConfigurationRepository, @Application private val scope: CoroutineScope, + @ShadeDisplayLog private val logBuffer: LogBuffer, ) : CoreStartable { override fun start() { scope.launchTraced("ShadeStateTraceLogger") { launch { val stateLogger = createTraceStateLogger("isShadeLayoutWide") - shadeInteractor.isShadeLayoutWide.collect { stateLogger.log(it.toString()) } + shadeModeInteractor.isShadeLayoutWide.collect { stateLogger.log(it.toString()) } } launch { val stateLogger = createTraceStateLogger("shadeMode") @@ -72,6 +75,18 @@ constructor( "configurationChange#smallestScreenWidthDp", it.smallestScreenWidthDp, ) + logBuffer.log( + "ShadeStateTraceLogger", + LogLevel.DEBUG, + { + int1 = it.smallestScreenWidthDp + int2 = it.densityDpi + }, + { + "New configuration change from Shade window. " + + "smallestScreenWidthDp: $int1, densityDpi: $int2" + }, + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt index 0e0f58dc8d0e..d48d56c2403b 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt @@ -27,8 +27,11 @@ import com.android.systemui.common.ui.data.repository.ConfigurationRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.shade.ShadeDisplayChangeLatencyTracker +import com.android.systemui.shade.ShadeDisplayLog import com.android.systemui.shade.ShadeTraceLogger.logMoveShadeWindowTo import com.android.systemui.shade.ShadeTraceLogger.t import com.android.systemui.shade.ShadeTraceLogger.traceReparenting @@ -69,6 +72,7 @@ constructor( private val notificationRebindingTracker: NotificationRebindingTracker, private val notificationStackRebindingHider: NotificationStackRebindingHider, @ShadeDisplayAware private val configForwarder: ConfigurationForwarder, + @ShadeDisplayLog private val logBuffer: LogBuffer, ) : CoreStartable { private val hasActiveNotifications: Boolean @@ -101,7 +105,12 @@ constructor( /** Tries to move the shade. If anything wrong happens, fails gracefully without crashing. */ private suspend fun moveShadeWindowTo(destinationId: Int) { - Log.d(TAG, "Trying to move shade window to display with id $destinationId") + logBuffer.log( + TAG, + LogLevel.DEBUG, + { int1 = destinationId }, + { "Trying to move shade window to display with id $int1" }, + ) logMoveShadeWindowTo(destinationId) var currentId = -1 try { @@ -113,7 +122,12 @@ constructor( val currentDisplay = shadeContext.display ?: error("Current shade display is null") currentId = currentDisplay.displayId if (currentId == destinationId) { - Log.w(TAG, "Trying to move the shade to a display ($currentId) it was already in ") + logBuffer.log( + TAG, + LogLevel.WARNING, + { int1 = currentId }, + { "Trying to move the shade to a display ($int1) it was already in." }, + ) return } @@ -128,9 +142,14 @@ constructor( } } } catch (e: IllegalStateException) { - Log.e( + logBuffer.log( TAG, - "Unable to move the shade window from display $currentId to $destinationId", + LogLevel.ERROR, + { + int1 = currentId + int2 = destinationId + }, + { "Unable to move the shade window from display $int1 to $int2" }, e, ) } @@ -200,7 +219,7 @@ constructor( } private fun errorLog(s: String) { - Log.e(TAG, s) + logBuffer.log(TAG, LogLevel.ERROR, s) } private fun checkContextDisplayMatchesExpected(destinationId: Int) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt index 6d68796454eb..b54b518ffbd6 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt @@ -56,15 +56,6 @@ interface ShadeInteractor : BaseShadeInteractor { /** Whether the shade can be expanded from QQS to QS. */ val isExpandToQsEnabled: Flow<Boolean> - - /** - * Whether the shade layout should be wide (true) or narrow (false). - * - * In a wide layout, notifications and quick settings each take up only half the screen width - * (whether they are shown at the same time or not). In a narrow layout, they can each be as - * wide as the entire screen. - */ - val isShadeLayoutWide: StateFlow<Boolean> } /** ShadeInteractor methods with implementations that differ between non-empty impls. */ diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt index 77e6a833c153..4154e2ca281b 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt @@ -46,7 +46,6 @@ class ShadeInteractorEmptyImpl @Inject constructor() : ShadeInteractor { override val isUserInteracting: StateFlow<Boolean> = inactiveFlowBoolean override val isShadeTouchable: Flow<Boolean> = inactiveFlowBoolean override val isExpandToQsEnabled: Flow<Boolean> = inactiveFlowBoolean - override val isShadeLayoutWide: StateFlow<Boolean> = inactiveFlowBoolean override fun expandNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) {} diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt index e8b5d5bdf7df..fb3fc524536d 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt @@ -55,11 +55,7 @@ constructor( userSetupRepository: UserSetupRepository, userSwitcherInteractor: UserSwitcherInteractor, private val baseShadeInteractor: BaseShadeInteractor, - shadeModeInteractor: ShadeModeInteractor, -) : - ShadeInteractor, - BaseShadeInteractor by baseShadeInteractor, - ShadeModeInteractor by shadeModeInteractor { +) : ShadeInteractor, BaseShadeInteractor by baseShadeInteractor { override val isShadeEnabled: StateFlow<Boolean> = disableFlagsInteractor.disableFlags .map { it.isShadeEnabled() } @@ -127,8 +123,4 @@ constructor( disableFlags.isQuickSettingsEnabled() && !isDozing } - - companion object { - private const val TAG = "ShadeInteractor" - } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt index c264e3525026..d03f09175b04 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt @@ -99,12 +99,12 @@ constructor( traceName = "showClock", initialValue = shouldShowClock( - isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value, + isShadeLayoutWide = shadeModeInteractor.isShadeLayoutWide.value, overlays = sceneInteractor.currentOverlays.value, ), source = combine( - shadeInteractor.isShadeLayoutWide, + shadeModeInteractor.isShadeLayoutWide, sceneInteractor.currentOverlays, ::shouldShowClock, ), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt b/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt index 9940ae523b60..6d04c27b9e5e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt @@ -28,7 +28,7 @@ import android.util.Log import android.util.MathUtils import android.view.CrossWindowBlurListeners import android.view.CrossWindowBlurListeners.CROSS_WINDOW_BLUR_SUPPORTED -import android.view.SurfaceControl +import android.view.SyncRtSurfaceTransactionApplier import android.view.ViewRootImpl import androidx.annotation.VisibleForTesting import com.android.systemui.Dumpable @@ -36,26 +36,35 @@ import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager -import java.io.PrintWriter -import javax.inject.Inject import com.android.systemui.keyguard.ui.transitions.BlurConfig import com.android.systemui.res.R +import java.io.PrintWriter +import javax.inject.Inject @SysUISingleton -open class BlurUtils @Inject constructor( +open class BlurUtils +@Inject +constructor( @Main resources: Resources, blurConfig: BlurConfig, private val crossWindowBlurListeners: CrossWindowBlurListeners, - dumpManager: DumpManager + dumpManager: DumpManager, ) : Dumpable { val minBlurRadius = resources.getDimensionPixelSize(R.dimen.min_window_blur_radius).toFloat() - val maxBlurRadius = if (Flags.notificationShadeBlur()) { - blurConfig.maxBlurRadiusPx - } else { - resources.getDimensionPixelSize(R.dimen.max_window_blur_radius).toFloat() - } + val maxBlurRadius = + if (Flags.notificationShadeBlur()) { + blurConfig.maxBlurRadiusPx + } else { + resources.getDimensionPixelSize(R.dimen.max_window_blur_radius).toFloat() + } private var lastAppliedBlur = 0 + private var lastTargetViewRootImpl: ViewRootImpl? = null + private var _transactionApplier = SyncRtSurfaceTransactionApplier(null) + @VisibleForTesting + open val transactionApplier: SyncRtSurfaceTransactionApplier + get() = _transactionApplier + private var earlyWakeupEnabled = false /** When this is true, early wakeup flag is not reset on surface flinger when blur drops to 0 */ @@ -65,9 +74,7 @@ open class BlurUtils @Inject constructor( dumpManager.registerDumpable(this) } - /** - * Translates a ratio from 0 to 1 to a blur radius in pixels. - */ + /** Translates a ratio from 0 to 1 to a blur radius in pixels. */ fun blurRadiusOfRatio(ratio: Float): Float { if (ratio == 0f) { return 0f @@ -75,15 +82,18 @@ open class BlurUtils @Inject constructor( return MathUtils.lerp(minBlurRadius, maxBlurRadius, ratio) } - /** - * Translates a blur radius in pixels to a ratio between 0 to 1. - */ + /** Translates a blur radius in pixels to a ratio between 0 to 1. */ fun ratioOfBlurRadius(blur: Float): Float { if (blur == 0f) { return 0f } - return MathUtils.map(minBlurRadius, maxBlurRadius, - 0f /* maxStart */, 1f /* maxStop */, blur) + return MathUtils.map( + minBlurRadius, + maxBlurRadius, + 0f /* maxStart */, + 1f /* maxStop */, + blur, + ) } /** @@ -91,16 +101,20 @@ open class BlurUtils @Inject constructor( * early-wakeup flag in SurfaceFlinger. */ fun prepareBlur(viewRootImpl: ViewRootImpl?, radius: Int) { - if (viewRootImpl == null || !viewRootImpl.surfaceControl.isValid || - !shouldBlur(radius) || earlyWakeupEnabled + if ( + viewRootImpl == null || + !viewRootImpl.surfaceControl.isValid || + !shouldBlur(radius) || + earlyWakeupEnabled ) { return } + updateTransactionApplier(viewRootImpl) + val builder = + SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(viewRootImpl.surfaceControl) if (lastAppliedBlur == 0 && radius != 0) { - createTransaction().use { - earlyWakeupStart(it, "eEarlyWakeup (prepareBlur)") - it.apply() - } + earlyWakeupStart(builder, "eEarlyWakeup (prepareBlur)") + transactionApplier.scheduleApply(builder.build()) } } @@ -115,25 +129,32 @@ open class BlurUtils @Inject constructor( if (viewRootImpl == null || !viewRootImpl.surfaceControl.isValid) { return } - createTransaction().use { - if (shouldBlur(radius)) { - it.setBackgroundBlurRadius(viewRootImpl.surfaceControl, radius) - if (!earlyWakeupEnabled && lastAppliedBlur == 0 && radius != 0) { - earlyWakeupStart(it, "eEarlyWakeup (applyBlur)") - } - if ( - earlyWakeupEnabled && - lastAppliedBlur != 0 && - radius == 0 && - !persistentEarlyWakeupRequired - ) { - earlyWakeupEnd(it, "applyBlur") - } - lastAppliedBlur = radius + updateTransactionApplier(viewRootImpl) + val builder = + SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(viewRootImpl.surfaceControl) + if (shouldBlur(radius)) { + builder.withBackgroundBlur(radius) + if (!earlyWakeupEnabled && lastAppliedBlur == 0 && radius != 0) { + earlyWakeupStart(builder, "eEarlyWakeup (applyBlur)") + } + if ( + earlyWakeupEnabled && + lastAppliedBlur != 0 && + radius == 0 && + !persistentEarlyWakeupRequired + ) { + earlyWakeupEnd(builder, "applyBlur") } - it.setOpaque(viewRootImpl.surfaceControl, opaque) - it.apply() + lastAppliedBlur = radius } + builder.withOpaque(opaque) + transactionApplier.scheduleApply(builder.build()) + } + + private fun updateTransactionApplier(viewRootImpl: ViewRootImpl) { + if (lastTargetViewRootImpl == viewRootImpl) return + _transactionApplier = SyncRtSurfaceTransactionApplier(viewRootImpl.view) + lastTargetViewRootImpl = viewRootImpl } private fun v(verboseLog: String) { @@ -141,47 +162,49 @@ open class BlurUtils @Inject constructor( } @SuppressLint("MissingPermission") - private fun earlyWakeupStart(transaction: SurfaceControl.Transaction, traceMethodName: String) { + private fun earlyWakeupStart( + builder: SyncRtSurfaceTransactionApplier.SurfaceParams.Builder, + traceMethodName: String, + ) { v("earlyWakeupStart from $traceMethodName") Trace.asyncTraceForTrackBegin(TRACE_TAG_APP, TRACK_NAME, traceMethodName, 0) - transaction.setEarlyWakeupStart() + builder.withEarlyWakeupStart() earlyWakeupEnabled = true } @SuppressLint("MissingPermission") - private fun earlyWakeupEnd(transaction: SurfaceControl.Transaction, loggingContext: String) { + private fun earlyWakeupEnd( + builder: SyncRtSurfaceTransactionApplier.SurfaceParams.Builder, + loggingContext: String, + ) { v("earlyWakeupEnd from $loggingContext") - transaction.setEarlyWakeupEnd() + builder.withEarlyWakeupEnd() Trace.asyncTraceForTrackEnd(TRACE_TAG_APP, TRACK_NAME, 0) earlyWakeupEnabled = false } - @VisibleForTesting - open fun createTransaction(): SurfaceControl.Transaction { - return SurfaceControl.Transaction() - } - private fun shouldBlur(radius: Int): Boolean { return supportsBlursOnWindows() || - ((Flags.notificationShadeBlur() || Flags.bouncerUiRevamp()) && - supportsBlursOnWindowsBase() && - lastAppliedBlur > 0 && - radius == 0) + ((Flags.notificationShadeBlur() || Flags.bouncerUiRevamp()) && + supportsBlursOnWindowsBase() && + lastAppliedBlur > 0 && + radius == 0) } /** * If this device can render blurs. * - * @see android.view.SurfaceControl.Transaction#setBackgroundBlurRadius(SurfaceControl, int) * @return {@code true} when supported. + * @see android.view.SurfaceControl.Transaction#setBackgroundBlurRadius(SurfaceControl, int) */ open fun supportsBlursOnWindows(): Boolean { return supportsBlursOnWindowsBase() && crossWindowBlurListeners.isCrossWindowBlurEnabled } private fun supportsBlursOnWindowsBase(): Boolean { - return CROSS_WINDOW_BLUR_SUPPORTED && ActivityManager.isHighEndGfx() && - !SystemProperties.getBoolean("persist.sysui.disableBlur", false) + return CROSS_WINDOW_BLUR_SUPPORTED && + ActivityManager.isHighEndGfx() && + !SystemProperties.getBoolean("persist.sysui.disableBlur", false) } override fun dump(pw: PrintWriter, args: Array<out String>) { @@ -203,12 +226,14 @@ open class BlurUtils @Inject constructor( fun setPersistentEarlyWakeup(persistentWakeup: Boolean, viewRootImpl: ViewRootImpl?) { persistentEarlyWakeupRequired = persistentWakeup if (viewRootImpl == null || !supportsBlursOnWindows()) return + + updateTransactionApplier(viewRootImpl) + val builder = + SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(viewRootImpl.surfaceControl) if (persistentEarlyWakeupRequired) { if (earlyWakeupEnabled) return - createTransaction().use { - earlyWakeupStart(it, "setEarlyWakeup") - it.apply() - } + earlyWakeupStart(builder, "setEarlyWakeup") + transactionApplier.scheduleApply(builder.build()) } else { if (!earlyWakeupEnabled) return if (lastAppliedBlur > 0) { @@ -219,10 +244,8 @@ open class BlurUtils @Inject constructor( " was still active", ) } - createTransaction().use { - earlyWakeupEnd(it, "resetEarlyWakeup") - it.apply() - } + earlyWakeupEnd(builder, "resetEarlyWakeup") + transactionApplier.scheduleApply(builder.build()) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt index d2f424a46620..ba446837a72e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt @@ -74,7 +74,7 @@ constructor( private val singleShadeOverScrollerFactory: SingleShadeLockScreenOverScroller.Factory, private val activityStarter: ActivityStarter, wakefulnessLifecycle: WakefulnessLifecycle, - configurationController: ConfigurationController, + @ShadeDisplayAware configurationController: ConfigurationController, falsingManager: FalsingManager, dumpManager: DumpManager, qsTransitionControllerFactory: LockscreenShadeQsTransitionController.Factory, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java index 2030606e4274..72ece3db307b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java @@ -73,7 +73,7 @@ public class NotificationGroupingUtil { } return null; } else { - return row.getEntry().getSbn().getNotification(); + return row.getEntryLegacy().getSbn().getNotification(); } } }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt index e292bcf1f7a8..50d634f6ac54 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt @@ -334,6 +334,14 @@ constructor( private fun onBlurApplied(appliedBlurRadius: Int, zoomOutFromShadeRadius: Float) { lastAppliedBlur = appliedBlurRadius + onZoomOutChanged(zoomOutFromShadeRadius) + listeners.forEach { it.onBlurRadiusChanged(appliedBlurRadius) } + notificationShadeWindowController.setBackgroundBlurRadius(appliedBlurRadius) + } + + private fun onZoomOutChanged(zoomOutFromShadeRadius: Float) { + TrackTracer.instantForGroup("shade", "zoom_out", zoomOutFromShadeRadius) + Log.v(TAG, "onZoomOutChanged $zoomOutFromShadeRadius") wallpaperController.setNotificationShadeZoom(zoomOutFromShadeRadius) if (spatialModelAppPushback()) { appZoomOutOptional.ifPresent { appZoomOut -> @@ -341,13 +349,15 @@ constructor( } keyguardInteractor.setZoomOut(zoomOutFromShadeRadius) } - listeners.forEach { - it.onWallpaperZoomOutChanged(zoomOutFromShadeRadius) - it.onBlurRadiusChanged(appliedBlurRadius) - } - notificationShadeWindowController.setBackgroundBlurRadius(appliedBlurRadius) } + private val applyZoomOutForFrame = + Choreographer.FrameCallback { + updateScheduled = false + val (_, zoomOutFromShadeRadius) = computeBlurAndZoomOut() + onZoomOutChanged(zoomOutFromShadeRadius) + } + /** Animate blurs when unlocking. */ private val keyguardStateCallback = object : KeyguardStateController.Callback { @@ -628,8 +638,17 @@ constructor( val (blur, zoomOutFromShadeRadius) = computeBlurAndZoomOut() zoomOutCalculatedFromShadeRadius = zoomOutFromShadeRadius if (Flags.bouncerUiRevamp() || Flags.glanceableHubBlurredBackground()) { - updateScheduled = - windowRootViewBlurInteractor.requestBlurForShade(blur, shouldBlurBeOpaque) + if (windowRootViewBlurInteractor.isBlurCurrentlySupported.value) { + updateScheduled = + windowRootViewBlurInteractor.requestBlurForShade(blur, shouldBlurBeOpaque) + return + } + // When blur is not supported, zoom out still needs to happen when scheduleUpdate + // is invoked and a separate frame callback has to be wired-up to support that. + if (!updateScheduled) { + updateScheduled = true + choreographer.postFrameCallback(applyZoomOutForFrame) + } return } if (updateScheduled) { @@ -761,9 +780,6 @@ constructor( /** Invoked when changes are needed in z-space */ interface DepthListener { - /** Current wallpaper zoom out, where 0 is the closest, and 1 the farthest */ - fun onWallpaperZoomOutChanged(zoomOut: Float) - fun onBlurRadiusChanged(blurRadius: Int) {} } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/OWNERS b/packages/SystemUI/src/com/android/systemui/statusbar/OWNERS index 6d3c12d139db..0ebe194018cf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/statusbar/OWNERS @@ -6,8 +6,8 @@ caitlinshk@google.com evanlaird@google.com pixel@google.com -per-file *Biometrics* = set noparent -per-file *Biometrics* = file:../keyguard/OWNERS +per-file *Biometric* = set noparent +per-file *Biometric* = file:../keyguard/OWNERS per-file *Doze* = set noparent per-file *Doze* = file:../keyguard/OWNERS per-file *Keyboard* = set noparent diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/OperatorNameViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/OperatorNameViewController.java index f5d443443838..60a62d480633 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/OperatorNameViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/OperatorNameViewController.java @@ -97,7 +97,13 @@ public class OperatorNameViewController extends ViewController<OperatorNameView> boolean showOperatorName = mCarrierConfigTracker .getShowOperatorNameInStatusBarConfig(defaultSubInfo.getSubId()) - && (mTunerService.getValue(KEY_SHOW_OPERATOR_NAME, 1) != 0); + && (mTunerService.getValue( + KEY_SHOW_OPERATOR_NAME, + mView.getResources() + .getInteger( + com.android.internal.R.integer + .config_showOperatorNameDefault)) + != 0); mView.update( showOperatorName, mTelephonyManager.isDataCapable(), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt index 9cae6c135fb0..0763cb7751a0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt @@ -21,6 +21,7 @@ import android.content.ComponentName import android.content.Context import android.view.View import com.android.internal.jank.Cuj +import com.android.internal.logging.InstanceId import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.animation.ComposableControllerFactory import com.android.systemui.animation.DelegateTransitionAnimatorController @@ -41,6 +42,7 @@ import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel +import com.android.systemui.statusbar.chips.uievents.StatusBarChipsUiEventLogger import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel @@ -59,7 +61,7 @@ import kotlinx.coroutines.flow.stateIn /** View model for the ongoing phone call chip shown in the status bar. */ @SysUISingleton -open class CallChipViewModel +class CallChipViewModel @Inject constructor( @Main private val context: Context, @@ -68,6 +70,7 @@ constructor( systemClock: SystemClock, private val activityStarter: ActivityStarter, @StatusBarChipsLog private val logger: LogBuffer, + private val uiEventLogger: StatusBarChipsUiEventLogger, ) : OngoingActivityChipViewModel { /** The transition cookie used to register and unregister launch and return animations. */ private val cookie = @@ -180,7 +183,7 @@ constructor( isHidden: Boolean, transitionState: TransitionState = TransitionState.NoTransition, ): OngoingActivityChipModel.Active { - val key = state.notificationKey + val key = "$KEY_PREFIX${state.notificationKey}" val contentDescription = getContentDescription(state.appName) val icon = if (state.notificationIconView != null) { @@ -199,6 +202,8 @@ constructor( } val colors = ColorsModel.AccentThemed + val intent = state.intent + val instanceId = state.notificationInstanceId // This block mimics OngoingCallController#updateChip. if (state.startTimeMs <= 0L) { @@ -208,10 +213,11 @@ constructor( key = key, icon = icon, colors = colors, - onClickListenerLegacy = getOnClickListener(state.intent), - clickBehavior = getClickBehavior(state.intent), + onClickListenerLegacy = getOnClickListener(intent, instanceId), + clickBehavior = getClickBehavior(intent, instanceId), isHidden = isHidden, transitionManager = getTransitionManager(state, transitionState), + instanceId = instanceId, ) } else { val startTimeInElapsedRealtime = @@ -221,19 +227,26 @@ constructor( icon = icon, colors = colors, startTimeMs = startTimeInElapsedRealtime, - onClickListenerLegacy = getOnClickListener(state.intent), - clickBehavior = getClickBehavior(state.intent), + onClickListenerLegacy = getOnClickListener(intent, instanceId), + clickBehavior = getClickBehavior(intent, instanceId), isHidden = isHidden, transitionManager = getTransitionManager(state, transitionState), + instanceId = instanceId, ) } } - private fun getOnClickListener(intent: PendingIntent?): View.OnClickListener? { + private fun getOnClickListener( + intent: PendingIntent?, + instanceId: InstanceId?, + ): View.OnClickListener? { if (intent == null) return null return View.OnClickListener { view -> StatusBarChipsModernization.assertInLegacyMode() + logger.log(TAG, LogLevel.INFO, {}, { "Chip clicked" }) + uiEventLogger.logChipTapToShow(instanceId) + val backgroundView = view.requireViewById<ChipBackgroundContainer>(R.id.ongoing_activity_chip_background) // This mimics OngoingCallController#updateChipClickListener. @@ -247,13 +260,20 @@ constructor( } } - private fun getClickBehavior(intent: PendingIntent?): OngoingActivityChipModel.ClickBehavior = + private fun getClickBehavior( + intent: PendingIntent?, + instanceId: InstanceId?, + ): OngoingActivityChipModel.ClickBehavior = if (intent == null) { OngoingActivityChipModel.ClickBehavior.None } else { OngoingActivityChipModel.ClickBehavior.ExpandAction( onClick = { expandable -> StatusBarChipsModernization.unsafeAssertInNewMode() + + logger.log(TAG, LogLevel.INFO, {}, { "Chip clicked" }) + uiEventLogger.logChipTapToShow(instanceId) + val animationController = if ( !StatusBarChipsReturnAnimations.isEnabled || @@ -413,6 +433,8 @@ constructor( ) private val TAG = "CallVM".pad() + const val KEY_PREFIX = "callChip-" + /** Determines whether or not an active call chip should be hidden. */ private fun shouldChipBeHidden( oldState: OngoingCallModel, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt index c862460ad6f7..596770ff9dfc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel import android.content.Context import androidx.annotation.DrawableRes -import androidx.annotation.VisibleForTesting import com.android.internal.jank.Cuj import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator @@ -44,6 +43,7 @@ import com.android.systemui.statusbar.chips.ui.viewmodel.ChipTransitionHelper import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickCallback import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener +import com.android.systemui.statusbar.chips.uievents.StatusBarChipsUiEventLogger import com.android.systemui.util.time.SystemClock import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -70,7 +70,11 @@ constructor( private val dialogTransitionAnimator: DialogTransitionAnimator, private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper, @StatusBarChipsLog private val logger: LogBuffer, + private val uiEventLogger: StatusBarChipsUiEventLogger, ) : OngoingActivityChipViewModel { + // There can only be 1 active cast-to-other-device chip at a time, so we can re-use the ID. + private val instanceId = uiEventLogger.createNewInstanceId() + /** The cast chip to show, based only on MediaProjection API events. */ private val projectionChip: StateFlow<OngoingActivityChipModel> = mediaProjectionChipInteractor.projection @@ -213,8 +217,10 @@ constructor( createCastScreenToOtherDeviceDialogDelegate(state), dialogTransitionAnimator, DIALOG_CUJ, - logger, - TAG, + instanceId = instanceId, + uiEventLogger = uiEventLogger, + logger = logger, + tag = TAG, ), clickBehavior = OngoingActivityChipModel.ClickBehavior.ExpandAction( @@ -223,10 +229,13 @@ constructor( createCastScreenToOtherDeviceDialogDelegate(state), dialogTransitionAnimator, DIALOG_CUJ, - logger, - TAG, + instanceId = instanceId, + uiEventLogger = uiEventLogger, + logger = logger, + tag = TAG, ) ), + instanceId = instanceId, ) } @@ -248,8 +257,10 @@ constructor( createGenericCastToOtherDeviceDialogDelegate(deviceName), dialogTransitionAnimator, DIALOG_CUJ_AUDIO_ONLY, - logger, - TAG, + instanceId = instanceId, + uiEventLogger = uiEventLogger, + logger = logger, + tag = TAG, ), clickBehavior = OngoingActivityChipModel.ClickBehavior.ExpandAction( @@ -257,10 +268,13 @@ constructor( createGenericCastToOtherDeviceDialogDelegate(deviceName), dialogTransitionAnimator, DIALOG_CUJ_AUDIO_ONLY, - logger, - TAG, + instanceId = instanceId, + uiEventLogger = uiEventLogger, + logger = logger, + tag = TAG, ) ), + instanceId = instanceId, ) } @@ -281,7 +295,7 @@ constructor( ) companion object { - @VisibleForTesting const val KEY = "CastToOtherDevice" + const val KEY = "CastToOtherDevice" @DrawableRes val CAST_TO_OTHER_DEVICE_ICON = R.drawable.ic_cast_connected private val DIALOG_CUJ = DialogCuj(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, tag = "Cast to other device") diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt index a0de879845d3..6e0682c4d5b9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt @@ -149,6 +149,7 @@ constructor( creationTime = creationTime, isAppVisible = appVisibility.isAppCurrentlyVisible, lastAppVisibleTime = appVisibility.lastAppVisibleTime, + instanceId = instanceId, ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt index 356731cb3777..dad51baced56 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.notification.domain.model +import com.android.internal.logging.InstanceId import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels @@ -35,4 +36,6 @@ data class NotificationChipModel( * hasn't become visible since the notification became promoted. */ val lastAppVisibleTime: Long?, + /** An optional per-notification ID used for logging. */ + val instanceId: InstanceId?, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/shared/StatusBarNotifChips.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/shared/StatusBarNotifChips.kt index 6431f303089f..5b989d8e1e7c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/shared/StatusBarNotifChips.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/shared/StatusBarNotifChips.kt @@ -16,15 +16,18 @@ package com.android.systemui.statusbar.chips.notification.shared -import com.android.systemui.Flags +import android.app.Flags import com.android.systemui.flags.FlagToken import com.android.systemui.flags.RefactorFlagUtils +// NOTE: We're merging this flag with the `ui_rich_ongoing` flag. +// We'll replace all usages of this class with PromotedNotificationUi as a follow-up. + /** Helper for reading or using the status bar promoted notification chips flag state. */ @Suppress("NOTHING_TO_INLINE") object StatusBarNotifChips { /** The aconfig flag name */ - const val FLAG_NAME = Flags.FLAG_STATUS_BAR_NOTIFICATION_CHIPS + const val FLAG_NAME = Flags.FLAG_UI_RICH_ONGOING /** A token used for dependency declaration */ val token: FlagToken @@ -33,7 +36,7 @@ object StatusBarNotifChips { /** Is the refactor enabled */ @JvmStatic inline val isEnabled - get() = Flags.statusBarNotificationChips() + get() = Flags.uiRichOngoing() /** * Called to ensure code is only run when the flag is enabled. This protects users from the diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt index e45524b59837..83ef13d341d8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt @@ -127,6 +127,7 @@ constructor( onClickListenerLegacy = onClickListenerLegacy, clickBehavior = clickBehavior, isHidden = isHidden, + instanceId = instanceId, ) } @@ -139,6 +140,7 @@ constructor( onClickListenerLegacy = onClickListenerLegacy, clickBehavior = clickBehavior, isHidden = isHidden, + instanceId = instanceId, ) } @@ -154,6 +156,7 @@ constructor( onClickListenerLegacy = onClickListenerLegacy, clickBehavior = clickBehavior, isHidden = isHidden, + instanceId = instanceId, ) } @@ -165,6 +168,7 @@ constructor( onClickListenerLegacy = onClickListenerLegacy, clickBehavior = clickBehavior, isHidden = isHidden, + instanceId = instanceId, ) } @@ -182,6 +186,7 @@ constructor( onClickListenerLegacy = onClickListenerLegacy, clickBehavior = clickBehavior, isHidden = isHidden, + instanceId = instanceId, ) } else { // Don't show a `when` time that's close to now or in the past because it's @@ -199,6 +204,7 @@ constructor( onClickListenerLegacy = onClickListenerLegacy, clickBehavior = clickBehavior, isHidden = isHidden, + instanceId = instanceId, ) } } @@ -212,6 +218,7 @@ constructor( onClickListenerLegacy = onClickListenerLegacy, clickBehavior = clickBehavior, isHidden = isHidden, + instanceId = instanceId, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt index 363b8beab2d7..336a9f47e5d1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel import android.app.ActivityManager import android.content.Context import androidx.annotation.DrawableRes -import androidx.annotation.VisibleForTesting import com.android.internal.jank.Cuj import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator @@ -44,6 +43,7 @@ import com.android.systemui.statusbar.chips.ui.viewmodel.ChipTransitionHelper import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickCallback import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener +import com.android.systemui.statusbar.chips.uievents.StatusBarChipsUiEventLogger import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.time.SystemClock import javax.inject.Inject @@ -66,7 +66,9 @@ constructor( private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper, private val dialogTransitionAnimator: DialogTransitionAnimator, @StatusBarChipsLog private val logger: LogBuffer, + private val uiEventLogger: StatusBarChipsUiEventLogger, ) : OngoingActivityChipViewModel { + private val instanceId = uiEventLogger.createNewInstanceId() /** A direct mapping from [ScreenRecordChipModel] to [OngoingActivityChipModel]. */ private val simpleChip = @@ -80,6 +82,7 @@ constructor( isImportantForPrivacy = true, colors = ColorsModel.Red, secondsUntilStarted = state.millisUntilStarted.toCountdownSeconds(), + instanceId = instanceId, ) } is ScreenRecordChipModel.Recording -> { @@ -102,8 +105,10 @@ constructor( createDelegate(state.recordedTask), dialogTransitionAnimator, DIALOG_CUJ, - logger, - TAG, + instanceId = instanceId, + uiEventLogger = uiEventLogger, + logger = logger, + tag = TAG, ), clickBehavior = OngoingActivityChipModel.ClickBehavior.ExpandAction( @@ -111,10 +116,13 @@ constructor( dialogDelegate = createDelegate(state.recordedTask), dialogTransitionAnimator = dialogTransitionAnimator, DIALOG_CUJ, - logger, - TAG, + instanceId = instanceId, + uiEventLogger = uiEventLogger, + logger = logger, + tag = TAG, ) ), + instanceId = instanceId, ) } } @@ -167,7 +175,7 @@ constructor( } companion object { - @VisibleForTesting const val KEY = "ScreenRecord" + const val KEY = "ScreenRecord" @DrawableRes val ICON = R.drawable.ic_screenrecord private val DIALOG_CUJ = DialogCuj(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, tag = "Screen record") diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt index 0defa531d18d..26787189209e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel import android.content.Context import androidx.annotation.DrawableRes -import androidx.annotation.VisibleForTesting import com.android.internal.jank.Cuj import com.android.systemui.CoreStartable import com.android.systemui.animation.DialogCuj @@ -44,6 +43,7 @@ import com.android.systemui.statusbar.chips.ui.viewmodel.ChipTransitionHelper import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickCallback import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener +import com.android.systemui.statusbar.chips.uievents.StatusBarChipsUiEventLogger import com.android.systemui.util.kotlin.sample import com.android.systemui.util.time.SystemClock import javax.inject.Inject @@ -72,7 +72,10 @@ constructor( private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper, private val dialogTransitionAnimator: DialogTransitionAnimator, @StatusBarChipsLog private val logger: LogBuffer, + private val uiEventLogger: StatusBarChipsUiEventLogger, ) : OngoingActivityChipViewModel, CoreStartable { + // There can only be 1 active cast-to-other-device chip at a time, so we can re-use the ID. + private val instanceId = uiEventLogger.createNewInstanceId() private val _stopDialogToShow: MutableStateFlow<MediaProjectionStopDialogModel> = MutableStateFlow(MediaProjectionStopDialogModel.Hidden) @@ -238,8 +241,10 @@ constructor( createShareScreenToAppDialogDelegate(state), dialogTransitionAnimator, DIALOG_CUJ, - logger, - TAG, + instanceId = instanceId, + uiEventLogger = uiEventLogger, + logger = logger, + tag = TAG, ), clickBehavior = OngoingActivityChipModel.ClickBehavior.ExpandAction( @@ -248,10 +253,13 @@ constructor( createShareScreenToAppDialogDelegate(state), dialogTransitionAnimator, DIALOG_CUJ, - logger, - TAG, + instanceId = instanceId, + uiEventLogger = uiEventLogger, + logger = logger, + tag = TAG, ) ), + instanceId = instanceId, ) } @@ -274,8 +282,10 @@ constructor( createGenericShareToAppDialogDelegate(), dialogTransitionAnimator, DIALOG_CUJ_AUDIO_ONLY, - logger, - TAG, + instanceId = instanceId, + uiEventLogger = uiEventLogger, + logger = logger, + tag = TAG, ), clickBehavior = OngoingActivityChipModel.ClickBehavior.ExpandAction( @@ -283,10 +293,13 @@ constructor( createGenericShareToAppDialogDelegate(), dialogTransitionAnimator, DIALOG_CUJ_AUDIO_ONLY, - logger, - TAG, + instanceId = instanceId, + uiEventLogger = uiEventLogger, + logger = logger, + tag = TAG, ) ), + instanceId = instanceId, ) } @@ -306,7 +319,7 @@ constructor( ) companion object { - @VisibleForTesting const val KEY = "ShareToApp" + const val KEY = "ShareToApp" @DrawableRes val SHARE_TO_APP_ICON = R.drawable.ic_present_to_all private val DIALOG_CUJ = DialogCuj(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, tag = "Share to app") diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt index fa8d25623d67..18cecb4abc31 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt @@ -132,7 +132,11 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Active, modifier: Modifier = } is OngoingActivityChipModel.Active.ShortTimeDelta -> { - val timeRemainingState = rememberTimeRemainingState(futureTimeMillis = viewModel.time) + val timeRemainingState = + rememberTimeRemainingState( + futureTimeMillis = viewModel.time, + timeSource = viewModel.timeSource, + ) timeRemainingState.timeRemainingData?.let { val text = formatTimeRemainingData(it) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt index 104c2b546200..167035b2d17d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt @@ -16,8 +16,11 @@ package com.android.systemui.statusbar.chips.ui.compose +import android.annotation.IdRes import android.content.res.ColorStateList +import android.util.Log import android.view.ViewGroup +import android.widget.FrameLayout import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -234,10 +237,35 @@ private fun StatusBarIcon( AndroidView( modifier = modifier, factory = { _ -> - iconFactory.invoke()?.apply { - layoutParams = ViewGroup.LayoutParams(iconSizePx, iconSizePx) - } ?: throw IllegalStateException("Missing StatusBarIconView for $notificationKey") + // Use a wrapper frame layout so that we still return a view even if the icon is null + val wrapperFrameLayout = FrameLayout(context) + + val icon = iconFactory.invoke() + if (icon == null) { + Log.e(TAG, "Missing StatusBarIconView for $notificationKey") + } else { + icon.apply { + id = CUSTOM_ICON_VIEW_ID + layoutParams = ViewGroup.LayoutParams(iconSizePx, iconSizePx) + } + // If needed, remove the icon from its old parent (views can only be attached + // to 1 parent at a time) + (icon.parent as? ViewGroup)?.apply { + this.removeView(icon) + this.removeTransientView(icon) + } + wrapperFrameLayout.addView(icon) + } + + wrapperFrameLayout + }, + update = { frameLayout -> + frameLayout.findViewById<StatusBarIconView>(CUSTOM_ICON_VIEW_ID)?.apply { + this.imageTintList = colorTintList + } }, - update = { iconView -> iconView.imageTintList = colorTintList }, ) } + +private const val TAG = "OngoingActivityChip" +@IdRes private val CUSTOM_ICON_VIEW_ID = R.id.ongoing_activity_chip_custom_icon diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt index ba60e2ce2aa6..2c4746f5fafb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt @@ -20,6 +20,7 @@ import android.annotation.CurrentTimeMillisLong import android.annotation.ElapsedRealtimeLong import android.os.SystemClock import android.view.View +import com.android.internal.logging.InstanceId import com.android.systemui.animation.ComposableControllerFactory import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.ContentDescription @@ -81,6 +82,11 @@ sealed class OngoingActivityChipModel { open val isHidden: Boolean, /** Whether the transition from hidden to shown should be animated. */ open val shouldAnimate: Boolean, + /** + * An optional per-chip ID used for logging. Should stay the same throughout the lifetime of + * a single chip. + */ + open val instanceId: InstanceId? = null, ) : OngoingActivityChipModel() { /** This chip shows only an icon and nothing else. */ @@ -94,6 +100,7 @@ sealed class OngoingActivityChipModel { override val transitionManager: TransitionManager? = null, override val isHidden: Boolean = false, override val shouldAnimate: Boolean = true, + override val instanceId: InstanceId? = null, ) : Active( key, @@ -105,6 +112,7 @@ sealed class OngoingActivityChipModel { transitionManager, isHidden, shouldAnimate, + instanceId, ) { override val logName = "Active.Icon" } @@ -128,7 +136,8 @@ sealed class OngoingActivityChipModel { /** * The [TimeSource] that should be used to track the current time for this timer. Should - * be compatible with [startTimeMs]. + * be compatible units with [startTimeMs]. Only used in the Compose version of the + * chips. */ val timeSource: TimeSource = TimeSource { SystemClock.elapsedRealtime() }, @@ -143,6 +152,7 @@ sealed class OngoingActivityChipModel { override val transitionManager: TransitionManager? = null, override val isHidden: Boolean = false, override val shouldAnimate: Boolean = true, + override val instanceId: InstanceId? = null, ) : Active( key, @@ -154,6 +164,7 @@ sealed class OngoingActivityChipModel { transitionManager, isHidden, shouldAnimate, + instanceId, ) { override val logName = "Active.Timer" } @@ -177,11 +188,18 @@ sealed class OngoingActivityChipModel { * this model and the [Timer] model use the same units. */ @CurrentTimeMillisLong val time: Long, + + /** + * The [TimeSource] that should be used to track the current time for this timer. Should + * be compatible units with [time]. Only used in the Compose version of the chips. + */ + val timeSource: TimeSource = TimeSource { System.currentTimeMillis() }, override val onClickListenerLegacy: View.OnClickListener?, override val clickBehavior: ClickBehavior, override val transitionManager: TransitionManager? = null, override val isHidden: Boolean = false, override val shouldAnimate: Boolean = true, + override val instanceId: InstanceId? = null, ) : Active( key, @@ -193,6 +211,7 @@ sealed class OngoingActivityChipModel { transitionManager, isHidden, shouldAnimate, + instanceId, ) { init { StatusBarNotifChips.unsafeAssertInNewMode() @@ -214,6 +233,7 @@ sealed class OngoingActivityChipModel { override val transitionManager: TransitionManager? = null, override val isHidden: Boolean = false, override val shouldAnimate: Boolean = true, + override val instanceId: InstanceId? = null, ) : Active( key, @@ -225,6 +245,7 @@ sealed class OngoingActivityChipModel { transitionManager, isHidden, shouldAnimate, + instanceId, ) { override val logName = "Active.Countdown" } @@ -241,6 +262,7 @@ sealed class OngoingActivityChipModel { override val transitionManager: TransitionManager? = null, override val isHidden: Boolean = false, override val shouldAnimate: Boolean = true, + override val instanceId: InstanceId? = null, ) : Active( key, @@ -252,6 +274,7 @@ sealed class OngoingActivityChipModel { transitionManager, isHidden, shouldAnimate, + instanceId, ) { override val logName = "Active.Text" } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt index 229d1a56e177..efc54f1c5a74 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.chips.ui.viewmodel import android.view.View +import com.android.internal.logging.InstanceId import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable @@ -26,6 +27,7 @@ import com.android.systemui.res.R import com.android.systemui.statusbar.chips.StatusBarChipsLog import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer +import com.android.systemui.statusbar.chips.uievents.StatusBarChipsUiEventLogger import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import kotlinx.coroutines.flow.StateFlow @@ -44,12 +46,17 @@ interface OngoingActivityChipViewModel { dialogDelegate: SystemUIDialog.Delegate, dialogTransitionAnimator: DialogTransitionAnimator, cuj: DialogCuj, + instanceId: InstanceId, + uiEventLogger: StatusBarChipsUiEventLogger, @StatusBarChipsLog logger: LogBuffer, tag: String, ): View.OnClickListener { return View.OnClickListener { view -> StatusBarChipsModernization.assertInLegacyMode() + logger.log(tag, LogLevel.INFO, {}, { "Chip clicked" }) + uiEventLogger.logChipTapToShow(instanceId) + val dialog = dialogDelegate.createDialog() val launchableView = view.requireViewById<ChipBackgroundContainer>( @@ -67,12 +74,17 @@ interface OngoingActivityChipViewModel { dialogDelegate: SystemUIDialog.Delegate, dialogTransitionAnimator: DialogTransitionAnimator, cuj: DialogCuj, + instanceId: InstanceId, + uiEventLogger: StatusBarChipsUiEventLogger, @StatusBarChipsLog logger: LogBuffer, tag: String, ): (Expandable) -> Unit { return { expandable -> StatusBarChipsModernization.unsafeAssertInNewMode() + logger.log(tag, LogLevel.INFO, {}, { "Chip clicked" }) + uiEventLogger.logChipTapToShow(instanceId) + val dialog = dialogDelegate.createDialog() val controller = expandable.dialogTransitionController(cuj) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt index 76d2af86e239..606604a090cf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt @@ -264,6 +264,7 @@ constructor( // [OngoingActivityChipModel.Active.Countdown] is the only chip without an icon and // [shouldSquish] returns false for that model, but protect against it just in case.) val currentIcon = icon ?: return this + // TODO(b/364653005): Make sure every field is copied over. return OngoingActivityChipModel.Active.IconOnly( key = key, isImportantForPrivacy = isImportantForPrivacy, @@ -271,6 +272,7 @@ constructor( colors = colors, onClickListenerLegacy = onClickListenerLegacy, clickBehavior = clickBehavior, + instanceId = instanceId, ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt index 803d422c0f0f..2d2d13ce6c27 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt @@ -100,10 +100,7 @@ class TimeRemainingState(private val timeSource: TimeSource, private val futureT /** Remember and manage the TimeRemainingState */ @Composable -fun rememberTimeRemainingState( - futureTimeMillis: Long, - timeSource: TimeSource = remember { TimeSource { System.currentTimeMillis() } }, -): TimeRemainingState { +fun rememberTimeRemainingState(futureTimeMillis: Long, timeSource: TimeSource): TimeRemainingState { val state = remember(timeSource, futureTimeMillis) { TimeRemainingState(timeSource, futureTimeMillis) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/uievents/StatusBarChipUiEvent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/uievents/StatusBarChipUiEvent.kt new file mode 100644 index 000000000000..837493016c95 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/uievents/StatusBarChipUiEvent.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.chips.uievents + +import com.android.internal.logging.UiEvent +import com.android.internal.logging.UiEventLogger + +/** All UI Events related to the status bar chips. */ +enum class StatusBarChipUiEvent(private val _id: Int) : UiEventLogger.UiEventEnum { + // New chip events, with chip type embedded in the event + @UiEvent(doc = "New status bar chip: Call") STATUS_BAR_NEW_CHIP_CALL(2211), + @UiEvent(doc = "New status bar chip: Screen record") STATUS_BAR_NEW_CHIP_SCREEN_RECORD(2212), + @UiEvent(doc = "New status bar chip: Share screen/audio to another app") + STATUS_BAR_NEW_CHIP_SHARE_TO_APP(2213), + @UiEvent(doc = "New status bar chip: Cast screen/audio to different device") + STATUS_BAR_NEW_CHIP_CAST_TO_OTHER_DEVICE(2214), + @UiEvent(doc = "New status bar chip: Promoted notification") + STATUS_BAR_NEW_CHIP_NOTIFICATION(2215), + + // Other chip events, which don't need the chip type embedded in the event because an instanceId + // should also be provided with the new event and all subsequent events + @UiEvent(doc = "A status bar chip was removed") STATUS_BAR_CHIP_REMOVED(2216), + @UiEvent(doc = "A status bar chip was tapped to show more information") + STATUS_BAR_CHIP_TAP_TO_SHOW(2217), + @UiEvent( + doc = "A status bar chip was re-tapped to hide the information that was previously shown" + ) + STATUS_BAR_CHIP_TAP_TO_HIDE(2218); + + override fun getId() = _id +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/uievents/StatusBarChipsUiEventLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/uievents/StatusBarChipsUiEventLogger.kt new file mode 100644 index 000000000000..c2349db2a188 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/uievents/StatusBarChipsUiEventLogger.kt @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.chips.uievents + +import com.android.internal.logging.InstanceId +import com.android.internal.logging.InstanceIdSequence +import com.android.internal.logging.UiEventLogger +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.chips.call.ui.viewmodel.CallChipViewModel +import com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel.CastToOtherDeviceChipViewModel +import com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.ScreenRecordChipViewModel +import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.ShareToAppChipViewModel +import com.android.systemui.statusbar.pipeline.shared.ui.model.ChipsVisibilityModel +import com.android.systemui.util.kotlin.pairwise +import javax.inject.Inject +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +/** Does all the UiEvent-related logging for the status bar chips. */ +@SysUISingleton +class StatusBarChipsUiEventLogger @Inject constructor(private val logger: UiEventLogger) { + private val instanceIdSequence = InstanceIdSequence(INSTANCE_ID_MAX) + + /** Get a new instance ID for a status bar chip. */ + fun createNewInstanceId(): InstanceId { + return instanceIdSequence.newInstanceId() + } + + /** Logs that the chip with the given ID was tapped to show additional information. */ + fun logChipTapToShow(instanceId: InstanceId?) { + logger.log(StatusBarChipUiEvent.STATUS_BAR_CHIP_TAP_TO_SHOW, instanceId) + } + + /** + * Logs that the chip with the given ID was tapped to hide the additional information that was + * previously shown. + */ + fun logChipTapToHide(instanceId: InstanceId?) { + logger.log(StatusBarChipUiEvent.STATUS_BAR_CHIP_TAP_TO_HIDE, instanceId) + } + + /** Starts UiEvent logging for the chips. */ + suspend fun hydrateUiEventLogging(chipsFlow: Flow<ChipsVisibilityModel>) { + coroutineScope { + launch { + chipsFlow + .map { it.chips } + .distinctUntilChanged() + .pairwise() + .collect { (old, new) -> + val oldActive: Map<String, Pair<InstanceId?, Int>> = + old.active.withIndex().associate { + it.value.key to Pair(it.value.instanceId, it.index) + } + val newActive: Map<String, Pair<InstanceId?, Int>> = + new.active.withIndex().associate { + it.value.key to Pair(it.value.instanceId, it.index) + } + + // Newly active keys + newActive.keys.minus(oldActive.keys).forEach { key -> + val uiEvent = key.getUiEventForNewChip() + val instanceId = newActive[key]!!.first + val position = newActive[key]!!.second + logger.logWithInstanceIdAndPosition( + uiEvent, + /* uid= */ 0, + /* packageName= */ null, + instanceId, + position, + ) + } + + // Newly inactive keys + oldActive.keys.minus(newActive.keys).forEach { key -> + val instanceId = oldActive[key]?.first + logger.log(StatusBarChipUiEvent.STATUS_BAR_CHIP_REMOVED, instanceId) + } + } + } + } + } + + companion object { + private const val INSTANCE_ID_MAX = 1 shl 20 + + /** + * Given a key from an [OngoingActivityChipModel.Active] instance that was just added, + * return the right UiEvent type to log. + */ + private fun String.getUiEventForNewChip(): StatusBarChipUiEvent { + return when { + this == ScreenRecordChipViewModel.KEY -> + StatusBarChipUiEvent.STATUS_BAR_NEW_CHIP_SCREEN_RECORD + this == ShareToAppChipViewModel.KEY -> + StatusBarChipUiEvent.STATUS_BAR_NEW_CHIP_SHARE_TO_APP + this == CastToOtherDeviceChipViewModel.KEY -> + StatusBarChipUiEvent.STATUS_BAR_NEW_CHIP_CAST_TO_OTHER_DEVICE + this.startsWith(CallChipViewModel.KEY_PREFIX) -> + StatusBarChipUiEvent.STATUS_BAR_NEW_CHIP_CALL + else -> StatusBarChipUiEvent.STATUS_BAR_NEW_CHIP_NOTIFICATION + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt index df8fb5e75368..afbec7f356b2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt @@ -71,46 +71,49 @@ constructor( Notification.MessagingStyle.CONVERSATION_TYPE_IMPORTANT else if (entry.ranking.isConversation) Notification.MessagingStyle.CONVERSATION_TYPE_NORMAL - else - Notification.MessagingStyle.CONVERSATION_TYPE_LEGACY + else Notification.MessagingStyle.CONVERSATION_TYPE_LEGACY entry.ranking.conversationShortcutInfo?.let { shortcutInfo -> logger.logAsyncTaskProgress(entry.logKey, "getting shortcut icon") messagingStyle.shortcutIcon = launcherApps.getShortcutIcon(shortcutInfo) shortcutInfo.label?.let { label -> messagingStyle.conversationTitle = label } } - if (NmSummarizationUiFlag.isEnabled && !TextUtils.isEmpty(entry.ranking.summarization)) { - val icon = context.getDrawable(R.drawable.ic_notification_summarization)?.mutate() - val imageSpan = - icon?.let { - it.setBounds( - /* left= */ 0, - /* top= */ 0, - icon.getIntrinsicWidth(), - icon.getIntrinsicHeight(), - ) - ImageSpan(it, ImageSpan.ALIGN_CENTER) - } - val decoratedSummary = - SpannableString("x " + entry.ranking.summarization).apply { - setSpan( - /* what = */ imageSpan, - /* start = */ 0, - /* end = */ 1, - /* flags = */ Spanned.SPAN_INCLUSIVE_EXCLUSIVE, - ) - entry.ranking.summarization?.let { + if (NmSummarizationUiFlag.isEnabled) { + if (!TextUtils.isEmpty(entry.ranking.summarization)) { + val icon = context.getDrawable(R.drawable.ic_notification_summarization)?.mutate() + val imageSpan = + icon?.let { + it.setBounds( + /* left= */ 0, + /* top= */ 0, + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight(), + ) + ImageSpan(it, ImageSpan.ALIGN_CENTER) + } + val decoratedSummary = + SpannableString("x " + entry.ranking.summarization).apply { setSpan( - /* what = */ StyleSpan(Typeface.ITALIC), - /* start = */ 2, - /* end = */ it.length + 2, - /* flags = */ Spanned.SPAN_EXCLUSIVE_INCLUSIVE, + /* what = */ imageSpan, + /* start = */ 0, + /* end = */ 1, + /* flags = */ Spanned.SPAN_INCLUSIVE_EXCLUSIVE, ) + entry.ranking.summarization?.let { + setSpan( + /* what = */ StyleSpan(Typeface.ITALIC), + /* start = */ 2, + /* end = */ it.length + 2, + /* flags = */ Spanned.SPAN_EXCLUSIVE_INCLUSIVE, + ) + } } - } - entry.sbn.notification.extras.putCharSequence( - EXTRA_SUMMARIZED_CONTENT, - decoratedSummary, - ) + entry.sbn.notification.extras.putCharSequence( + EXTRA_SUMMARIZED_CONTENT, + decoratedSummary, + ) + } else { + entry.sbn.notification.extras.putCharSequence(EXTRA_SUMMARIZED_CONTENT, null) + } } messagingStyle.unreadMessageCount = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS b/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS index ba4001014681..e19ffb4f2018 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS @@ -17,4 +17,4 @@ valiiftime@google.com yurilin@google.com per-file MediaNotificationProcessor.java = ethibodeau@google.com -per-file MagicActionBackgroundDrawable.kt = dupin@google.com +per-file AnimatedActionBackgroundDrawable.kt = dupin@google.com diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimator.kt index 68c13afe82dc..7b0d90ca3b60 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimator.kt @@ -24,14 +24,19 @@ import com.android.internal.dynamicanimation.animation.SpringForce import com.android.systemui.res.R import com.android.systemui.statusbar.notification.PhysicsPropertyAnimator.Companion.createDefaultSpring import com.android.systemui.statusbar.notification.stack.AnimationProperties +import kotlin.math.sign /** * A physically animatable property of a view. * * @param tag the view tag to safe this property in * @param property the property to animate. + * @param avoidDoubleOvershoot should this property avoid double overshoot when animated */ -data class PhysicsProperty(val tag: Int, val property: Property<View, Float>) { +data class PhysicsProperty +@JvmOverloads constructor( + val tag: Int, val property: Property<View, Float>, val avoidDoubleOvershoot: Boolean = true +) { val offsetProperty = object : FloatProperty<View>(property.name) { override fun get(view: View): Float { @@ -61,6 +66,8 @@ data class PropertyData( var offset: Float = 0f, var animator: SpringAnimation? = null, var delayRunnable: Runnable? = null, + var startOffset: Float = 0f, + var doubleOvershootAvoidingListener: DynamicAnimation.OnAnimationUpdateListener? = null ) /** @@ -140,30 +147,67 @@ private fun startAnimation( if (animator == null) { animator = SpringAnimation(view, animatableProperty.offsetProperty) propertyData.animator = animator - animator.setSpring(createDefaultSpring()) val listener = properties?.getAnimationEndListener(animatableProperty.property) if (listener != null) { animator.addEndListener(listener) - // We always notify things as started even if we have a delay - properties.getAnimationStartListener(animatableProperty.property)?.accept(animator) } + // We always notify things as started even if we have a delay + properties?.getAnimationStartListener(animatableProperty.property)?.accept(animator) // remove the tag when the animation is finished - animator.addEndListener { _, _, _, _ -> propertyData.animator = null } + animator.addEndListener { _, _, _, _ -> + propertyData.animator = null + propertyData.doubleOvershootAvoidingListener = null + // Let's make sure we never get stuck with an offset even when canceling + // We never actually cancel running animations but keep it around, so this only + // triggers if things really should end. + propertyData.offset = 0f + } + } + if (animatableProperty.avoidDoubleOvershoot + && propertyData.doubleOvershootAvoidingListener == null) { + propertyData.doubleOvershootAvoidingListener = + DynamicAnimation.OnAnimationUpdateListener { _, offset: Float, velocity: Float -> + val isOscillatingBackwards = velocity.sign == propertyData.startOffset.sign + val didAlreadyRemoveBounciness = + animator.spring.dampingRatio == SpringForce.DAMPING_RATIO_NO_BOUNCY + val isOvershooting = offset.sign != propertyData.startOffset.sign + if (isOvershooting && isOscillatingBackwards && !didAlreadyRemoveBounciness) { + // our offset is starting to decrease, let's remove all overshoot + animator.spring.setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY) + } else if (!isOvershooting + && (didAlreadyRemoveBounciness || isOscillatingBackwards)) { + // we already did overshoot, let's skip to the end to avoid oscillations. + // Usually we shouldn't hit this as setting the damping ratio avoid overshoots + // but it may still happen if we see jank + animator.skipToEnd(); + } + } + animator.addUpdateListener(propertyData.doubleOvershootAvoidingListener) + } else if (!animatableProperty.avoidDoubleOvershoot + && propertyData.doubleOvershootAvoidingListener != null) { + animator.removeUpdateListener(propertyData.doubleOvershootAvoidingListener) } + // reset a new spring as it may have been modified + animator.setSpring(createDefaultSpring().setFinalPosition(0f)) // TODO(b/393581344): look at custom spring endListener?.let { animator.addEndListener(it) } - val newOffset = previousEndValue - newEndValue + propertyData.offset - // Immedialely set the new offset that compensates for the immediate end value change - propertyData.offset = newOffset - property.set(view, newEndValue + newOffset) + val startOffset = previousEndValue - newEndValue + propertyData.offset + // Immediately set the new offset that compensates for the immediate end value change + propertyData.offset = startOffset + propertyData.startOffset = startOffset + property.set(view, newEndValue + startOffset) // cancel previous starters still pending view.removeCallbacks(propertyData.delayRunnable) - animator.setStartValue(newOffset) + animator.setStartValue(startOffset) val startRunnable = Runnable { animator.animateToFinalPosition(0f) propertyData.delayRunnable = null + // When setting a new spring on a running animation it doesn't properly set the finish + // conditions and will never actually end them only calling start explicitly does that, + // so let's start them again! + animator.start() } if (properties != null && properties.delay > 0 && !animator.isRunning) { propertyData.delayRunnable = startRunnable diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java index 8fc6cbe7c9e7..e69de29bb2d1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.notification.collection; - -import static android.app.NotificationChannel.NEWS_ID; -import static android.app.NotificationChannel.PROMOTIONS_ID; -import static android.app.NotificationChannel.RECS_ID; -import static android.app.NotificationChannel.SOCIAL_MEDIA_ID; - -import android.app.Notification; -import android.content.Context; -import android.os.Build; -import android.service.notification.StatusBarNotification; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -import com.android.systemui.statusbar.notification.icon.IconPack; -import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection; -import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import kotlinx.coroutines.flow.MutableStateFlow; -import kotlinx.coroutines.flow.StateFlow; -import kotlinx.coroutines.flow.StateFlowKt; - -/** - * Class to represent notifications bundled by classification. - */ -public class BundleEntry extends PipelineEntry { - - // TODO(b/394483200): move NotificationEntry's implementation to PipelineEntry? - private final MutableStateFlow<Boolean> mSensitive = StateFlowKt.MutableStateFlow(false); - - // TODO (b/389839319): implement the row - private ExpandableNotificationRow mRow; - - private final List<ListEntry> mChildren = new ArrayList<>(); - - private final List<ListEntry> mUnmodifiableChildren = Collections.unmodifiableList(mChildren); - - public BundleEntry(String key) { - super(key); - } - - void addChild(ListEntry child) { - mChildren.add(child); - } - - @NonNull - public List<ListEntry> getChildren() { - return mUnmodifiableChildren; - } - - void clearChildren() { - mChildren.clear(); - } - - /** - * @return Null because bundles do not have an associated NotificationEntry. - */ - @Nullable - @Override - public NotificationEntry getRepresentativeEntry() { - return null; - } - - @Nullable - @Override - public PipelineEntry getParent() { - return null; - } - - @Override - public boolean wasAttachedInPreviousPass() { - return false; - } - - @Nullable - public ExpandableNotificationRow getRow() { - return mRow; - } - - public static final List<BundleEntry> ROOT_BUNDLES = List.of( - new BundleEntry(PROMOTIONS_ID), - new BundleEntry(SOCIAL_MEDIA_ID), - new BundleEntry(NEWS_ID), - new BundleEntry(RECS_ID)); - - public MutableStateFlow<Boolean> isSensitive() { - return mSensitive; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.kt new file mode 100644 index 000000000000..0da76c333a1f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.notification.collection + +import android.app.NotificationChannel +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import java.util.Collections +import kotlinx.coroutines.flow.MutableStateFlow + +/** Class to represent notifications bundled by classification. */ +class BundleEntry(key: String) : PipelineEntry(key) { + // TODO(b/394483200): move NotificationEntry's implementation to PipelineEntry? + val isSensitive: MutableStateFlow<Boolean> = MutableStateFlow(false) + + // TODO (b/389839319): implement the row + val row: ExpandableNotificationRow? = null + + private val _children: MutableList<ListEntry> = ArrayList() + val children: List<ListEntry> = Collections.unmodifiableList(_children) + + fun addChild(child: ListEntry) { + _children.add(child) + } + + fun clearChildren() { + _children.clear() + } + + /** @return Null because bundles do not have an associated NotificationEntry. */ + override fun getRepresentativeEntry(): NotificationEntry? { + return null + } + + override fun getParent(): PipelineEntry? { + return null + } + + override fun wasAttachedInPreviousPass(): Boolean { + return false + } + + companion object { + val ROOT_BUNDLES: List<BundleEntry> = + listOf( + BundleEntry(NotificationChannel.PROMOTIONS_ID), + BundleEntry(NotificationChannel.SOCIAL_MEDIA_ID), + BundleEntry(NotificationChannel.NEWS_ID), + BundleEntry(NotificationChannel.RECS_ID), + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt index 6a3f8f166c34..98714aeb1248 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt @@ -33,6 +33,11 @@ class BundleEntryAdapter( private val highPriorityProvider: HighPriorityProvider, val entry: BundleEntry, ) : EntryAdapter { + + override fun getBackingHashCode(): Int { + return entry.hashCode() + } + /** TODO (b/394483200): convert to PipelineEntry.ROOT_ENTRY when pipeline is migrated? */ override fun getParent(): GroupEntry { return GroupEntry.ROOT_ENTRY diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java index 16d9c787d435..43ae4d9296c7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java @@ -36,6 +36,11 @@ import kotlinx.coroutines.flow.StateFlow; public interface EntryAdapter { /** + * Returns the hash code of the backing entry + */ + int getBackingHashCode(); + + /** * Gets the parent of this entry, or null if the entry's view is not attached */ @Nullable PipelineEntry getParent(); @@ -195,5 +200,6 @@ public interface EntryAdapter { NotificationEntry.DismissState getDismissState(); void onEntryClicked(ExpandableNotificationRow row); + } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryWithDismissStats.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryWithDismissStats.kt index 48a8c01e7c47..e37a210c1c2a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryWithDismissStats.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryWithDismissStats.kt @@ -23,7 +23,10 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.Di * A holder class for a [NotificationEntry] and an associated [DismissedByUserStats], used by * [NotifCollection] for handling dismissal. */ -data class EntryWithDismissStats(val entry: NotificationEntry, val stats: DismissedByUserStats) { +data class EntryWithDismissStats(val entry: NotificationEntry?, + val stats: DismissedByUserStats, + val key: String, + val entryHashCode: Int) { /** * Creates deep a copy of this object, but with the entry, key and rank updated to correspond to * the given entry. @@ -42,5 +45,7 @@ data class EntryWithDismissStats(val entry: NotificationEntry, val stats: Dismis /* visible= */ false, ), ), + key = newEntry.key, + entryHashCode = newEntry.hashCode() ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java index b7fe39e9c757..10d7b9cce559 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java @@ -98,6 +98,7 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.Ra import com.android.systemui.statusbar.notification.collection.notifcollection.RankingUpdatedEvent; import com.android.systemui.statusbar.notification.collection.notifcollection.UpdateSource; import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.util.Assert; import com.android.systemui.util.NamedListenerSet; import com.android.systemui.util.time.SystemClock; @@ -283,53 +284,55 @@ public class NotifCollection implements Dumpable, PipelineDumpable { final int entryCount = entriesToDismiss.size(); final List<NotificationEntry> entriesToLocallyDismiss = new ArrayList<>(); for (int i = 0; i < entriesToDismiss.size(); i++) { - NotificationEntry entry = entriesToDismiss.get(i).getEntry(); + String key = entriesToDismiss.get(i).getKey(); + int hashCode = entriesToDismiss.get(i).getEntryHashCode(); DismissedByUserStats stats = entriesToDismiss.get(i).getStats(); requireNonNull(stats); - NotificationEntry storedEntry = mNotificationSet.get(entry.getKey()); + NotificationEntry storedEntry = mNotificationSet.get(key); if (storedEntry == null) { - mLogger.logDismissNonExistentNotif(entry, i, entryCount); + mLogger.logDismissNonExistentNotif(key, i, entryCount); continue; } - if (entry != storedEntry) { + if (hashCode != storedEntry.hashCode()) { throw mEulogizer.record( new IllegalStateException("Invalid entry: " - + "different stored and dismissed entries for " + logKey(entry) + + "different stored and dismissed entries for " + logKey(key) + " (" + i + "/" + entryCount + ")" - + " dismissed=@" + Integer.toHexString(entry.hashCode()) + + " dismissed=@" + Integer.toHexString(hashCode) + " stored=@" + Integer.toHexString(storedEntry.hashCode()))); } - if (entry.getDismissState() == DISMISSED) { - mLogger.logDismissAlreadyDismissedNotif(entry, i, entryCount); + if (storedEntry.getDismissState() == DISMISSED) { + mLogger.logDismissAlreadyDismissedNotif(storedEntry, i, entryCount); continue; - } else if (entry.getDismissState() == PARENT_DISMISSED) { - mLogger.logDismissAlreadyParentDismissedNotif(entry, i, entryCount); + } else if (storedEntry.getDismissState() == PARENT_DISMISSED) { + mLogger.logDismissAlreadyParentDismissedNotif(storedEntry, i, entryCount); } - updateDismissInterceptors(entry); - if (isDismissIntercepted(entry)) { - mLogger.logNotifDismissedIntercepted(entry, i, entryCount); + updateDismissInterceptors(storedEntry); + if (isDismissIntercepted(storedEntry)) { + mLogger.logNotifDismissedIntercepted(storedEntry, i, entryCount); continue; } - entriesToLocallyDismiss.add(entry); - if (!entry.isCanceled()) { + entriesToLocallyDismiss.add(storedEntry); + if (!storedEntry.isCanceled()) { int finalI = i; // send message to system server if this notification hasn't already been cancelled mBgExecutor.execute(() -> { try { mStatusBarService.onNotificationClear( - entry.getSbn().getPackageName(), - entry.getSbn().getUser().getIdentifier(), - entry.getSbn().getKey(), + storedEntry.getSbn().getPackageName(), + storedEntry.getSbn().getUser().getIdentifier(), + storedEntry.getSbn().getKey(), stats.dismissalSurface, stats.dismissalSentiment, stats.notificationVisibility); } catch (RemoteException e) { // system process is dead if we're here. - mLogger.logRemoteExceptionOnNotificationClear(entry, finalI, entryCount, e); + mLogger.logRemoteExceptionOnNotificationClear( + storedEntry, finalI, entryCount, e); } }); } @@ -343,28 +346,43 @@ public class NotifCollection implements Dumpable, PipelineDumpable { List<EntryWithDismissStats> entriesToDismiss) { final HashSet<NotificationEntry> entriesSet = new HashSet<>(entriesToDismiss.size()); for (EntryWithDismissStats entryToStats : entriesToDismiss) { - entriesSet.add(entryToStats.getEntry()); + NotificationEntry entry = getEntryFromDismissalStats(entryToStats); + if (entry != null) { + entriesSet.add(entry); + } } final List<EntryWithDismissStats> entriesPlusSummaries = new ArrayList<>(entriesToDismiss.size() + 1); for (EntryWithDismissStats entryToStats : entriesToDismiss) { entriesPlusSummaries.add(entryToStats); - NotificationEntry summary = fetchSummaryToDismiss(entryToStats.getEntry()); - if (summary != null && !entriesSet.contains(summary)) { - entriesPlusSummaries.add(entryToStats.copyForEntry(summary)); + NotificationEntry entry = getEntryFromDismissalStats(entryToStats); + if (entry != null) { + NotificationEntry summary = fetchSummaryToDismiss(entry); + if (summary != null && !entriesSet.contains(summary)) { + entriesPlusSummaries.add(entryToStats.copyForEntry(summary)); + } } } return entriesPlusSummaries; } + private NotificationEntry getEntryFromDismissalStats(EntryWithDismissStats stats) { + if (NotificationBundleUi.isEnabled()) { + return mNotificationSet.get(stats.getKey()); + } else { + return stats.getEntry(); + } + } + /** * Dismisses a single notification on behalf of the user. */ public void dismissNotification( NotificationEntry entry, @NonNull DismissedByUserStats stats) { - dismissNotifications(List.of(new EntryWithDismissStats(entry, stats))); + dismissNotifications(List.of(new EntryWithDismissStats( + entry, stats, entry.getKey(), entry.hashCode()))); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt index 339a999e1535..b8b4e9886c66 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt @@ -44,6 +44,9 @@ class NotificationEntryAdapter( private val headsUpManager: HeadsUpManager, private val entry: NotificationEntry, ) : EntryAdapter { + override fun getBackingHashCode(): Int { + return entry.hashCode() + } override fun getParent(): PipelineEntry? { return entry.parent diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index 26b86f9ed74d..a40a2285d8a2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -15,18 +15,19 @@ */ package com.android.systemui.statusbar.notification.collection.coordinator -import com.android.systemui.Flags.notificationSkipSilentUpdates - import android.app.Notification import android.app.Notification.GROUP_ALERT_SUMMARY +import android.app.NotificationChannel.SYSTEM_RESERVED_IDS import android.util.ArrayMap import android.util.ArraySet import com.android.internal.annotations.VisibleForTesting +import com.android.systemui.Flags.notificationSkipSilentUpdates import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.NotificationRemoteInputManager import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips +import com.android.systemui.statusbar.chips.uievents.StatusBarChipsUiEventLogger import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.BundleEntry import com.android.systemui.statusbar.notification.collection.GroupEntry @@ -48,7 +49,10 @@ import com.android.systemui.statusbar.notification.headsup.HeadsUpManager import com.android.systemui.statusbar.notification.headsup.OnHeadsUpChangedListener import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionLogger import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProviderImpl.DecisionImpl +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType import com.android.systemui.statusbar.notification.logKey import com.android.systemui.statusbar.notification.row.NotificationActionClickManager import com.android.systemui.statusbar.notification.shared.GroupHunAnimationFix @@ -81,6 +85,7 @@ class HeadsUpCoordinator constructor( @Application private val applicationScope: CoroutineScope, private val mLogger: HeadsUpCoordinatorLogger, + private val mInterruptLogger: VisualInterruptionDecisionLogger, private val mSystemClock: SystemClock, private val notifCollection: NotifCollection, private val mHeadsUpManager: HeadsUpManager, @@ -91,6 +96,7 @@ constructor( private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider, private val mFlags: NotifPipelineFlags, private val statusBarNotificationChipsInteractor: StatusBarNotificationChipsInteractor, + private val statusBarChipsUiEventLogger: StatusBarChipsUiEventLogger, @IncomingHeader private val mIncomingHeaderController: NodeController, @Main private val mExecutor: DelayableExecutor, ) : Coordinator { @@ -146,6 +152,14 @@ constructor( // not just any notification. val isCurrentlyHeadsUp = mHeadsUpManager.isHeadsUpEntry(entry.key) + + if (isCurrentlyHeadsUp) { + // If the chip's notif is currently showing as heads up, then we'll stop showing it. + statusBarChipsUiEventLogger.logChipTapToHide(entry.sbn.instanceId) + } else { + statusBarChipsUiEventLogger.logChipTapToShow(entry.sbn.instanceId) + } + val posted = PostedEntry( entry, @@ -281,6 +295,19 @@ constructor( return@forEach } + if (isDisqualifiedChild(childToReceiveParentHeadsUp)) { + mInterruptLogger.logDecision( + VisualInterruptionType.PEEK.name, + childToReceiveParentHeadsUp, + DecisionImpl(shouldInterrupt = false, + logReason = "disqualified-transfer-target")) + postedEntries.forEach { + it.shouldHeadsUpEver = false + it.shouldHeadsUpAgain = false + handlePostedEntry(it, hunMutator, scenario = "disqualified-transfer-target") + } + return@forEach + } // At this point we just need to initiate the transfer val summaryUpdate = mPostedEntries[logicalSummary.key] @@ -383,6 +410,14 @@ constructor( cleanUpEntryTimes() } + private fun isDisqualifiedChild(entry: NotificationEntry): Boolean { + if (entry.channel == null || entry.channel.id == null) { + return false + } + return entry.channel.id in SYSTEM_RESERVED_IDS + } + + /** * Find the posted child with the newest when, and return it if it is isolated and has * GROUP_ALERT_SUMMARY so that it can be heads uped. @@ -478,8 +513,10 @@ constructor( // instead of waiting for any sort of minimum timeout. // TODO(b/401068530) Ensure that status bar chip HUNs are not // removed for silent update - hunMutator.removeNotification(posted.key, - /* releaseImmediately= */ true) + hunMutator.removeNotification( + posted.key, + /* releaseImmediately= */ true, + ) } else { // Do NOT remove HUN for non-user update. // Let the HUN show for its remaining duration. @@ -596,8 +633,11 @@ constructor( // TODO(b/403703828) Move canceling to OnBeforeFinalizeFilter, since we are not // removing from HeadsUpManager and don't need to deal with re-entrant behavior // between HeadsUpCoordinator, HeadsUpManager, and VisualStabilityManager. - if (posted?.shouldHeadsUpEver == false - && !posted.isHeadsUpEntry && posted.isBinding) { + if ( + posted?.shouldHeadsUpEver == false && + !posted.isHeadsUpEntry && + posted.isBinding + ) { // Don't let the bind finish cancelHeadsUpBind(posted.entry) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java index 6b32c6a18ec0..a0b3c1729154 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java @@ -298,6 +298,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { mRowContentBindStage.requestRebind(entry, en -> { mLogger.logRebindComplete(entry); row.setIsMinimized(isMinimized); + row.setRedactionType(redactionType); if (inflationCallback != null) { inflationCallback.onAsyncInflationFinished(en); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt index ae2c70a284e9..cfd42d5a5cae 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt @@ -119,9 +119,9 @@ class NotifCollectionLogger @Inject constructor( }) } - fun logDismissNonExistentNotif(entry: NotificationEntry, index: Int, count: Int) { + fun logDismissNonExistentNotif(entryKey: String, index: Int, count: Int) { buffer.log(TAG, INFO, { - str1 = entry.logKey + str1 = logKey(entryKey) int1 = index int2 = count }, { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt index e7cc342ab65c..9f42a1d9b145 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt @@ -27,6 +27,7 @@ import android.graphics.drawable.Icon import android.service.notification.StatusBarNotification import android.util.ArrayMap import com.android.app.tracing.traceSection +import com.android.internal.logging.InstanceId import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.collection.GroupEntry @@ -167,7 +168,7 @@ private class ActiveNotificationsStoreBuilder( packageName = sbn.packageName, appName = sbn.notification.loadHeaderAppName(context), contentIntent = sbn.notification.contentIntent, - instanceId = sbn.instanceId?.id, + instanceId = sbn.instanceId, isGroupSummary = sbn.notification.isGroupSummary, bucket = bucket, callType = sbn.toCallType(), @@ -196,7 +197,7 @@ private fun ActiveNotificationsStore.createOrReuse( packageName: String, appName: String, contentIntent: PendingIntent?, - instanceId: Int?, + instanceId: InstanceId?, isGroupSummary: Boolean, bucket: Int, callType: CallType, @@ -278,7 +279,7 @@ private fun ActiveNotificationModel.isCurrent( packageName: String, appName: String, contentIntent: PendingIntent?, - instanceId: Int?, + instanceId: InstanceId?, isGroupSummary: Boolean, bucket: Int, callType: CallType, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt index 8240a0abaa9d..9f67e50ef920 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt @@ -87,7 +87,7 @@ constructor( val eventLogData: EventLogData? } - private class DecisionImpl( + class DecisionImpl( override val shouldInterrupt: Boolean, override val logReason: String, ) : Decision diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java index 175512336b8e..bd06a375d5e6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java @@ -33,6 +33,7 @@ import android.service.notification.StatusBarNotification; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.logging.nano.Notifications; import com.android.systemui.statusbar.notification.stack.PriorityBucket; @@ -64,6 +65,8 @@ public interface NotificationPanelLogger { */ void logNotificationDrag(NotificationEntry draggedNotification); + void logNotificationDrag(EntryAdapter draggedNotification); + enum NotificationPanelEvent implements UiEventLogger.UiEventEnum { @UiEvent(doc = "Notification panel shown from status bar.") NOTIFICATION_PANEL_OPEN_STATUS_BAR(200), @@ -123,6 +126,42 @@ public interface NotificationPanelLogger { } /** + * Composes a NotificationsList proto from the list of visible notifications. + * @param visibleNotifications as provided by NotificationEntryManager.getVisibleNotifications() + * @return NotificationList proto suitable for SysUiStatsLog.write(NOTIFICATION_PANEL_REPORTED) + */ + static Notifications.NotificationList adapterToNotificationProto( + @Nullable List<EntryAdapter> visibleNotifications) { + Notifications.NotificationList notificationList = new Notifications.NotificationList(); + if (visibleNotifications == null) { + return notificationList; + } + final Notifications.Notification[] proto_array = + new Notifications.Notification[visibleNotifications.size()]; + int i = 0; + for (EntryAdapter ne : visibleNotifications) { + final StatusBarNotification n = ne.getSbn(); + if (n != null) { + final Notifications.Notification proto = new Notifications.Notification(); + proto.uid = n.getUid(); + proto.packageName = n.getPackageName(); + if (n.getInstanceId() != null) { + proto.instanceId = n.getInstanceId().getId(); + } + // TODO set np.groupInstanceId + if (n.getNotification() != null) { + proto.isGroupSummary = n.getNotification().isGroupSummary(); + } + proto.section = toNotificationSection(ne.getSectionBucket()); + proto_array[i] = proto; + } + ++i; + } + notificationList.notifications = proto_array; + return notificationList; + } + + /** * Maps PriorityBucket enum to Notification.SECTION constant. The two lists should generally * use matching names, but the values may differ, because PriorityBucket order changes from * time to time, while logs need to have stable meanings. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java index d7f7b760dd04..7eb74a4bf83d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.logging; import static com.android.systemui.statusbar.notification.logging.NotificationPanelLogger.NotificationPanelEvent.NOTIFICATION_DRAG; import com.android.systemui.shared.system.SysUiStatsLog; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.logging.nano.Notifications; import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor; @@ -62,4 +63,15 @@ public class NotificationPanelLoggerImpl implements NotificationPanelLogger { /* num_notifications = */ proto.notifications.length, /* notifications = */ MessageNano.toByteArray(proto)); } + + @Override + public void logNotificationDrag(EntryAdapter draggedNotification) { + final Notifications.NotificationList proto = + NotificationPanelLogger.adapterToNotificationProto( + Collections.singletonList(draggedNotification)); + SysUiStatsLog.write(SysUiStatsLog.NOTIFICATION_PANEL_REPORTED, + /* event_id = */ NOTIFICATION_DRAG.getId(), + /* num_notifications = */ proto.notifications.length, + /* notifications = */ MessageNano.toByteArray(proto)); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt index fdbd75bd33ca..2238db505948 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt @@ -16,13 +16,13 @@ package com.android.systemui.statusbar.notification.promoted -import android.app.Flags import android.app.Flags.notificationsRedesignTemplates import android.app.Notification import android.content.Context import android.graphics.PorterDuff import android.util.Log import android.view.LayoutInflater +import android.view.NotificationTopLineView import android.view.View import android.view.View.GONE import android.view.View.MeasureSpec.AT_MOST @@ -81,7 +81,7 @@ fun AODPromotedNotification( viewModelFactory: AODPromotedNotificationViewModel.Factory, modifier: Modifier = Modifier, ) { - if (!PromotedNotificationUiAod.isEnabled) { + if (!PromotedNotificationUi.isEnabled) { return } @@ -170,24 +170,35 @@ private class FrameLayoutWithMaxHeight(maxHeight: Int, context: Context) : Frame // This mirrors the logic in NotificationContentView.onMeasure. override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - if (childCount < 1) { - return + if (childCount != 1) { + Log.wtf(TAG, "Should contain exactly one child.") + return super.onMeasure(widthMeasureSpec, heightMeasureSpec) } - val child = getChildAt(0) - val childLayoutHeight = child.layoutParams.height - val childHeightSpec = - if (childLayoutHeight >= 0) { - makeMeasureSpec(maxHeight.coerceAtMost(childLayoutHeight), EXACTLY) - } else { - makeMeasureSpec(maxHeight, AT_MOST) - } - measureChildWithMargins(child, widthMeasureSpec, 0, childHeightSpec, 0) - val childMeasuredHeight = child.measuredHeight + val horizPadding = paddingStart + paddingEnd + val vertPadding = paddingTop + paddingBottom + val ownWidthSize = MeasureSpec.getSize(widthMeasureSpec) val ownHeightMode = MeasureSpec.getMode(heightMeasureSpec) val ownHeightSize = MeasureSpec.getSize(heightMeasureSpec) + val availableHeight = + if (ownHeightMode != UNSPECIFIED) { + maxHeight.coerceAtMost(ownHeightSize) + } else { + maxHeight + } + + val child = getChildAt(0) + val childWidthSpec = makeMeasureSpec(ownWidthSize, EXACTLY) + val childHeightSpec = + child.layoutParams.height + .takeIf { it >= 0 } + ?.let { makeMeasureSpec(availableHeight.coerceAtMost(it), EXACTLY) } + ?: run { makeMeasureSpec(availableHeight, AT_MOST) } + measureChildWithMargins(child, childWidthSpec, horizPadding, childHeightSpec, vertPadding) + val childMeasuredHeight = child.measuredHeight + val ownMeasuredWidth = MeasureSpec.getSize(widthMeasureSpec) val ownMeasuredHeight = if (ownHeightMode != UNSPECIFIED) { @@ -195,28 +206,31 @@ private class FrameLayoutWithMaxHeight(maxHeight: Int, context: Context) : Frame } else { childMeasuredHeight } - setMeasuredDimension(ownMeasuredWidth, ownMeasuredHeight) } } private val PromotedNotificationContentModel.layoutResource: Int? get() { - return if (Flags.notificationsRedesignTemplates()) { + return if (notificationsRedesignTemplates()) { when (style) { Style.Base -> R.layout.notification_2025_template_expanded_base + Style.CollapsedBase -> R.layout.notification_2025_template_collapsed_base Style.BigPicture -> R.layout.notification_2025_template_expanded_big_picture Style.BigText -> R.layout.notification_2025_template_expanded_big_text Style.Call -> R.layout.notification_2025_template_expanded_call + Style.CollapsedCall -> R.layout.notification_2025_template_collapsed_call Style.Progress -> R.layout.notification_2025_template_expanded_progress Style.Ineligible -> null } } else { when (style) { Style.Base -> R.layout.notification_template_material_big_base + Style.CollapsedBase -> R.layout.notification_template_material_base Style.BigPicture -> R.layout.notification_template_material_big_picture Style.BigText -> R.layout.notification_template_material_big_text Style.Call -> R.layout.notification_template_material_big_call + Style.CollapsedCall -> R.layout.notification_template_material_call Style.Progress -> R.layout.notification_template_material_progress Style.Ineligible -> null } @@ -258,6 +272,7 @@ private class AODPromotedNotificationViewUpdater(root: View) { private val time: DateTimeView? = root.findViewById(R.id.time) private val timeDivider: TextView? = root.findViewById(R.id.time_divider) private val title: TextView? = root.findViewById(R.id.title) + private val topLine: NotificationTopLineView? = root.findViewById(R.id.notification_top_line) private val verificationDivider: TextView? = root.findViewById(R.id.verification_divider) private val verificationIcon: ImageView? = root.findViewById(R.id.verification_icon) private val verificationText: TextView? = root.findViewById(R.id.verification_text) @@ -266,6 +281,29 @@ private class AODPromotedNotificationViewUpdater(root: View) { private var oldProgressBar: ProgressBar? = null private val newProgressBar = root.findViewById<View>(R.id.progress) as? NotificationProgressBar + private val largeIconSizePx: Int = + root.context.resources.getDimensionPixelSize(R.dimen.notification_right_icon_size) + + private val endMarginPx: Int = + if (notificationsRedesignTemplates()) { + root.context.resources.getDimensionPixelSize(R.dimen.notification_2025_margin) + } else { + root.context.resources.getDimensionPixelSize( + systemuiR.dimen.notification_shade_content_margin_horizontal + ) + } + + private val imageEndMarginPx: Int + get() = largeIconSizePx + 2 * endMarginPx + + private val PromotedNotificationContentModel.imageEndMarginPxIfHasLargeIcon: Int + get() = + if (!skeletonLargeIcon.isNullOrEmpty()) { + imageEndMarginPx + } else { + 0 + } + init { // Hide views that are never visible in the skeleton promoted notification. alternateExpandTarget?.visibility = GONE @@ -283,13 +321,20 @@ private class AODPromotedNotificationViewUpdater(root: View) { ?.mutate() ?.setColorFilter(SecondaryText.colorInt, PorterDuff.Mode.SRC_IN) + (rightIcon?.layoutParams as? MarginLayoutParams)?.let { + it.marginEnd = endMarginPx + rightIcon.layoutParams = it + } + bigText?.setImageEndMargin(largeIconSizePx + endMarginPx) + text?.setImageEndMargin(largeIconSizePx + endMarginPx) + setTextViewColor(appNameDivider, SecondaryText) setTextViewColor(headerTextDivider, SecondaryText) setTextViewColor(headerTextSecondaryDivider, SecondaryText) setTextViewColor(timeDivider, SecondaryText) setTextViewColor(verificationDivider, SecondaryText) - if (Flags.notificationsRedesignTemplates()) { + if (notificationsRedesignTemplates()) { (mainColumn?.layoutParams as? MarginLayoutParams)?.let { mainColumnMargins -> mainColumnMargins.topMargin = Notification.Builder.getContentMarginTop( @@ -302,10 +347,12 @@ private class AODPromotedNotificationViewUpdater(root: View) { fun update(content: PromotedNotificationContentModel, audiblyAlertedIconVisible: Boolean) { when (content.style) { - Style.Base -> updateBase(content) + Style.Base -> updateBase(content, collapsed = false) + Style.CollapsedBase -> updateBase(content, collapsed = true) Style.BigPicture -> updateBigPictureStyle(content) Style.BigText -> updateBigTextStyle(content) - Style.Call -> updateCallStyle(content) + Style.Call -> updateCallStyle(content, collapsed = false) + Style.CollapsedCall -> updateCallStyle(content, collapsed = true) Style.Progress -> updateProgressStyle(content) Style.Ineligible -> {} } @@ -315,44 +362,45 @@ private class AODPromotedNotificationViewUpdater(root: View) { private fun updateBase( content: PromotedNotificationContentModel, - textView: ImageFloatingTextView? = null, - showOldProgress: Boolean = true, + collapsed: Boolean, + textView: ImageFloatingTextView? = text, ) { - updateHeader(content, hideTitle = true) + val headerTitleView = if (collapsed) title else null + updateHeader(content, titleView = headerTitleView, collapsed = collapsed) - updateTitle(title, content) - updateText(textView ?: text, content) + if (headerTitleView == null) { + updateTitle(title, content) + } + updateText(textView, content) updateSmallIcon(icon, content) updateImageView(rightIcon, content.skeletonLargeIcon) - - if (showOldProgress) { - updateOldProgressBar(content) - } + updateOldProgressBar(content) } private fun updateBigPictureStyle(content: PromotedNotificationContentModel) { - updateBase(content) + updateBase(content, collapsed = false) } private fun updateBigTextStyle(content: PromotedNotificationContentModel) { - updateBase(content, textView = bigText) + updateBase(content, collapsed = false, textView = bigText) } - private fun updateCallStyle(content: PromotedNotificationContentModel) { - updateConversationHeader(content) + private fun updateCallStyle(content: PromotedNotificationContentModel, collapsed: Boolean) { + updateConversationHeader(content, collapsed = collapsed) updateText(text, content) } private fun updateProgressStyle(content: PromotedNotificationContentModel) { - updateBase(content, showOldProgress = false) + updateBase(content, collapsed = false) updateNewProgressBar(content) } private fun updateOldProgressBar(content: PromotedNotificationContentModel) { if ( - content.oldProgress == null || + content.style == Style.Progress || + content.oldProgress == null || content.oldProgress.max == 0 || content.oldProgress.isIndeterminate ) { @@ -383,25 +431,33 @@ private class AODPromotedNotificationViewUpdater(root: View) { private fun updateHeader( content: PromotedNotificationContentModel, - hideTitle: Boolean = false, + collapsed: Boolean, + titleView: TextView?, ) { - updateAppName(content) + val hasTitle = titleView != null && content.title != null + val hasSubText = content.subText != null + // the collapsed form doesn't show the app name unless there is no other text in the header + val appNameRequired = !hasTitle && !hasSubText + val hideAppName = (!appNameRequired && collapsed) + + updateAppName(content, forceHide = hideAppName) updateTextView(headerTextSecondary, content.subText) - if (!hideTitle) { - updateTitle(headerText, content) - } + updateTitle(titleView, content) updateTimeAndChronometer(content) - updateHeaderDividers(content, hideTitle = hideTitle) + updateHeaderDividers(content, hideTitle = !hasTitle, hideAppName = hideAppName) + + updateTopLine(content) } private fun updateHeaderDividers( content: PromotedNotificationContentModel, - hideTitle: Boolean = false, + hideAppName: Boolean, + hideTitle: Boolean, ) { - val hasAppName = content.appName != null && content.appName.isNotEmpty() - val hasSubText = content.subText != null && content.subText.isNotEmpty() - val hasHeader = content.title != null && content.title.isNotEmpty() && !hideTitle + val hasAppName = content.appName != null && !hideAppName + val hasSubText = content.subText != null + val hasHeader = content.title != null && !hideTitle val hasTimeOrChronometer = content.time != null val hasTextBeforeSubText = hasAppName @@ -417,27 +473,34 @@ private class AODPromotedNotificationViewUpdater(root: View) { timeDivider?.isVisible = showDividerBeforeTime } - private fun updateConversationHeader(content: PromotedNotificationContentModel) { - updateTitle(conversationText, content) - updateAppName(content) + private fun updateConversationHeader( + content: PromotedNotificationContentModel, + collapsed: Boolean, + ) { + updateAppName(content, forceHide = collapsed) updateTimeAndChronometer(content) - updateConversationHeaderDividers(content, hideTitle = true) updateImageView(verificationIcon, content.verificationIcon) updateTextView(verificationText, content.verificationText) + updateConversationHeaderDividers(content, hideTitle = true, hideAppName = collapsed) + + updateTopLine(content) + updateSmallIcon(conversationIcon, content) + updateTitle(conversationText, content) } private fun updateConversationHeaderDividers( content: PromotedNotificationContentModel, - hideTitle: Boolean = false, + hideTitle: Boolean, + hideAppName: Boolean, ) { val hasTitle = content.title != null && !hideTitle - val hasAppName = content.appName != null + val hasAppName = content.appName != null && !hideAppName val hasTimeOrChronometer = content.time != null val hasVerification = - !content.verificationIcon.isNullOrEmpty() || !content.verificationText.isNullOrEmpty() + !content.verificationIcon.isNullOrEmpty() || content.verificationText != null val hasTextBeforeAppName = hasTitle val hasTextBeforeTime = hasTitle || hasAppName @@ -452,11 +515,15 @@ private class AODPromotedNotificationViewUpdater(root: View) { verificationDivider?.isVisible = showDividerBeforeVerification } - private fun updateAppName(content: PromotedNotificationContentModel) { - updateTextView(appNameText, content.appName) + private fun updateAppName(content: PromotedNotificationContentModel, forceHide: Boolean) { + updateTextView(appNameText, content.appName?.takeUnless { forceHide }) } private fun updateTitle(titleView: TextView?, content: PromotedNotificationContentModel) { + (titleView?.layoutParams as? MarginLayoutParams)?.let { + it.marginEnd = content.imageEndMarginPxIfHasLargeIcon + titleView.layoutParams = it + } updateTextView(titleView, content.title, color = PrimaryText) } @@ -505,6 +572,10 @@ private class AODPromotedNotificationViewUpdater(root: View) { chronometer?.appendFontFeatureSetting("tnum") } + private fun updateTopLine(content: PromotedNotificationContentModel) { + topLine?.headerTextMarginEnd = content.imageEndMarginPxIfHasLargeIcon + } + private fun inflateOldProgressBar() { if (oldProgressBar != null) { return @@ -518,7 +589,8 @@ private class AODPromotedNotificationViewUpdater(root: View) { view: ImageFloatingTextView?, content: PromotedNotificationContentModel, ) { - view?.setHasImage(false) + view?.setHasImage(!content.skeletonLargeIcon.isNullOrEmpty()) + view?.setNumIndentLines(if (content.title != null) 0 else 1) updateTextView(view, content.text) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt index a8a7e885d1f7..9fe3ff4c4bce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt @@ -112,12 +112,13 @@ constructor( if (redactionType == REDACTION_TYPE_NONE) { privateVersion } else { - if (notification.publicVersion == null) { - privateVersion.toDefaultPublicVersion() - } else { - // TODO(b/400991304): implement extraction for [Notification.publicVersion] - privateVersion.toDefaultPublicVersion() - } + notification.publicVersion?.let { publicNotification -> + createAppDefinedPublicVersion( + privateModel = privateVersion, + publicNotification = publicNotification, + imageModelProvider = imageModelProvider, + ) + } ?: createDefaultPublicVersion(privateModel = privateVersion) } return PromotedNotificationContentModels( privateVersion = privateVersion, @@ -126,19 +127,59 @@ constructor( .also { logger.logExtractionSucceeded(entry, it) } } - private fun PromotedNotificationContentModel.toDefaultPublicVersion(): - PromotedNotificationContentModel = - PromotedNotificationContentModel.Builder(key = identity.key).let { - it.style = if (style == Style.Ineligible) Style.Ineligible else Style.Base - it.smallIcon = smallIcon - it.iconLevel = iconLevel - it.appName = appName - it.time = time - it.lastAudiblyAlertedMs = lastAudiblyAlertedMs - it.profileBadgeResId = profileBadgeResId - it.colors = colors - it.build() - } + private fun copyNonSensitiveFields( + privateModel: PromotedNotificationContentModel, + publicBuilder: PromotedNotificationContentModel.Builder, + ) { + publicBuilder.smallIcon = privateModel.smallIcon + publicBuilder.iconLevel = privateModel.iconLevel + publicBuilder.appName = privateModel.appName + publicBuilder.time = privateModel.time + publicBuilder.lastAudiblyAlertedMs = privateModel.lastAudiblyAlertedMs + publicBuilder.profileBadgeResId = privateModel.profileBadgeResId + publicBuilder.colors = privateModel.colors + } + + private fun createDefaultPublicVersion( + privateModel: PromotedNotificationContentModel + ): PromotedNotificationContentModel = + PromotedNotificationContentModel.Builder(key = privateModel.identity.key) + .also { + it.style = + if (privateModel.style == Style.Ineligible) Style.Ineligible else Style.Base + copyNonSensitiveFields(privateModel, it) + } + .build() + + private fun createAppDefinedPublicVersion( + privateModel: PromotedNotificationContentModel, + publicNotification: Notification, + imageModelProvider: ImageModelProvider, + ): PromotedNotificationContentModel = + PromotedNotificationContentModel.Builder(key = privateModel.identity.key) + .also { publicBuilder -> + val notificationStyle = publicNotification.notificationStyle + publicBuilder.style = + when { + privateModel.style == Style.Ineligible -> Style.Ineligible + notificationStyle == CallStyle::class.java -> Style.CollapsedCall + else -> Style.CollapsedBase + } + copyNonSensitiveFields(privateModel = privateModel, publicBuilder = publicBuilder) + publicBuilder.shortCriticalText = publicNotification.shortCriticalText() + publicBuilder.subText = publicNotification.subText() + // The standard public version is extracted as a collapsed notification, + // so avoid using bigTitle or bigText, and instead get the collapsed versions. + publicBuilder.title = publicNotification.title(notificationStyle, expanded = false) + publicBuilder.text = publicNotification.text() + publicBuilder.skeletonLargeIcon = + publicNotification.skeletonLargeIcon(imageModelProvider) + // Only CallStyle has styled content that shows in the collapsed version. + if (publicBuilder.style == Style.Call) { + extractCallStyleContent(publicNotification, publicBuilder, imageModelProvider) + } + } + .build() private fun extractPrivateContent( key: String, @@ -163,8 +204,8 @@ constructor( contentBuilder.shortCriticalText = notification.shortCriticalText() contentBuilder.lastAudiblyAlertedMs = lastAudiblyAlertedMs contentBuilder.profileBadgeResId = null // TODO - contentBuilder.title = notification.title(recoveredBuilder.style) - contentBuilder.text = notification.text(recoveredBuilder.style) + contentBuilder.title = notification.title(recoveredBuilder.style?.javaClass) + contentBuilder.text = notification.text(recoveredBuilder.style?.javaClass) contentBuilder.skeletonLargeIcon = notification.skeletonLargeIcon(imageModelProvider) contentBuilder.oldProgress = notification.oldProgress() @@ -183,45 +224,51 @@ constructor( private fun Notification.smallIconModel(imageModelProvider: ImageModelProvider): ImageModel? = imageModelProvider.getImageModel(smallIcon, SmallSquare) - private fun Notification.title(): CharSequence? = extras?.getCharSequence(EXTRA_TITLE) + private fun Notification.title(): CharSequence? = getCharSequenceExtraUnlessEmpty(EXTRA_TITLE) - private fun Notification.bigTitle(): CharSequence? = extras?.getCharSequence(EXTRA_TITLE_BIG) + private fun Notification.bigTitle(): CharSequence? = + getCharSequenceExtraUnlessEmpty(EXTRA_TITLE_BIG) private fun Notification.callPerson(): Person? = extras?.getParcelable(EXTRA_CALL_PERSON, Person::class.java) - private fun Notification.title(style: Notification.Style?): CharSequence? { - return when (style) { - is BigTextStyle, - is BigPictureStyle, - is InboxStyle -> bigTitle() - is CallStyle -> callPerson()?.name + private fun Notification.title( + styleClass: Class<out Notification.Style>?, + expanded: Boolean = true, + ): CharSequence? { + // bigTitle is only used in the expanded form of 3 styles. + return when (styleClass) { + BigTextStyle::class.java, + BigPictureStyle::class.java, + InboxStyle::class.java -> if (expanded) bigTitle() else null + CallStyle::class.java -> callPerson()?.name?.takeUnlessEmpty() else -> null } ?: title() } - private fun Notification.text(): CharSequence? = extras?.getCharSequence(EXTRA_TEXT) + private fun Notification.text(): CharSequence? = getCharSequenceExtraUnlessEmpty(EXTRA_TEXT) - private fun Notification.bigText(): CharSequence? = extras?.getCharSequence(EXTRA_BIG_TEXT) + private fun Notification.bigText(): CharSequence? = + getCharSequenceExtraUnlessEmpty(EXTRA_BIG_TEXT) - private fun Notification.text(style: Notification.Style?): CharSequence? { - return when (style) { - is BigTextStyle -> bigText() + private fun Notification.text(styleClass: Class<out Notification.Style>?): CharSequence? { + return when (styleClass) { + BigTextStyle::class.java -> bigText() else -> null } ?: text() } - private fun Notification.subText(): String? = extras?.getString(EXTRA_SUB_TEXT) + private fun Notification.subText(): String? = getStringExtraUnlessEmpty(EXTRA_SUB_TEXT) private fun Notification.shortCriticalText(): String? { if (!android.app.Flags.apiRichOngoing()) { return null } - if (this.shortCriticalText != null) { - return this.shortCriticalText + if (shortCriticalText != null) { + return shortCriticalText } if (Flags.promoteNotificationsAutomatically()) { - return this.extras?.getString(EXTRA_AUTOMATICALLY_EXTRACTED_SHORT_CRITICAL_TEXT) + return getStringExtraUnlessEmpty(EXTRA_AUTOMATICALLY_EXTRACTED_SHORT_CRITICAL_TEXT) } return null } @@ -277,7 +324,7 @@ constructor( } private fun Notification.verificationText(): CharSequence? = - extras.getCharSequence(EXTRA_VERIFICATION_TEXT) + getCharSequenceExtraUnlessEmpty(EXTRA_VERIFICATION_TEXT) private fun Notification.Builder.extractStyleContent( notification: Notification, @@ -291,17 +338,15 @@ constructor( null -> Style.Base is BigPictureStyle -> { - style.extractContent(contentBuilder) Style.BigPicture } is BigTextStyle -> { - style.extractContent(contentBuilder) Style.BigText } is CallStyle -> { - style.extractContent(notification, contentBuilder, imageModelProvider) + extractCallStyleContent(notification, contentBuilder, imageModelProvider) Style.Call } @@ -314,19 +359,7 @@ constructor( } } - private fun BigPictureStyle.extractContent( - contentBuilder: PromotedNotificationContentModel.Builder - ) { - // Big title is handled in resolveTitle, and big picture is unsupported. - } - - private fun BigTextStyle.extractContent( - contentBuilder: PromotedNotificationContentModel.Builder - ) { - // Big title and big text are handled in resolveTitle and resolveText. - } - - private fun CallStyle.extractContent( + private fun extractCallStyleContent( notification: Notification, contentBuilder: PromotedNotificationContentModel.Builder, imageModelProvider: ImageModelProvider, @@ -344,3 +377,11 @@ constructor( contentBuilder.newProgress = createProgressModel(0xffffffff.toInt(), 0xff000000.toInt()) } } + +private fun Notification.getCharSequenceExtraUnlessEmpty(key: String): CharSequence? = + extras?.getCharSequence(key)?.takeUnlessEmpty() + +private fun Notification.getStringExtraUnlessEmpty(key: String): String? = + extras?.getString(key)?.takeUnlessEmpty() + +private fun <T : CharSequence> T.takeUnlessEmpty(): T? = takeUnless { it.isEmpty() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiForceExpanded.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiForceExpanded.kt deleted file mode 100644 index adeddde8ccc3..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiForceExpanded.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.notification.promoted - -import com.android.systemui.Flags -import com.android.systemui.flags.FlagToken -import com.android.systemui.flags.RefactorFlagUtils - -/** Helper for reading or using the expanded ui rich ongoing flag state. */ -@Suppress("NOTHING_TO_INLINE") -object PromotedNotificationUiForceExpanded { - /** The aconfig flag name */ - const val FLAG_NAME = Flags.FLAG_UI_RICH_ONGOING_FORCE_EXPANDED - - /** A token used for dependency declaration */ - val token: FlagToken - get() = FlagToken(FLAG_NAME, isEnabled) - - /** Is the refactor enabled */ - @JvmStatic - inline val isEnabled - get() = Flags.uiRichOngoingForceExpanded() - - /** - * Called to ensure code is only run when the flag is enabled. This protects users from the - * unintended behaviors caused by accidentally running new logic, while also crashing on an eng - * build to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun isUnexpectedlyInLegacyMode() = - RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) - - /** - * Called to ensure code is only run when the flag is enabled. This will throw an exception if - * the flag is not enabled to ensure that the refactor author catches issues in testing. - * Caution!! Using this check incorrectly will cause crashes in nextfood builds! - */ - @JvmStatic - @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) - inline fun unsafeAssertInNewMode() = - RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) - - /** - * Called to ensure code is only run when the flag is disabled. This will throw an exception if - * the flag is enabled to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt index d9778bdde0a5..fa9a7b9b524e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt @@ -18,10 +18,13 @@ package com.android.systemui.statusbar.notification.promoted.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.policy.domain.interactor.SensitiveNotificationProtectionInteractor import com.android.systemui.util.kotlin.FlowDumperImpl import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -30,14 +33,31 @@ class AODPromotedNotificationInteractor @Inject constructor( promotedNotificationsInteractor: PromotedNotificationsInteractor, + keyguardInteractor: KeyguardInteractor, + sensitiveNotificationProtectionInteractor: SensitiveNotificationProtectionInteractor, dumpManager: DumpManager, ) : FlowDumperImpl(dumpManager) { + + /** + * Whether the system is unlocked and not screensharing such that private notification content + * is allowed to show on the aod + */ + private val canShowPrivateNotificationContent: Flow<Boolean> = + combine( + keyguardInteractor.isKeyguardDismissible, + sensitiveNotificationProtectionInteractor.isSensitiveStateActive, + ) { isKeyguardDismissible, isSensitive -> + isKeyguardDismissible && !isSensitive + } + /** The content to show as the promoted notification on AOD */ val content: Flow<PromotedNotificationContentModel?> = - promotedNotificationsInteractor.aodPromotedNotification - .map { - // TODO(b/400991304): show the private version when unlocked - it?.publicVersion + combine( + promotedNotificationsInteractor.aodPromotedNotification, + canShowPrivateNotificationContent, + ) { promotedContent, showPrivateContent -> + if (showPrivateContent) promotedContent?.privateVersion + else promotedContent?.publicVersion } .distinctUntilNewInstance() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt index ffacf62fccce..ae6b2cc6cb1f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt @@ -23,11 +23,13 @@ import android.app.Notification import android.app.Notification.FLAG_PROMOTED_ONGOING import androidx.annotation.ColorInt import com.android.internal.widget.NotificationProgressModel +import com.android.systemui.Flags import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi import com.android.systemui.statusbar.notification.row.ImageResult import com.android.systemui.statusbar.notification.row.LazyImage import com.android.systemui.statusbar.notification.row.shared.ImageModel +import com.android.systemui.util.Compile data class PromotedNotificationContentModels( /** The potentially redacted version of the content that will be exposed to the public */ @@ -172,9 +174,11 @@ data class PromotedNotificationContentModel( /** The promotion-eligible style of a notification, or [Style.Ineligible] if not. */ enum class Style { Base, // style == null + CollapsedBase, // style == null BigPicture, BigText, Call, + CollapsedCall, Progress, Ineligible, } @@ -238,6 +242,10 @@ data class PromotedNotificationContentModel( */ @JvmStatic fun isPromotedForStatusBarChip(notification: Notification): Boolean { + if (Compile.IS_DEBUG && Flags.debugLiveUpdatesPromoteAll()) { + return true + } + // Notification.isPromotedOngoing checks the ui_rich_ongoing flag, but we want the // status bar chip to be ready before all the features behind the ui_rich_ongoing flag // are ready. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AnimatedActionBackgroundDrawable.kt index a9ca6359b570..514e16557b65 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AnimatedActionBackgroundDrawable.kt @@ -36,7 +36,7 @@ import com.android.wm.shell.shared.animation.Interpolators import android.graphics.drawable.RippleDrawable import androidx.core.content.ContextCompat -class MagicActionBackgroundDrawable( +class AnimatedActionBackgroundDrawable( context: Context, ) : RippleDrawable( ContextCompat.getColorStateList( @@ -56,13 +56,13 @@ class BaseBackgroundDrawable( context: Context, ) : Drawable() { - private val cornerRadius = context.resources.getDimension(R.dimen.magic_action_button_corner_radius) - private val outlineStrokeWidth = context.resources.getDimension(R.dimen.magic_action_button_outline_stroke_width) + private val cornerRadius = context.resources.getDimension(R.dimen.animated_action_button_corner_radius) + private val outlineStrokeWidth = context.resources.getDimension(R.dimen.animated_action_button_outline_stroke_width) private val insetVertical = 8 * context.resources.displayMetrics.density private val buttonShape = Path() // Color and style - private val outlineStaticColor = context.getColor(R.color.magic_action_button_stroke_color) + private val outlineStaticColor = context.getColor(R.color.animated_action_button_stroke_color) private val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { val bgColor = context.getColor( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionButton.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AnimatedActionButton.kt index d735360f1d4e..a1d75c69bf5b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionButton.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AnimatedActionButton.kt @@ -22,15 +22,15 @@ import android.util.AttributeSet import android.widget.Button /** - * Custom Button for Magic Action Button, which includes the custom background and foreground. + * Custom Button for Animated Action Button, which includes the custom background and foreground. */ @SuppressLint("AppCompatCustomView") -class MagicActionButton @JvmOverloads constructor( +class AnimatedActionButton @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, ) : Button(context, attrs, defStyleAttr) { init { - background = MagicActionBackgroundDrawable(context) + background = AnimatedActionBackgroundDrawable(context) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index bef3c691cb4d..3fed78674cf9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -26,6 +26,7 @@ import static com.android.systemui.Flags.notificationRowAccessibilityExpanded; import static com.android.systemui.Flags.notificationRowTransparency; import static com.android.systemui.Flags.notificationsPinnedHunInShade; import static com.android.systemui.flags.Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE; import static com.android.systemui.statusbar.notification.NotificationUtils.logKey; import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP; @@ -102,6 +103,7 @@ import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.res.R; import com.android.systemui.scene.shared.flag.SceneContainerFlag; +import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType; import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.SmartReplyController; import com.android.systemui.statusbar.StatusBarIconView; @@ -126,7 +128,6 @@ import com.android.systemui.statusbar.notification.headsup.PinnedStatus; import com.android.systemui.statusbar.notification.logging.NotificationCounters; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi; -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded; import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction; import com.android.systemui.statusbar.notification.row.ui.viewmodel.BundleHeaderViewModelImpl; @@ -503,7 +504,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private final ListenerSet<DismissButtonTargetVisibilityListener> mDismissButtonTargetVisibilityListeners = new ListenerSet<>(); - + @RedactionType + private int mRedactionType = REDACTION_TYPE_NONE; public NotificationContentView[] getLayouts() { return Arrays.copyOf(mLayouts, mLayouts.length); } @@ -879,7 +881,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private void updateLimitsForView(NotificationContentView layout) { final int maxExpandedHeight; - if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) { + if (isPromotedOngoing()) { maxExpandedHeight = mMaxExpandedHeightForPromotedOngoing; } else { maxExpandedHeight = mMaxExpandedHeight; @@ -1378,7 +1380,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (mIsSummaryWithChildren) { return mChildrenContainer.getIntrinsicHeight(); } - if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) { + if (isPromotedOngoing()) { return getMaxExpandHeight(); } if (mExpandedWhenPinned) { @@ -1867,6 +1869,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } /** + * Set the redaction type of the row. + */ + public void setRedactionType(@RedactionType int redactionType) { + mRedactionType = redactionType; + } + + /** * Init the bundle header view. The ComposeView is initialized within with the passed viewModel. * This can only be init once and not in conjunction with any other header view. */ @@ -3020,7 +3029,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (mIsSummaryWithChildren && !shouldShowPublic()) { return !mChildrenExpanded; } - if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) { + if (isPromotedOngoing()) { return false; } return mEnableNonGroupedNotificationExpand && mExpandable; @@ -3131,7 +3140,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public void setUserLocked(boolean userLocked) { - if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) return; + if (isPromotedOngoing()) return; mUserLocked = userLocked; mPrivateLayout.setUserExpanding(userLocked); @@ -3401,7 +3410,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public boolean isExpanded(boolean allowOnKeyguard) { - if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) { + if (isPromotedOngoing()) { return isPromotedNotificationExpanded(allowOnKeyguard); } @@ -4516,6 +4525,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView pw.print(", expandedWhenPinned: " + mExpandedWhenPinned); pw.print(", isMinimized: " + mIsMinimized); pw.print(", isAboveShelf: " + isAboveShelf()); + pw.print(", redactionType: " + mRedactionType); pw.println(); if (NotificationContentView.INCLUDE_HEIGHTS_TO_DUMP) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java index 20b826a3ca92..c04613af452a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java @@ -145,7 +145,11 @@ public class ExpandableNotificationRowDragController { | View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION); if (result) { // Log notification drag only if it succeeds - mNotificationPanelLogger.logNotificationDrag(enr.getEntry()); + if (NotificationBundleUi.isEnabled()) { + mNotificationPanelLogger.logNotificationDrag(enr.getEntryAdapter()); + } else { + mNotificationPanelLogger.logNotificationDrag(enr.getEntryLegacy()); + } view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); if (enr.isPinned()) { mHeadsUpManager.releaseAllImmediately(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index 488aa44ddd3b..756a2c19c10e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -1248,7 +1248,7 @@ public class NotificationContentView extends FrameLayout implements Notification final boolean isSingleLineViewPresent = mSingleLineView != null; if (shouldShowSingleLineView && !isSingleLineViewPresent) { - Log.e(TAG, "calculateVisibleType: SingleLineView is not available!"); + Log.wtf(TAG, "calculateVisibleType: SingleLineView is not available!"); } final int collapsedVisualType = shouldShowSingleLineView && isSingleLineViewPresent @@ -1274,9 +1274,6 @@ public class NotificationContentView extends FrameLayout implements Notification } final boolean shouldShowSingleLineView = mIsChildInGroup && !isGroupExpanded(); final boolean isSingleLinePresent = mSingleLineView != null; - if (shouldShowSingleLineView && !isSingleLinePresent) { - Log.e(TAG, "getVisualTypeForHeight: singleLineView is not available."); - } if (!mUserExpanding && shouldShowSingleLineView && isSingleLinePresent) { return VISIBLE_TYPE_SINGLELINE; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java index 2cf3b14bb8c5..0257b4c2397e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java @@ -218,7 +218,7 @@ public class NotificationConversationInfo extends LinearLayout implements @Background Handler bgHandler, OnConversationSettingsClickListener onConversationSettingsClickListener, Optional<BubblesManager> bubblesManagerOptional, - ShadeController shadeController) { + ShadeController shadeController, boolean isDismissable, OnClickListener onCloseClick) { mINotificationManager = iNotificationManager; mPeopleSpaceWidgetManager = peopleSpaceWidgetManager; mOnUserInteractionCallback = onUserInteractionCallback; @@ -263,6 +263,11 @@ public class NotificationConversationInfo extends LinearLayout implements bindHeader(); bindActions(); + View dismissButton = findViewById(R.id.inline_dismiss); + dismissButton.setOnClickListener(onCloseClick); + dismissButton.setVisibility(dismissButton.hasOnClickListeners() && isDismissable + ? VISIBLE : GONE); + View done = findViewById(R.id.done); done.setOnClickListener(mOnDone); done.setAccessibilityDelegate(mGutsContainer.getAccessibilityDelegate()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java index 6c7c7a79348f..d0567f08c2f1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java @@ -608,7 +608,9 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta mBgHandler, onConversationSettingsListener, mBubblesManagerOptional, - mShadeController); + mShadeController, + row.canViewBeDismissed(), + row.getCloseButtonOnClickListener(row)); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java index f494a4ce40dd..2e3a95e07083 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java @@ -39,6 +39,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.FrameLayout.LayoutParams; +import android.widget.ImageView; import com.android.app.animation.Interpolators; import com.android.internal.annotations.VisibleForTesting; @@ -485,19 +486,23 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl @Override public void onParentHeightUpdate() { - if (mParent == null - || (mLeftMenuItems.isEmpty() && mRightMenuItems.isEmpty()) - || mMenuContainer == null) { - return; - } - int parentHeight = mParent.getActualHeight(); - float translationY; - if (parentHeight < mVertSpaceForIcons) { - translationY = (parentHeight / 2) - (mHorizSpaceForIcon / 2); - } else { - translationY = (mVertSpaceForIcons - mHorizSpaceForIcon) / 2; + // If we are using only icon-based buttons, adjust layout for height changes. + // For permission helper full-layout buttons, do not adjust. + if (!Flags.permissionHelperInlineUiRichOngoing()) { + if (mParent == null + || (mLeftMenuItems.isEmpty() && mRightMenuItems.isEmpty()) + || mMenuContainer == null) { + return; + } + int parentHeight = mParent.getActualHeight(); + float translationY; + if (parentHeight < mVertSpaceForIcons) { + translationY = (parentHeight / 2) - (mHorizSpaceForIcon / 2); + } else { + translationY = (mVertSpaceForIcons - mHorizSpaceForIcon) / 2; + } + mMenuContainer.setTranslationY(translationY); } - mMenuContainer.setTranslationY(translationY); } @Override @@ -697,8 +702,11 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl PromotedPermissionGutsContent demoteContent = (PromotedPermissionGutsContent) LayoutInflater.from(context).inflate( R.layout.promoted_permission_guts, null, false); + View demoteButton = LayoutInflater.from(context) + .inflate(R.layout.promoted_menu_item, null, false); MenuItem info = new NotificationMenuItem(context, null, demoteContent, - R.drawable.unpin_icon); + demoteButton); + return info; } @@ -758,10 +766,12 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl menuView.setAlpha(mAlpha); parent.addView(menuView); menuView.setOnClickListener(this); - FrameLayout.LayoutParams lp = (LayoutParams) menuView.getLayoutParams(); - lp.width = mHorizSpaceForIcon; - lp.height = mHorizSpaceForIcon; - menuView.setLayoutParams(lp); + if (item instanceof ImageView) { + FrameLayout.LayoutParams lp = (LayoutParams) menuView.getLayoutParams(); + lp.width = mHorizSpaceForIcon; + lp.height = mHorizSpaceForIcon; + menuView.setLayoutParams(lp); + } } mMenuItemsByView.put(menuView, item); } @@ -860,6 +870,17 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl mGutsContent = content; } + + /** + * Add a new 'guts' panel with custom view. + */ + public NotificationMenuItem(Context context, String contentDescription, GutsContent content, + View itemView) { + mMenuView = itemView; + mContentDescription = contentDescription; + mGutsContent = content; + } + @Override @Nullable public View getMenuView() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowImageInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowImageInflater.kt index 7bac17f4c227..215988471f00 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowImageInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowImageInflater.kt @@ -21,7 +21,7 @@ import android.graphics.drawable.Drawable import android.graphics.drawable.Icon import androidx.annotation.VisibleForTesting import com.android.app.tracing.traceSection -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi import com.android.systemui.statusbar.notification.row.shared.IconData import com.android.systemui.statusbar.notification.row.shared.ImageModel import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider @@ -80,7 +80,7 @@ interface RowImageInflater { companion object { @Suppress("NOTHING_TO_INLINE") @JvmStatic - inline fun featureFlagEnabled() = PromotedNotificationUiAod.isEnabled + inline fun featureFlagEnabled() = PromotedNotificationUi.isEnabled @JvmStatic fun newInstance(previousIndex: ImageModelIndex?, reinflating: Boolean): RowImageInflater = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java index 19321dcef5c7..c4fe25031de3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java @@ -43,7 +43,6 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ContrastColorUtil; import com.android.internal.widget.NotificationActionListLayout; import com.android.systemui.Dependency; -import com.android.systemui.Flags; import com.android.systemui.UiOffloadThread; import com.android.systemui.res.R; import com.android.systemui.statusbar.CrossFadeHelper; @@ -196,7 +195,7 @@ public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapp } private void adjustTitleAndRightIconForPromotedOngoing() { - if (Flags.uiRichOngoingForceExpanded() && mRow.isPromotedOngoing() && mRightIcon != null) { + if (mRow.isPromotedOngoing() && mRightIcon != null) { final int horizontalMargin; if (notificationsRedesignTemplates()) { horizontalMargin = mView.getResources().getDimensionPixelSize( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt index 96527389b5fe..3c8ebfd695a6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.shared import android.app.PendingIntent import android.graphics.drawable.Icon import android.util.Log +import com.android.internal.logging.InstanceId import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels @@ -78,7 +79,7 @@ data class ActiveNotificationModel( /** The intent to execute if UI related to this notification is clicked. */ val contentIntent: PendingIntent?, /** A small per-notification ID, used for statsd logging. */ - val instanceId: Int?, + val instanceId: InstanceId?, /** If this notification is the group summary for a group of notifications. */ val isGroupSummary: Boolean, /** Indicates in which section the notification is displayed in. @see [PriorityBucket]. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiAod.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/AvalancheReplaceHunWhenCritical.kt index c6e3da1c5750..0ccd6064d9a3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiAod.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/AvalancheReplaceHunWhenCritical.kt @@ -14,16 +14,17 @@ * limitations under the License. */ -package com.android.systemui.statusbar.notification.promoted +package com.android.systemui.statusbar.notification.shared import com.android.systemui.Flags import com.android.systemui.flags.FlagToken import com.android.systemui.flags.RefactorFlagUtils -/** Helper for reading or using the promoted ongoing notifications AOD flag state. */ -object PromotedNotificationUiAod { +/** Helper for reading or using the avalanche replace Hun when critical flag state. */ +@Suppress("NOTHING_TO_INLINE") +object AvalancheReplaceHunWhenCritical { /** The aconfig flag name */ - const val FLAG_NAME = Flags.FLAG_AOD_UI_RICH_ONGOING + const val FLAG_NAME = Flags.FLAG_AVALANCHE_REPLACE_HUN_WHEN_CRITICAL /** A token used for dependency declaration */ val token: FlagToken @@ -32,7 +33,7 @@ object PromotedNotificationUiAod { /** Is the refactor enabled */ @JvmStatic inline val isEnabled - get() = Flags.aodUiRichOngoing() + get() = Flags.avalancheReplaceHunWhenCritical() /** * Called to ensure code is only run when the flag is enabled. This protects users from the @@ -45,16 +46,6 @@ object PromotedNotificationUiAod { /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if - * the flag is not enabled to ensure that the refactor author catches issues in testing. - * Caution!! Using this check incorrectly will cause crashes in nextfood builds! - */ - @JvmStatic - @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) - inline fun unsafeAssertInNewMode() = - RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) - - /** - * Called to ensure code is only run when the flag is disabled. This will throw an exception if * the flag is enabled to ensure that the refactor author catches issues in testing. */ @JvmStatic diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java index 8cf9dd365b60..d1e6c6f335c7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java @@ -30,6 +30,7 @@ import android.view.View; import androidx.annotation.NonNull; import com.android.app.animation.Interpolators; +import com.android.internal.dynamicanimation.animation.DynamicAnimation; import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.PhysicsProperty; import com.android.systemui.statusbar.notification.PhysicsPropertyAnimator; @@ -199,15 +200,19 @@ public class ExpandableViewState extends ViewState { if (animateHeight) { expandableView.setActualHeightAnimating(true); } + DynamicAnimation.OnAnimationEndListener endListener = null; + if (!ViewState.isAnimating(expandableView, HEIGHT_PROPERTY)) { + // only Add the end listener if we haven't already + endListener = (animation, canceled, value, velocity) -> { + expandableView.setActualHeightAnimating(false); + if (!canceled && child instanceof ExpandableNotificationRow row) { + row.setGroupExpansionChanging(false /* isExpansionChanging */); + } + }; + } PhysicsPropertyAnimator.setProperty(child, HEIGHT_PROPERTY, this.height, properties, animateHeight, - (animation, canceled, value, velocity) -> { - expandableView.setActualHeightAnimating(false); - if (!canceled && child instanceof ExpandableNotificationRow) { - ((ExpandableNotificationRow) child).setGroupExpansionChanging( - false /* isExpansionChanging */); - } - }); + endListener); } else { startHeightAnimationInterpolator(expandableView, properties); } 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 afa988dd8e89..a01e504d47c6 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 @@ -64,7 +64,6 @@ import android.util.AttributeSet; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.MathUtils; -import android.util.Pair; import android.view.DisplayCutout; import android.view.InputDevice; import android.view.LayoutInflater; @@ -141,7 +140,6 @@ import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScr import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; import com.android.systemui.statusbar.policy.ScrollAdapter; import com.android.systemui.statusbar.policy.SplitShadeStateController; -import com.android.systemui.statusbar.ui.SystemBarUtilsProxy; import com.android.systemui.util.Assert; import com.android.systemui.util.ColorUtilKt; import com.android.systemui.util.DumpUtilsKt; @@ -2253,6 +2251,7 @@ public class NotificationStackScrollLayout } public void setFinishScrollingCallback(Runnable runnable) { + SceneContainerFlag.assertInLegacyMode(); mFinishScrollingCallback = runnable; } @@ -2763,6 +2762,8 @@ public class NotificationStackScrollLayout * which means we want to scroll towards the top. */ protected void fling(int velocityY) { + // Scrolls and flings are handled by the Composables with SceneContainer enabled + SceneContainerFlag.assertInLegacyMode(); if (getChildCount() > 0) { float topAmount = getCurrentOverScrollAmount(true); float bottomAmount = getCurrentOverScrollAmount(false); @@ -3857,7 +3858,10 @@ public class NotificationStackScrollLayout } break; case ACTION_UP: - if (mIsBeingDragged) { + if (SceneContainerFlag.isEnabled() && mIsBeingDragged) { + mActivePointerId = INVALID_POINTER; + endDrag(); + } else if (mIsBeingDragged) { final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); @@ -3920,6 +3924,7 @@ public class NotificationStackScrollLayout } boolean isFlingAfterUpEvent() { + SceneContainerFlag.assertInLegacyMode(); return mFlingAfterUpEvent; } 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 66c9b17ef235..db56718e9f22 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 @@ -97,6 +97,7 @@ import com.android.systemui.statusbar.notification.ColorUpdateLogger; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.LaunchAnimationParameters; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.EntryWithDismissStats; import com.android.systemui.statusbar.notification.collection.NotifCollection; import com.android.systemui.statusbar.notification.collection.NotifPipeline; @@ -1654,6 +1655,13 @@ public class NotificationStackScrollLayoutController implements Dumpable { mVisibilityProvider.obtain(entry, true)); } + private DismissedByUserStats getDismissedByUserStats(String entryKey) { + return new DismissedByUserStats( + DISMISSAL_SHADE, + DISMISS_SENTIMENT_NEUTRAL, + mVisibilityProvider.obtain(entryKey, true)); + } + private View getGutsView() { NotificationGuts guts = mNotificationGutsManager.getExposedGuts(); NotificationMenuRowPlugin menuRow = mSwipeHelper.getCurrentMenuRow(); @@ -1705,9 +1713,19 @@ public class NotificationStackScrollLayoutController implements Dumpable { final List<EntryWithDismissStats> entriesWithRowsDismissedFromShade = new ArrayList<>(); for (ExpandableNotificationRow row : viewsToRemove) { - final NotificationEntry entry = row.getEntry(); - entriesWithRowsDismissedFromShade.add( - new EntryWithDismissStats(entry, getDismissedByUserStats(entry))); + if (NotificationBundleUi.isEnabled()) { + EntryAdapter entryAdapter = row.getEntryAdapter(); + entriesWithRowsDismissedFromShade.add( + new EntryWithDismissStats(null, + getDismissedByUserStats(entryAdapter.getKey()), + entryAdapter.getKey(), + entryAdapter.getBackingHashCode())); + } else { + final NotificationEntry entry = row.getEntryLegacy(); + entriesWithRowsDismissedFromShade.add( + new EntryWithDismissStats(entry, getDismissedByUserStats(entry), + entry.getKey(), entry.hashCode())); + } } mNotifCollection.dismissNotifications(entriesWithRowsDismissedFromShade); } @@ -2073,7 +2091,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { // We log any touches other than down, which will be captured by onTouchEvent. // In the intercept we only start tracing when it's not a down (otherwise that down // would be duplicated when intercepted). - if (mJankMonitor != null && scrollWantsIt + if (!SceneContainerFlag.isEnabled() && mJankMonitor != null && scrollWantsIt && ev.getActionMasked() != MotionEvent.ACTION_DOWN) { mJankMonitor.begin(mView, CUJ_NOTIFICATION_SHADE_SCROLL_FLING); } @@ -2154,7 +2172,9 @@ public class NotificationStackScrollLayoutController implements Dumpable { } mView.setCheckForLeaveBehind(true); } - traceJankOnTouchEvent(ev.getActionMasked(), scrollerWantsIt); + if (!SceneContainerFlag.isEnabled()) { + traceJankOnTouchEvent(ev.getActionMasked(), scrollerWantsIt); + } return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt || longPressWantsIt || hunWantsIt; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt index e5071d9c1e53..58df1703a925 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt @@ -29,7 +29,6 @@ import com.android.systemui.statusbar.LockscreenShadeTransitionController import com.android.systemui.statusbar.StatusBarState.KEYGUARD import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.notification.shared.NotificationBundleUi @@ -476,9 +475,7 @@ constructor( if (onLockscreen) { if ( view is ExpandableNotificationRow && - (canPeek || - (PromotedNotificationUiForceExpanded.isEnabled && - view.isPromotedOngoing)) + (canPeek || view.isPromotedOngoing) ) { height } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java index 5414318b29bd..f0c03016a604 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java @@ -405,7 +405,7 @@ public class StackStateAnimator { public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value, float velocity) { mAnimatorSet.remove(animation); - if (mAnimatorSet.isEmpty() && !canceled) { + if (mAnimatorSet.isEmpty()) { onAnimationFinished(); } mAnimationEndPool.push(this); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java index 29dbeb2c8ee4..2dc5a8127412 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java @@ -398,6 +398,14 @@ public class ViewState implements Dumpable { return childTag != null; } + public static boolean isAnimating(View view, PhysicsProperty property) { + Object childTag = getChildTag(view, property.getTag()); + if (childTag instanceof PropertyData propertyData) { + return propertyData.getAnimator() != null; + } + return childTag != null; + } + /** * Start an animation to this viewstate * @@ -680,13 +688,18 @@ public class ViewState implements Dumpable { private void startYTranslationAnimation(final View child, AnimationProperties properties) { if (mUsePhysicsForMovement) { // Y Translation does some extra calls when it ends, so lets add a listener - DynamicAnimation.OnAnimationEndListener endListener = - (animation, canceled, value, velocity) -> { - if (!canceled) { - HeadsUpUtil.setNeedsHeadsUpDisappearAnimationAfterClick(child, false); - onYTranslationAnimationFinished(child); - } - }; + DynamicAnimation.OnAnimationEndListener endListener = null; + if (!isAnimatingY(child)) { + // Only add a listener if we're not animating yet + endListener = + (animation, canceled, value, velocity) -> { + if (!canceled) { + HeadsUpUtil.setNeedsHeadsUpDisappearAnimationAfterClick(child, + false); + onYTranslationAnimationFinished(child); + } + }; + } PhysicsPropertyAnimator.setProperty(child, Y_TRANSLATION, this.mYTranslation, properties, properties.getAnimationFilter().animateY, endListener); } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt index 5689230f6bed..fdf06e1e1770 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt @@ -319,7 +319,7 @@ private fun List<ActiveNotificationModel>.toNotificationProto(): Notifications.N Notifications.Notification().apply { uid = notification.uid packageName = notification.packageName - notification.instanceId?.let { instanceId = it } + notification.instanceId?.let { instanceId = it.id } // TODO(b/308623704) check if we can set groupInstanceId as well isGroupSummary = notification.isGroupSummary section = NotificationPanelLogger.toNotificationSection(notification.bucket) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index 9d55e1d9d592..e3c2fb5b6e59 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -25,9 +25,11 @@ import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor +import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dump.DumpManager +import com.android.systemui.kairos.awaitClose import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.Edge @@ -95,6 +97,7 @@ import com.android.systemui.util.kotlin.BooleanFlowOperators.not import com.android.systemui.util.kotlin.FlowDumperImpl import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine import com.android.systemui.util.kotlin.sample +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -245,7 +248,7 @@ constructor( val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> = if (SceneContainerFlag.isEnabled) { combine( - shadeInteractor.isShadeLayoutWide, + shadeModeInteractor.isShadeLayoutWide, shadeModeInteractor.shadeMode, configurationInteractor.onAnyConfigurationChange, ) { isShadeLayoutWide, shadeMode, _ -> @@ -357,14 +360,31 @@ constructor( ) .dumpValue("isOnLockscreenWithoutShade") + private val aboutToTransitionToHub: Flow<Unit> = + if (SceneContainerFlag.isEnabled) { + emptyFlow() + } else { + conflatedCallbackFlow { + val callback = + CommunalSceneInteractor.OnSceneAboutToChangeListener { toScene, _ -> + if (toScene == CommunalScenes.Communal) { + trySend(Unit) + } + } + communalSceneInteractor.registerSceneStateProcessor(callback) + awaitClose { communalSceneInteractor.unregisterSceneStateProcessor(callback) } + } + } + /** If the user is visually on the glanceable hub or transitioning to/from it */ private val isOnGlanceableHub: Flow<Boolean> = - combine( - keyguardTransitionInteractor.isFinishedIn( - content = Scenes.Communal, - stateWithoutSceneContainer = GLANCEABLE_HUB, - ), + merge( + aboutToTransitionToHub.map { true }, anyOf( + keyguardTransitionInteractor.isFinishedIn( + content = Scenes.Communal, + stateWithoutSceneContainer = GLANCEABLE_HUB, + ), keyguardTransitionInteractor.isInTransition( edge = Edge.create(to = Scenes.Communal), edgeWithoutSceneContainer = Edge.create(to = GLANCEABLE_HUB), @@ -374,9 +394,7 @@ constructor( edgeWithoutSceneContainer = Edge.create(from = GLANCEABLE_HUB), ), ), - ) { isOnGlanceableHub, transitioningToOrFromHub -> - isOnGlanceableHub || transitioningToOrFromHub - } + ) .distinctUntilChanged() .dumpWhileCollecting("isOnGlanceableHub") @@ -532,6 +550,7 @@ constructor( emit(1f - qsExpansion) } } + Split -> combineTransform(isAnyExpanded, bouncerInteractor.bouncerExpansion) { isAnyExpanded, @@ -544,6 +563,7 @@ constructor( emit(1f) } } + Dual -> combineTransform( shadeModeInteractor.isShadeLayoutWide, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index 8a5b22183563..8f1d59c62844 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.phone; import static android.app.StatusBarManager.SESSION_KEYGUARD; +import static android.service.dreams.Flags.dreamsV2; import android.annotation.IntDef; import android.content.res.Resources; @@ -662,6 +663,9 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp final boolean deviceDreaming = mUpdateMonitor.isDreaming(); final boolean bypass = mKeyguardBypassController.getBypassEnabled() || mAuthController.isUdfpsFingerDown(); + final boolean isBouncerShowing = mKeyguardViewController.primaryBouncerIsOrWillBeShowing() + || mKeyguardTransitionInteractor.getCurrentState() + == KeyguardState.ALTERNATE_BOUNCER; logCalculateModeForPassiveAuth(unlockingAllowed, deviceInteractive, isKeyguardShowing, deviceDreaming, bypass, isStrongBiometric); @@ -685,15 +689,14 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp } } if (unlockingAllowed && deviceDreaming) { - return bypass ? MODE_WAKE_AND_UNLOCK_FROM_DREAM : MODE_ONLY_WAKE; + final boolean wakeAndUnlock = bypass || (dreamsV2() && isBouncerShowing); + return wakeAndUnlock ? MODE_WAKE_AND_UNLOCK_FROM_DREAM : MODE_ONLY_WAKE; } if (unlockingAllowed && mKeyguardStateController.isOccluded()) { return MODE_UNLOCK_COLLAPSING; } if (isKeyguardShowing) { - if ((mKeyguardViewController.primaryBouncerIsOrWillBeShowing() - || mKeyguardTransitionInteractor.getCurrentState() - == KeyguardState.ALTERNATE_BOUNCER) && unlockingAllowed) { + if (isBouncerShowing && unlockingAllowed) { return MODE_DISMISS_BOUNCER; } else if (unlockingAllowed && bypass) { return MODE_UNLOCK_COLLAPSING; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java index fa4fe46e690c..83e5db4db6fe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java @@ -459,7 +459,7 @@ public class KeyguardStatusBarView extends RelativeLayout { /** Should only be called from {@link KeyguardStatusBarViewController}. */ void onOverlayChanged() { - final int carrierTheme = R.style.TextAppearance_StatusBar_Clock; + final int carrierTheme = R.style.TextAppearance_StatusBar_Default; mCarrierLabel.setTextAppearance(carrierTheme); if (mBatteryView != null) { mBatteryView.updatePercentView(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt index b7eada1c6804..207f0ecfb7ed 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt @@ -24,6 +24,7 @@ import android.content.Context import android.view.View import androidx.annotation.VisibleForTesting import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.internal.logging.InstanceId import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -165,6 +166,7 @@ constructor( // is visible (we issue [OngoingCallModel.NoCall] below in that case), so this can // be safely made false. isAppVisible = false, + notificationInstanceId = currentInfo.instanceId, ) } else { return OngoingCallModel.NoCall @@ -222,6 +224,7 @@ constructor( notifModel.contentIntent, notifModel.uid, notifModel.appName, + notifModel.instanceId, notifModel.promotedContent, isOngoing = true, statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false, @@ -343,6 +346,7 @@ constructor( val intent: PendingIntent?, val uid: Int, val appName: String, + val instanceId: InstanceId?, /** * If the call notification also meets promoted notification criteria, this field is filled * in with the content related to promotion. Otherwise null. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt index bed9e7cf4646..ff9b7d5cf513 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt @@ -166,6 +166,7 @@ constructor( appName = model.appName, promotedContent = model.promotedContent, isAppVisible = isVisible, + notificationInstanceId = model.instanceId, ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt index 9546d374b595..e20d76453cd6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.phone.ongoingcall.shared.model import android.app.PendingIntent +import com.android.internal.logging.InstanceId import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels @@ -40,6 +41,8 @@ sealed interface OngoingCallModel { * @property promotedContent if the call notification also meets promoted notification criteria, * this field is filled in with the content related to promotion. Otherwise null. * @property isAppVisible whether the app to which the call belongs is currently visible. + * @property notificationInstanceId an optional per-chip ID used for logging. Should stay the + * same throughout the lifetime of a single chip. */ data class InCall( val startTimeMs: Long, @@ -49,5 +52,6 @@ sealed interface OngoingCallModel { val appName: String, val promotedContent: PromotedNotificationContentModels?, val isAppVisible: Boolean, + val notificationInstanceId: InstanceId?, ) : OngoingCallModel } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt index 10821dffd394..1f4ccd59b063 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt @@ -84,7 +84,7 @@ interface MobileIconsInteractor { val icons: StateFlow<List<MobileIconInteractor>> /** Whether the mobile icons can be stacked vertically. */ - val isStackable: StateFlow<Boolean> + val isStackable: Flow<Boolean> /** * Observable for the subscriptionId of the current mobile data connection. Null if we don't @@ -309,21 +309,20 @@ constructor( override val isStackable = if (NewStatusBarIcons.isEnabled && StatusBarRootModernization.isEnabled) { - icons.flatMapLatest { icons -> - combine(icons.map { it.signalLevelIcon }) { signalLevelIcons -> - // These are only stackable if: - // - They are cellular - // - There's exactly two - // - They have the same number of levels - signalLevelIcons.filterIsInstance<SignalIconModel.Cellular>().let { - it.size == 2 && it[0].numberOfLevels == it[1].numberOfLevels - } + icons.flatMapLatest { icons -> + combine(icons.map { it.signalLevelIcon }) { signalLevelIcons -> + // These are only stackable if: + // - They are cellular + // - There's exactly two + // - They have the same number of levels + signalLevelIcons.filterIsInstance<SignalIconModel.Cellular>().let { + it.size == 2 && it[0].numberOfLevels == it[1].numberOfLevels } } - } else { - flowOf(false) } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + } else { + flowOf(false) + } /** * Copied from the old pipeline. We maintain a 2s period of time where we will keep the diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt index 54cd8e3c46e4..72ff3b67c317 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt @@ -18,9 +18,11 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.binder import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.Flags import com.android.systemui.kairos.ExperimentalKairosApi @@ -48,7 +50,7 @@ object StackedMobileIconBinder { return SingleBindableStatusBarComposeIconView.withDefaultBinding( view = view, shouldBeVisible = { mobileIconsViewModel.isStackable.value }, - ) { _, tint -> + ) { _, tintFlow -> view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { view.composeView.apply { @@ -66,8 +68,9 @@ object StackedMobileIconBinder { viewModelFactory.create() } } + val tint by tintFlow.collectAsStateWithLifecycle() if (viewModel.isIconVisible) { - CompositionLocalProvider(LocalContentColor provides Color(tint())) { + CompositionLocalProvider(LocalContentColor provides Color(tint)) { StackedMobileIcon(viewModel) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt index 494d95e7f177..997b185fdee5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt @@ -36,6 +36,8 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @@ -99,7 +101,17 @@ constructor( } .stateIn(scope, SharingStarted.WhileSubscribed(), false) - val isStackable: StateFlow<Boolean> = interactor.isStackable + /** Whether all of [mobileSubViewModels] are visible or not. */ + private val iconsAreAllVisible = + mobileSubViewModels.flatMapLatest { viewModels -> + combine(viewModels.map { it.isVisible }) { isVisibleArray -> isVisibleArray.all { it } } + } + + val isStackable: StateFlow<Boolean> = + combine(iconsAreAllVisible, interactor.isStackable) { isVisible, isStackable -> + isVisible && isStackable + } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) init { scope.launch { subscriptionIdsFlow.collect { invalidateCaches(it) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarComposeIconView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarComposeIconView.kt index 8076040564fb..9d1df8967fc0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarComposeIconView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarComposeIconView.kt @@ -35,6 +35,7 @@ import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarV import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewVisibilityHelper import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow /** Compose view that is bound to bindable_status_bar_compose_icon.xml */ class SingleBindableStatusBarComposeIconView(context: Context, attrs: AttributeSet?) : @@ -78,7 +79,7 @@ class SingleBindableStatusBarComposeIconView(context: Context, attrs: AttributeS fun withDefaultBinding( view: SingleBindableStatusBarComposeIconView, shouldBeVisible: () -> Boolean, - block: suspend LifecycleOwner.(View, () -> Int) -> Unit, + block: suspend LifecycleOwner.(View, StateFlow<Int>) -> Unit, ): ModernStatusBarViewBinding { @StatusBarIconView.VisibleState val visibilityState: MutableStateFlow<Int> = MutableStateFlow(STATE_HIDDEN) @@ -90,7 +91,7 @@ class SingleBindableStatusBarComposeIconView(context: Context, attrs: AttributeS view.repeatWhenAttached { // Child binding - block(view) { iconTint.value } + block(view, iconTint) lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt index 540babad5dd1..2b1357144a7f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt @@ -54,6 +54,7 @@ import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChip import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModelLegacy import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModel +import com.android.systemui.statusbar.chips.uievents.StatusBarChipsUiEventLogger import com.android.systemui.statusbar.events.domain.interactor.SystemStatusEventAnimationInteractor import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips @@ -228,6 +229,7 @@ constructor( @Background bgScope: CoroutineScope, @Background bgDispatcher: CoroutineDispatcher, shadeDisplaysInteractor: Provider<ShadeDisplaysInteractor>, + private val uiEventLogger: StatusBarChipsUiEventLogger, ) : HomeStatusBarViewModel, ExclusiveActivatable() { private val hydrator = Hydrator(traceName = "HomeStatusBarViewModel.hydrator") @@ -591,6 +593,7 @@ constructor( if (StatusBarPopupChips.isEnabled) { launch { statusBarPopupChips.activate() } } + launch { uiEventLogger.hydrateUiEventLogging(chipsFlow = chipsVisibilityModel) } awaitCancellation() } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt index 6d959be1c5f4..4e18935834cf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt @@ -34,6 +34,10 @@ import android.graphics.drawable.Icon import android.os.Build import android.os.Bundle import android.os.SystemClock +import android.text.Annotation +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.ForegroundColorSpan import android.util.Log import android.view.ContextThemeWrapper import android.view.LayoutInflater @@ -397,13 +401,13 @@ constructor( delayOnClickListener: Boolean, packageContext: Context, ): Button { - val isMagicAction = - Flags.notificationMagicActionsTreatment() && + val isAnimatedAction = + Flags.notificationAnimatedActionsTreatment() && smartActions.fromAssistant && - action.extras.getBoolean(Notification.Action.EXTRA_IS_MAGIC, false) + action.extras.getBoolean(Notification.Action.EXTRA_IS_ANIMATED, false) val layoutRes = - if (isMagicAction) { - R.layout.magic_action_button + if (isAnimatedAction) { + R.layout.animated_action_button } else { if (notificationsRedesignTemplates()) { R.layout.notification_2025_smart_action_button @@ -504,15 +508,40 @@ constructor( choice: CharSequence, delayOnClickListener: Boolean, ): Button { - val layoutRes = + val enableAnimatedReply = Flags.notificationAnimatedActionsTreatment() && + smartReplies.fromAssistant && isAnimatedReply(choice) + val layoutRes = if (enableAnimatedReply) { + R.layout.animated_action_button + } else { if (notificationsRedesignTemplates()) R.layout.notification_2025_smart_reply_button else R.layout.smart_reply_button + } + return (LayoutInflater.from(parent.context).inflate(layoutRes, parent, false) as Button) .apply { - text = choice + // choiceToDeliver does not contain Annotation with extra data + val choiceToDeliver: CharSequence + if (enableAnimatedReply) { + choiceToDeliver = choice.toString() + // If the choice is animated reply, format the text by concatenating + // attributionText with different color to choice text + val fullTextWithAttribution = formatChoiceWithAttribution(choice) + text = fullTextWithAttribution + } else { + choiceToDeliver = choice + text = choice + } + val onClickListener = View.OnClickListener { - onSmartReplyClick(entry, smartReplies, replyIndex, parent, this, choice) + onSmartReplyClick( + entry, + smartReplies, + replyIndex, + parent, + this, + choiceToDeliver + ) } setOnClickListener( if (delayOnClickListener) @@ -600,6 +629,47 @@ constructor( RemoteInput.setResultsSource(intent, RemoteInput.SOURCE_CHOICE) return intent } + + // Check if the choice is animated reply + private fun isAnimatedReply(choice: CharSequence): Boolean { + if (choice is Spanned) { + val annotations = choice.getSpans(0, choice.length, Annotation::class.java) + for (annotation in annotations) { + if (annotation.key == "isAnimatedReply" && annotation.value == "1") { + return true + } + } + } + return false + } + + // Format the text by concatenating attributionText with attribution text color to choice text + private fun formatChoiceWithAttribution(choice: CharSequence): CharSequence { + val colorInt = context.getColor(R.color.animated_action_button_attribution_color) + if (choice is Spanned) { + val annotations = choice.getSpans(0, choice.length, Annotation::class.java) + for (annotation in annotations) { + if (annotation.key == "attributionText") { + // Extract the attribution text + val extraText = annotation.value + // Concatenate choice text and attribution text + val spannableWithColor = SpannableStringBuilder(choice) + spannableWithColor.append(" $extraText") + // Apply color to attribution text + spannableWithColor.setSpan( + ForegroundColorSpan(colorInt), + choice.length, + spannableWithColor.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + return spannableWithColor + } + } + } + + // Return the original if no attributionText found + return choice.toString() + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/SensitiveNotificationProtectionInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/SensitiveNotificationProtectionInteractor.kt new file mode 100644 index 000000000000..0a6a4c2e44e7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/SensitiveNotificationProtectionInteractor.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy.domain.interactor + +import com.android.server.notification.Flags +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf + +/** A interactor which provides the current sensitive notification protections status */ +@SysUISingleton +class SensitiveNotificationProtectionInteractor +@Inject +constructor(private val controller: SensitiveNotificationProtectionController) { + + /** sensitive notification protections status */ + val isSensitiveStateActive: Flow<Boolean> = + if (Flags.screenshareNotificationHiding()) { + conflatedCallbackFlow { + val listener = Runnable { trySend(controller.isSensitiveStateActive) } + controller.registerSensitiveStateListener(listener) + trySend(controller.isSensitiveStateActive) + awaitClose { controller.unregisterSensitiveStateListener(listener) } + } + .distinctUntilChanged() + } else { + flowOf(false) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowView.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowView.java index 36d64a9b405e..bc3eb23f5d09 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowView.java @@ -34,6 +34,7 @@ import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.systemui.Flags; import com.android.systemui.compose.ComposeInitializer; import com.android.systemui.statusbar.core.StatusBarRootModernization; import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController; @@ -117,9 +118,15 @@ public class StatusBarWindowView extends FrameLayout { * bound of the status bar view, in order for the touch event to be correctly dispatched down, * we jot down the position Y of the initial touch down event, offset it to 0 in the y-axis, * and calculate the movement based on first touch down position. + * + * TODO(b/391894499): Remove this doc once Flags.statusBarWindowNoCustomTouch() is rolled out. */ @Override public boolean dispatchTouchEvent(MotionEvent ev) { + if (Flags.statusBarWindowNoCustomTouch()) { + return super.dispatchTouchEvent(ev); + } + if (ev.getAction() == ACTION_DOWN && ev.getRawY() > getHeight()) { mTouchDownY = ev.getRawY(); ev.setLocation(ev.getRawX(), mTopInset); diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index bd3feadf4459..f36f765bb78b 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java @@ -537,8 +537,10 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { try { JSONObject object = new JSONObject(overlayPackageJson); String seedColorStr = Integer.toHexString(defaultSettings.seedColor.toArgb()); - object.put(OVERLAY_CATEGORY_SYSTEM_PALETTE, seedColorStr); - object.put(OVERLAY_CATEGORY_ACCENT_COLOR, seedColorStr); + if(defaultSettings.colorSource == COLOR_SOURCE_PRESET){ + object.put(OVERLAY_CATEGORY_SYSTEM_PALETTE, seedColorStr); + object.put(OVERLAY_CATEGORY_ACCENT_COLOR, seedColorStr); + } object.put(OVERLAY_COLOR_SOURCE, defaultSettings.colorSource); object.put(OVERLAY_CATEGORY_THEME_STYLE, Style.toString(mThemeStyle)); diff --git a/packages/SystemUI/src/com/android/systemui/underlay/OWNERS b/packages/SystemUI/src/com/android/systemui/underlay/OWNERS new file mode 100644 index 000000000000..04ac5530c72d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/underlay/OWNERS @@ -0,0 +1,9 @@ +# pixel team +dupin@google.com +klikli@google.com +chriscui@google.com + +# System UI +nijamkin@google.com +pixel@google.com + diff --git a/packages/SystemUI/src/com/android/systemui/underlay/UnderlayCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/underlay/UnderlayCoreStartable.kt new file mode 100644 index 000000000000..d411a28d5352 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/underlay/UnderlayCoreStartable.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.underlay + +import android.util.Log +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.underlay.shared.flag.UnderlayFlag +import javax.inject.Inject + +/** + * Core startable for the underlay. + * + * This is responsible for starting the underlay and its dependencies. + */ +@SysUISingleton +class UnderlayCoreStartable @Inject constructor() : CoreStartable { + + override fun start() { + if (!UnderlayFlag.isEnabled) { + Log.d(TAG, "Underlay flag is disabled, not starting.") + return + } + + Log.d(TAG, "start!") + } + + private companion object { + const val TAG = "UnderlayCoreStartable" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/underlay/UnderlayModule.kt b/packages/SystemUI/src/com/android/systemui/underlay/UnderlayModule.kt new file mode 100644 index 000000000000..5b1a47fd4a0c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/underlay/UnderlayModule.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.underlay + +import com.android.systemui.CoreStartable +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +interface UnderlayModule { + + @Binds + @IntoMap + @ClassKey(UnderlayCoreStartable::class) + fun bindUnderlayCoreStartable(startable: UnderlayCoreStartable): CoreStartable +} diff --git a/packages/SystemUI/src/com/android/systemui/underlay/shared/flag/UnderlayFlag.kt b/packages/SystemUI/src/com/android/systemui/underlay/shared/flag/UnderlayFlag.kt new file mode 100644 index 000000000000..42d0486f6033 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/underlay/shared/flag/UnderlayFlag.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("NOTHING_TO_INLINE") + +package com.android.systemui.underlay.shared.flag + +import com.android.systemui.Flags.enableUnderlay + +/** Helper for reading or using the underlay flag state. */ +object UnderlayFlag { + + @JvmStatic + inline val isEnabled + get() = enableUnderlay() +} diff --git a/packages/SystemUI/src/com/android/systemui/util/annotations/DeprecatedSysuiVisibleForTesting.kt b/packages/SystemUI/src/com/android/systemui/util/annotations/DeprecatedSysuiVisibleForTesting.kt new file mode 100644 index 000000000000..3d80864943da --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/annotations/DeprecatedSysuiVisibleForTesting.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.annotations + +/** + * Given the effort in go/internal-harmful to eliminate the attempt to use Kotlin `internal` as a + * test-visibility marker, we are centrally moving these APIs to public, marked both with + * [VisibleForTesting] and this annotation. Ideally, over time, these APIs should be replaced with + * explicit named testing APIs (see go/internal-harmful) + */ +@Deprecated( + "Indicates an API that has been marked @VisibleForTesting, but requires further thought" +) +annotation class DeprecatedSysuiVisibleForTesting() diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/AsyncSensorManagerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/AsyncSensorManagerExt.kt new file mode 100644 index 000000000000..e66ad2c209ae --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/AsyncSensorManagerExt.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.kotlin + +import android.hardware.Sensor +import android.hardware.TriggerEvent +import android.hardware.TriggerEventListener +import com.android.systemui.util.sensors.AsyncSensorManager +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import java.util.concurrent.atomic.AtomicBoolean +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow + +/** + * Helper for continuously observing a trigger sensor, which automatically unregisters itself after + * it executes once. We therefore re-register ourselves after each emission. + */ +fun AsyncSensorManager.observeTriggerSensor(sensor: Sensor): Flow<Unit> = conflatedCallbackFlow { + val isRegistered = AtomicBoolean(false) + fun registerCallbackInternal(callback: TriggerEventListener) { + if (isRegistered.compareAndSet(false, true)) { + requestTriggerSensor(callback, sensor) + } + } + + val callback = + object : TriggerEventListener() { + override fun onTrigger(event: TriggerEvent) { + trySend(Unit) + if (isRegistered.getAndSet(false)) { + registerCallbackInternal(this) + } + } + } + + registerCallbackInternal(callback) + + awaitClose { + if (isRegistered.getAndSet(false)) { + cancelTriggerSensor(callback, sensor) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/ui/compose/slider/Slider.kt b/packages/SystemUI/src/com/android/systemui/volume/ui/compose/slider/Slider.kt index 54d2f79509c3..0b11da362a82 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/ui/compose/slider/Slider.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/ui/compose/slider/Slider.kt @@ -18,9 +18,9 @@ package com.android.systemui.volume.ui.compose.slider -import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.SpringSpec +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.material3.ExperimentalMaterial3Api @@ -32,11 +32,11 @@ import androidx.compose.material3.SliderState import androidx.compose.material3.VerticalSlider import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier @@ -53,14 +53,9 @@ import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider import kotlin.math.round -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch - -private val defaultSpring = - SpringSpec<Float>(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessHigh) @Composable fun Slider( @@ -87,55 +82,31 @@ fun Slider( }, ) { require(stepDistance >= 0) { "stepDistance must not be negative" } - val coroutineScope = rememberCoroutineScope() - val snappedValue = snapValue(value, valueRange, stepDistance) + val snappedValue by valueState(value, isEnabled) val hapticsViewModel = haptics.createViewModel(snappedValue, valueRange, interactionSource) - val animatable = remember { Animatable(snappedValue) } - var animationJob: Job? by remember { mutableStateOf(null) } val sliderState = remember(valueRange) { SliderState(value = snappedValue, valueRange = valueRange) } val valueChange: (Float) -> Unit = { newValue -> hapticsViewModel?.onValueChange(newValue) - val snappedNewValue = snapValue(newValue, valueRange, stepDistance) - if (animatable.targetValue != snappedNewValue) { - onValueChanged(snappedNewValue) - animationJob?.cancel() - animationJob = - coroutineScope.launch { - animatable.animateTo( - targetValue = snappedNewValue, - animationSpec = defaultSpring, - ) - } - } + onValueChanged(newValue) } val semantics = createSemantics( accessibilityParams, - animatable.targetValue, + snappedValue, valueRange, valueChange, isEnabled, stepDistance, ) - LaunchedEffect(snappedValue) { - if (!animatable.isRunning && animatable.targetValue != snappedValue) { - animationJob?.cancel() - animationJob = - coroutineScope.launch { - animatable.animateTo(targetValue = snappedValue, animationSpec = defaultSpring) - } - } - } - sliderState.onValueChangeFinished = { hapticsViewModel?.onValueChangeEnded() - onValueChangeFinished?.invoke(animatable.targetValue) + onValueChangeFinished?.invoke(snappedValue) } sliderState.onValueChange = valueChange - sliderState.value = animatable.value + sliderState.value = snappedValue if (isVertical) { VerticalSlider( @@ -161,16 +132,26 @@ fun Slider( } } -private fun snapValue( - value: Float, - valueRange: ClosedFloatingPointRange<Float>, - stepDistance: Float, -): Float { - if (stepDistance == 0f) { - return value - } - val coercedValue = value.coerceIn(valueRange) - return Math.round(coercedValue / stepDistance) * stepDistance +@Composable +private fun valueState(targetValue: Float, isEnabled: Boolean): State<Float> { + var prevValue by remember { mutableFloatStateOf(targetValue) } + var prevEnabled by remember { mutableStateOf(isEnabled) } + // Don't animate slider value when receive the first value and when changing isEnabled state + val value = + if (prevEnabled != isEnabled) mutableFloatStateOf(targetValue) + else + animateFloatAsState( + targetValue = targetValue, + animationSpec = + spring( + dampingRatio = Spring.DampingRatioNoBouncy, + stiffness = Spring.StiffnessMedium, + ), + label = "VolumeSliderValueAnimation", + ) + prevValue = targetValue + prevEnabled = isEnabled + return value } private fun createSemantics( diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index bf79d11b2fb8..515a10792c02 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -1482,6 +1482,14 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } else { assertThat(hint.isNullOrBlank()).isTrue() } + + kosmos.promptViewModel.onClearUdfpsGuidanceHint(true) + + if (testCase.modalities.hasUdfps) { + assertThat(hint).isNull() + } else { + assertThat(hint.isNullOrBlank()).isTrue() + } } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java index 061f7984d44b..cd6757c6e7ea 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java @@ -38,6 +38,7 @@ import android.content.res.Resources; import android.graphics.Color; import android.media.AudioManager; import android.os.Handler; +import android.os.PowerManager; import android.os.UserManager; import android.provider.Settings; import android.testing.TestableLooper; @@ -142,6 +143,7 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { @Mock private SelectedUserInteractor mSelectedUserInteractor; @Mock private UserLogoutInteractor mLogoutInteractor; @Mock private OnBackInvokedDispatcher mOnBackInvokedDispatcher; + @Mock private PowerManager mPowerManager; @Captor private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback; private TestableLooper mTestableLooper; @@ -204,7 +206,8 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { mSelectedUserInteractor, mLogoutInteractor, mInteractor, - () -> new FakeDisplayWindowPropertiesRepository(mContext) + () -> new FakeDisplayWindowPropertiesRepository(mContext), + mPowerManager ); mGlobalActionsDialogLite.setZeroDialogPressDelayForTesting(); @@ -618,6 +621,8 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(true); mGlobalActionsDialogLite.showOrHideDialog(true, true, null /* view */, Display.DEFAULT_DISPLAY); + // Clear the dismiss override so we don't have behavior after dismissing the dialog + mGlobalActionsDialogLite.mDialog.setDismissOverride(null); // Then smart lock will be disabled verify(mLockPatternUtils).requireCredentialEntry(eq(expectedUser)); @@ -739,6 +744,8 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { // Show dialog with keyguard showing mGlobalActionsDialogLite.showOrHideDialog(true, true, null, Display.DEFAULT_DISPLAY); + // Clear the dismiss override so we don't have behavior after dismissing the dialog + mGlobalActionsDialogLite.mDialog.setDismissOverride(null); assertOneItemOfType(mGlobalActionsDialogLite.mItems, GlobalActionsDialogLite.SystemUpdateAction.class); @@ -764,12 +771,15 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { // Show dialog with keyguard showing mGlobalActionsDialogLite.showOrHideDialog(false, false, null, Display.DEFAULT_DISPLAY); + // Clear the dismiss override so we don't have behavior after dismissing the dialog + mGlobalActionsDialogLite.mDialog.setDismissOverride(null); assertNoItemsOfType(mGlobalActionsDialogLite.mItems, GlobalActionsDialogLite.SystemUpdateAction.class); // Hide dialog mGlobalActionsDialogLite.showOrHideDialog(false, false, null, Display.DEFAULT_DISPLAY); + } @Test @@ -783,6 +793,8 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { mGlobalActionsDialogLite.showOrHideDialog(false, true, null, Display.DEFAULT_DISPLAY); mTestableLooper.processAllMessages(); + // Clear the dismiss override so we don't have behavior after dismissing the dialog + mGlobalActionsDialogLite.mDialog.setDismissOverride(null); assertThat(mGlobalActionsDialogLite.mDialog.isShowing()).isTrue(); @@ -797,6 +809,101 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { assertThat(mGlobalActionsDialogLite.mDialog).isNull(); } + @Test + public void testShouldLogStandbyPress() { + GlobalActionsDialogLite.StandbyAction standbyAction = + mGlobalActionsDialogLite.new StandbyAction(); + standbyAction.onPress(); + verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_STANDBY_PRESS); + } + + @Test + public void testCreateActionItems_standbyEnabled_doesShowStandby() { + // Test like a TV, which only has standby and shut down + mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite); + doReturn(2).when(mGlobalActionsDialogLite).getMaxShownPowerItems(); + String[] actions = { + GlobalActionsDialogLite.GLOBAL_ACTION_KEY_STANDBY, + GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER + }; + doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions(); + mGlobalActionsDialogLite.createActionItems(); + + assertItemsOfType(mGlobalActionsDialogLite.mItems, + GlobalActionsDialogLite.StandbyAction.class, + GlobalActionsDialogLite.ShutDownAction.class); + assertThat(mGlobalActionsDialogLite.mOverflowItems).isEmpty(); + assertThat(mGlobalActionsDialogLite.mPowerItems).isEmpty(); + } + + @Test + public void testCreateActionItems_standbyDisabled_doesntStandbyAction() { + mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite); + doReturn(5).when(mGlobalActionsDialogLite).getMaxShownPowerItems(); + doReturn(true).when(mGlobalActionsDialogLite).shouldDisplayEmergency(); + doReturn(true).when(mGlobalActionsDialogLite).shouldDisplayLockdown(any()); + doReturn(true).when(mGlobalActionsDialogLite).shouldShowAction(any()); + String[] actions = { + GlobalActionsDialogLite.GLOBAL_ACTION_KEY_EMERGENCY, + GlobalActionsDialogLite.GLOBAL_ACTION_KEY_LOCKDOWN, + GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER, + GlobalActionsDialogLite.GLOBAL_ACTION_KEY_RESTART + }; + doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions(); + mGlobalActionsDialogLite.createActionItems(); + + assertNoItemsOfType(mGlobalActionsDialogLite.mItems, + GlobalActionsDialogLite.StandbyAction.class); + assertThat(mGlobalActionsDialogLite.mOverflowItems).isEmpty(); + assertThat(mGlobalActionsDialogLite.mPowerItems).isEmpty(); + } + + @Test + public void testCreateActionItems_standbyEnabled_locked_showsStandby() { + // Test like a TV, which only has standby and shut down + mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite); + doReturn(2).when(mGlobalActionsDialogLite).getMaxShownPowerItems(); + String[] actions = { + GlobalActionsDialogLite.GLOBAL_ACTION_KEY_STANDBY, + GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER + }; + doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions(); + + // Show dialog with keyguard showing and provisioned + mGlobalActionsDialogLite.showOrHideDialog(true, true, null, Display.DEFAULT_DISPLAY); + // Clear the dismiss override so we don't have behavior after dismissing the dialog + mGlobalActionsDialogLite.mDialog.setDismissOverride(null); + + assertOneItemOfType(mGlobalActionsDialogLite.mItems, + GlobalActionsDialogLite.StandbyAction.class); + + // Hide dialog + mGlobalActionsDialogLite.showOrHideDialog(true, true, null, Display.DEFAULT_DISPLAY); + } + + @Test + public void testCreateActionItems_standbyEnabled_notProvisioned_showsStandby() { + // Test like a TV, which only has standby and shut down. + mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite); + doReturn(2).when(mGlobalActionsDialogLite).getMaxShownPowerItems(); + String[] actions = { + GlobalActionsDialogLite.GLOBAL_ACTION_KEY_STANDBY, + GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER + }; + doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions(); + + // Show dialog without keyguard showing and not provisioned + mGlobalActionsDialogLite.showOrHideDialog(false, false, null, Display.DEFAULT_DISPLAY); + // Clear the dismiss override so we don't have behavior after dismissing the dialog + mGlobalActionsDialogLite.mDialog.setDismissOverride(null); + + assertOneItemOfType(mGlobalActionsDialogLite.mItems, + GlobalActionsDialogLite.StandbyAction.class); + + // Hide dialog + mGlobalActionsDialogLite.showOrHideDialog(false, false, null, Display.DEFAULT_DISPLAY); + } + private UserInfo mockCurrentUser(int flags) { return new UserInfo(10, "A User", flags); diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt index d6b778fe2bc2..0a8572a8a270 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt @@ -52,8 +52,10 @@ import com.android.systemui.dump.dumpManager import com.android.systemui.flags.featureFlagsClassic import com.android.systemui.flags.systemPropertiesHelper import com.android.systemui.jank.interactionJankMonitor +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionBootInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.backgroundScope import com.android.systemui.kosmos.runTest @@ -174,6 +176,12 @@ class KeyguardViewMediatorTestKt : SysuiTestCase() { @Test fun doKeyguardTimeout_changesCommunalScene() = kosmos.runTest { + // Transition fully to gone + fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.LOCKSCREEN, + KeyguardState.GONE, + ) + // Hub is enabled and hub condition is active. setCommunalV2Enabled(true) enableHubOnCharging() @@ -192,6 +200,11 @@ class KeyguardViewMediatorTestKt : SysuiTestCase() { @Test fun doKeyguardTimeout_communalNotAvailable_sleeps() = kosmos.runTest { + fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.LOCKSCREEN, + KeyguardState.GONE, + ) + // Hub disabled. setCommunalV2Enabled(false) @@ -212,6 +225,11 @@ class KeyguardViewMediatorTestKt : SysuiTestCase() { @Test fun doKeyguardTimeout_hubConditionNotActive_sleeps() = kosmos.runTest { + fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.LOCKSCREEN, + KeyguardState.GONE, + ) + // Communal enabled, but hub condition set to never. setCommunalV2Enabled(true) disableHubShowingAutomatically() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt index 78a4fbecabe8..a530dda1abfc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt @@ -39,6 +39,7 @@ import com.android.systemui.shade.NotificationPanelView import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.testKosmos import com.android.systemui.util.mockito.whenever +import com.google.android.msdl.domain.MSDLPlayer import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope @@ -59,6 +60,7 @@ class DefaultDeviceEntrySectionTest : SysuiTestCase() { private lateinit var featureFlags: FakeFeatureFlags @Mock private lateinit var falsingManager: FalsingManager @Mock private lateinit var deviceEntryIconViewModel: DeviceEntryIconViewModel + @Mock private lateinit var msdlPlayer: MSDLPlayer private lateinit var underTest: DefaultDeviceEntrySection @Before @@ -81,6 +83,7 @@ class DefaultDeviceEntrySectionTest : SysuiTestCase() { { mock(DeviceEntryBackgroundViewModel::class.java) }, { falsingManager }, { mock(VibratorHelper::class.java) }, + { msdlPlayer }, logcatLogBuffer(), logcatLogBuffer("blueprints"), ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt index 122af0639030..5084f318b05d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt @@ -39,7 +39,6 @@ import android.media.session.PlaybackState import android.net.Uri import android.os.Bundle import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import android.service.notification.StatusBarNotification import android.testing.TestableLooper.RunWithLooper @@ -53,7 +52,6 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dump.DumpManager import com.android.systemui.flags.Flags.MEDIA_REMOTE_RESUME -import com.android.systemui.flags.Flags.MEDIA_RESUME_PROGRESS import com.android.systemui.flags.Flags.MEDIA_RETAIN_SESSIONS import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.kosmos.testDispatcher @@ -289,7 +287,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(SMARTSPACE_CREATION_TIME) whenever(mediaSmartspaceTarget.expiryTimeMillis).thenReturn(SMARTSPACE_EXPIRY_TIME) fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, false) - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, false) fakeFeatureFlags.set(MEDIA_REMOTE_RESUME, false) whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId()) whenever(keyguardUpdateMonitor.isUserInLockdown(any())).thenReturn(false) @@ -971,8 +968,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa @Test fun testAddResumptionControls_hasPartialProgress() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added with partial progress val progress = 0.5 val extras = @@ -998,8 +993,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa @Test fun testAddResumptionControls_hasNotPlayedProgress() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added that have not been played val extras = Bundle().apply { @@ -1023,8 +1016,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa @Test fun testAddResumptionControls_hasFullProgress() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added with progress info val extras = Bundle().apply { @@ -1049,8 +1040,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa @Test fun testAddResumptionControls_hasNoExtras() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added that do not have any extras val desc = MediaDescription.Builder().run { @@ -1067,8 +1056,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa @Test fun testAddResumptionControls_hasEmptyTitle() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added that have empty title val desc = MediaDescription.Builder().run { @@ -1100,8 +1087,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa @Test fun testAddResumptionControls_hasBlankTitle() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added that have a blank title val desc = MediaDescription.Builder().run { @@ -2117,7 +2102,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa } @Test - @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION) fun postDuplicateNotification_doesNotCallListeners() { addNotificationAndLoad() reset(listener) @@ -2137,26 +2121,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa } @Test - @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION) - fun postDuplicateNotification_callsListeners() { - addNotificationAndLoad() - reset(listener) - mediaDataManager.onNotificationAdded(KEY, mediaNotification) - testScope.assertRunAllReady(foreground = 1, background = 1) - verify(listener) - .onMediaDataLoaded( - eq(KEY), - eq(KEY), - capture(mediaDataCaptor), - eq(true), - eq(0), - eq(false), - ) - verify(kosmos.mediaLogger, never()).logDuplicateMediaNotification(eq(KEY)) - } - - @Test - @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION) fun postDifferentIntentNotifications_CallsListeners() { addNotificationAndLoad() reset(listener) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt index 93c27f01b5ff..5ea669d74cc1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt @@ -56,7 +56,6 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.dump.DumpManager import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.Flags.MEDIA_REMOTE_RESUME -import com.android.systemui.flags.Flags.MEDIA_RESUME_PROGRESS import com.android.systemui.flags.Flags.MEDIA_RETAIN_SESSIONS import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.kosmos.testDispatcher @@ -312,7 +311,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(SMARTSPACE_CREATION_TIME) whenever(mediaSmartspaceTarget.expiryTimeMillis).thenReturn(SMARTSPACE_EXPIRY_TIME) fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, false) - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, false) fakeFeatureFlags.set(MEDIA_REMOTE_RESUME, false) whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId()) whenever(keyguardUpdateMonitor.isUserInLockdown(any())).thenReturn(false) @@ -990,8 +988,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun testAddResumptionControls_hasPartialProgress() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added with partial progress val progress = 0.5 val extras = @@ -1017,8 +1013,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun testAddResumptionControls_hasNotPlayedProgress() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added that have not been played val extras = Bundle().apply { @@ -1042,8 +1036,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun testAddResumptionControls_hasFullProgress() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added with progress info val extras = Bundle().apply { @@ -1068,8 +1060,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun testAddResumptionControls_hasNoExtras() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added that do not have any extras val desc = MediaDescription.Builder().run { @@ -1086,8 +1076,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun testAddResumptionControls_hasEmptyTitle() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added that have empty title val desc = MediaDescription.Builder().run { @@ -1119,8 +1107,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun testAddResumptionControls_hasBlankTitle() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added that have a blank title val desc = MediaDescription.Builder().run { @@ -2201,7 +2187,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test - @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION) fun postDuplicateNotification_doesNotCallListeners() { whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true) whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true) @@ -2226,31 +2211,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test - @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION) - fun postDuplicateNotification_callsListeners() { - whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true) - whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true) - - mediaDataProcessor.addInternalListener(mediaDataFilter) - mediaDataFilter.mediaDataProcessor = mediaDataProcessor - addNotificationAndLoad() - reset(listener) - mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) - testScope.assertRunAllReady(foreground = 1, background = 1) - verify(listener) - .onMediaDataLoaded( - eq(KEY), - eq(KEY), - capture(mediaDataCaptor), - eq(true), - eq(0), - eq(false), - ) - verify(kosmos.mediaLogger, never()).logDuplicateMediaNotification(eq(KEY)) - } - - @Test - @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION) fun postDifferentIntentNotifications_CallsListeners() { whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true) whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java index eb72acc0dade..ca8fae875244 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java @@ -1483,6 +1483,44 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { verify(mLocalMediaManager, atLeastOnce()).connectDevice(outputMediaDevice); } + @Test + public void connectDeviceButton_remoteDevice_noButton() { + when(mMediaDevice1.getFeatures()).thenReturn( + ImmutableList.of(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK)); + when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice1); + mMediaSwitchingController.start(mCb); + mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); + + List<MediaItem> resultList = mMediaSwitchingController.getMediaItemList(); + + assertThat(getNumberOfConnectDeviceButtons(resultList)).isEqualTo(0); + } + + @Test + public void connectDeviceButton_localDevice_hasButton() { + when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice1); + mMediaSwitchingController.start(mCb); + mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); + + List<MediaItem> resultList = mMediaSwitchingController.getMediaItemList(); + + assertThat(getNumberOfConnectDeviceButtons(resultList)).isEqualTo(1); + assertThat(resultList.get(resultList.size() - 1).getMediaItemType()).isEqualTo( + MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE); + } + + @Test + public void connectDeviceButton_localDeviceButtonDisabledByParam_noButton() { + when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice1); + mMediaSwitchingController.start(mCb); + mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); + + List<MediaItem> resultList = mMediaSwitchingController.getMediaItemList( + false /* addConnectDeviceButton */); + + assertThat(getNumberOfConnectDeviceButtons(resultList)).isEqualTo(0); + } + @DisableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL) @Test public void connectDeviceButton_presentAtAllTimesForNonGroupOutputs() { @@ -1495,7 +1533,8 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { .getSelectedMediaDevice(); // Verify that there is initially one "Connect a device" button present. - assertThat(getNumberOfConnectDeviceButtons()).isEqualTo(1); + assertThat(getNumberOfConnectDeviceButtons( + mMediaSwitchingController.getMediaItemList())).isEqualTo(1); // Change the selected device, and verify that there is still one "Connect a device" button // present. @@ -1504,7 +1543,8 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { .getSelectedMediaDevice(); mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); - assertThat(getNumberOfConnectDeviceButtons()).isEqualTo(1); + assertThat(getNumberOfConnectDeviceButtons( + mMediaSwitchingController.getMediaItemList())).isEqualTo(1); } @EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL) @@ -1523,7 +1563,8 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { doReturn(selectedInputMediaDevice).when(mInputRouteManager).getSelectedInputDevice(); // Verify that there is initially one "Connect a device" button present. - assertThat(getNumberOfConnectDeviceButtons()).isEqualTo(1); + assertThat(getNumberOfConnectDeviceButtons( + mMediaSwitchingController.getMediaItemList())).isEqualTo(1); // Change the selected device, and verify that there is still one "Connect a device" button // present. @@ -1532,7 +1573,8 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { .getSelectedMediaDevice(); mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); - assertThat(getNumberOfConnectDeviceButtons()).isEqualTo(1); + assertThat(getNumberOfConnectDeviceButtons( + mMediaSwitchingController.getMediaItemList())).isEqualTo(1); } @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) @@ -1633,7 +1675,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(false); mMediaSwitchingController.start(mCb); reset(mCb); - mMediaSwitchingController.getMediaItemList().clear(); + mMediaSwitchingController.clearMediaItemList(); } @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) @@ -1691,9 +1733,9 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { assertThat(items.get(0).isFirstDeviceInGroup()).isTrue(); } - private int getNumberOfConnectDeviceButtons() { + private int getNumberOfConnectDeviceButtons(List<MediaItem> itemList) { int numberOfConnectDeviceButtons = 0; - for (MediaItem item : mMediaSwitchingController.getMediaItemList()) { + for (MediaItem item : itemList) { if (item.getMediaItemType() == MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE) { numberOfConnectDeviceButtons++; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/OutputMediaItemListProxyTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/OutputMediaItemListProxyTest.java index f6edd49f142f..11a3670c20f6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/OutputMediaItemListProxyTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/OutputMediaItemListProxyTest.java @@ -58,7 +58,6 @@ public class OutputMediaItemListProxyTest extends SysuiTestCase { private MediaItem mMediaItem1; private MediaItem mMediaItem2; - private MediaItem mConnectNewDeviceMediaItem; private OutputMediaItemListProxy mOutputMediaItemListProxy; @Parameters(name = "{0}") @@ -83,7 +82,6 @@ public class OutputMediaItemListProxyTest extends SysuiTestCase { when(mMediaDevice4.getId()).thenReturn(DEVICE_ID_4); mMediaItem1 = MediaItem.createDeviceMediaItem(mMediaDevice1); mMediaItem2 = MediaItem.createDeviceMediaItem(mMediaDevice2); - mConnectNewDeviceMediaItem = MediaItem.createPairNewDeviceMediaItem(); mOutputMediaItemListProxy = new OutputMediaItemListProxy(mContext); } @@ -98,8 +96,7 @@ public class OutputMediaItemListProxyTest extends SysuiTestCase { /* devices= */ List.of(mMediaDevice2, mMediaDevice3), /* selectedDevices */ List.of(mMediaDevice3), /* connectedMediaDevice= */ null, - /* needToHandleMutingExpectedDevice= */ false, - /* connectNewDeviceMediaItem= */ null); + /* needToHandleMutingExpectedDevice= */ false); // Check the output media items to be // * a media item with the selected mMediaDevice3 @@ -115,8 +112,7 @@ public class OutputMediaItemListProxyTest extends SysuiTestCase { /* devices= */ List.of(mMediaDevice4, mMediaDevice1, mMediaDevice3, mMediaDevice2), /* selectedDevices */ List.of(mMediaDevice3), /* connectedMediaDevice= */ null, - /* needToHandleMutingExpectedDevice= */ false, - /* connectNewDeviceMediaItem= */ null); + /* needToHandleMutingExpectedDevice= */ false); // Check the output media items to be // * a media item with the selected route mMediaDevice3 @@ -136,8 +132,7 @@ public class OutputMediaItemListProxyTest extends SysuiTestCase { /* devices= */ List.of(mMediaDevice1, mMediaDevice3, mMediaDevice2), /* selectedDevices */ List.of(mMediaDevice1, mMediaDevice3), /* connectedMediaDevice= */ null, - /* needToHandleMutingExpectedDevice= */ false, - /* connectNewDeviceMediaItem= */ null); + /* needToHandleMutingExpectedDevice= */ false); // Check the output media items to be // * a media item with the selected route mMediaDevice3 @@ -161,8 +156,7 @@ public class OutputMediaItemListProxyTest extends SysuiTestCase { /* devices= */ List.of(mMediaDevice2, mMediaDevice4, mMediaDevice3, mMediaDevice1), /* selectedDevices */ List.of(mMediaDevice1, mMediaDevice2, mMediaDevice3), /* connectedMediaDevice= */ null, - /* needToHandleMutingExpectedDevice= */ false, - /* connectNewDeviceMediaItem= */ null); + /* needToHandleMutingExpectedDevice= */ false); if (Flags.enableOutputSwitcherDeviceGrouping()) { // When the device grouping is enabled, the order of selected devices are preserved: @@ -197,8 +191,7 @@ public class OutputMediaItemListProxyTest extends SysuiTestCase { /* devices= */ List.of(mMediaDevice4, mMediaDevice1, mMediaDevice3, mMediaDevice2), /* selectedDevices */ List.of(mMediaDevice2, mMediaDevice3), /* connectedMediaDevice= */ null, - /* needToHandleMutingExpectedDevice= */ false, - /* connectNewDeviceMediaItem= */ null); + /* needToHandleMutingExpectedDevice= */ false); if (Flags.enableOutputSwitcherDeviceGrouping()) { // When the device grouping is enabled, the order of selected devices are preserved: @@ -233,8 +226,7 @@ public class OutputMediaItemListProxyTest extends SysuiTestCase { /* devices= */ List.of(mMediaDevice1, mMediaDevice3, mMediaDevice4), /* selectedDevices */ List.of(mMediaDevice3), /* connectedMediaDevice= */ null, - /* needToHandleMutingExpectedDevice= */ false, - /* connectNewDeviceMediaItem= */ null); + /* needToHandleMutingExpectedDevice= */ false); if (Flags.enableOutputSwitcherDeviceGrouping()) { // When the device grouping is enabled, the order of selected devices are preserved: @@ -261,47 +253,6 @@ public class OutputMediaItemListProxyTest extends SysuiTestCase { } } - @EnableFlags(Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION) - @Test - public void updateMediaDevices_withConnectNewDeviceMediaItem_shouldUpdateMediaItemList() { - assertThat(mOutputMediaItemListProxy.isEmpty()).isTrue(); - - // Create the initial output media item list with a connect new device media item. - mOutputMediaItemListProxy.updateMediaDevices( - /* devices= */ List.of(mMediaDevice2, mMediaDevice3), - /* selectedDevices */ List.of(mMediaDevice3), - /* connectedMediaDevice= */ null, - /* needToHandleMutingExpectedDevice= */ false, - mConnectNewDeviceMediaItem); - - // Check the output media items to be - // * a media item with the selected mMediaDevice3 - // * a group divider for suggested devices - // * a media item with the mMediaDevice2 - // * a connect new device media item - assertThat(mOutputMediaItemListProxy.getOutputMediaItemList()) - .contains(mConnectNewDeviceMediaItem); - assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList())) - .containsExactly(mMediaDevice3, null, mMediaDevice2, null); - - // Update the output media item list without a connect new device media item. - mOutputMediaItemListProxy.updateMediaDevices( - /* devices= */ List.of(mMediaDevice2, mMediaDevice3), - /* selectedDevices */ List.of(mMediaDevice3), - /* connectedMediaDevice= */ null, - /* needToHandleMutingExpectedDevice= */ false, - /* connectNewDeviceMediaItem= */ null); - - // Check the output media items to be - // * a media item with the selected mMediaDevice3 - // * a group divider for suggested devices - // * a media item with the mMediaDevice2 - assertThat(mOutputMediaItemListProxy.getOutputMediaItemList()) - .doesNotContain(mConnectNewDeviceMediaItem); - assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList())) - .containsExactly(mMediaDevice3, null, mMediaDevice2); - } - @DisableFlags(Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION) @Test public void clearAndAddAll_shouldUpdateMediaItemList() { @@ -325,8 +276,7 @@ public class OutputMediaItemListProxyTest extends SysuiTestCase { /* devices= */ List.of(mMediaDevice1), /* selectedDevices */ List.of(), /* connectedMediaDevice= */ null, - /* needToHandleMutingExpectedDevice= */ false, - /* connectNewDeviceMediaItem= */ null); + /* needToHandleMutingExpectedDevice= */ false); assertThat(mOutputMediaItemListProxy.isEmpty()).isFalse(); mOutputMediaItemListProxy.clear(); @@ -354,8 +304,7 @@ public class OutputMediaItemListProxyTest extends SysuiTestCase { /* devices= */ List.of(mMediaDevice1), /* selectedDevices */ List.of(), /* connectedMediaDevice= */ null, - /* needToHandleMutingExpectedDevice= */ false, - /* connectNewDeviceMediaItem= */ null); + /* needToHandleMutingExpectedDevice= */ false); assertThat(mOutputMediaItemListProxy.isEmpty()).isFalse(); mOutputMediaItemListProxy.removeMutingExpectedDevices(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/data/repository/PowerRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/power/data/repository/PowerRepositoryImplTest.kt index 02a3429f9111..41a099a22085 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/power/data/repository/PowerRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/power/data/repository/PowerRepositoryImplTest.kt @@ -21,10 +21,14 @@ import android.content.BroadcastReceiver import android.content.Intent import android.content.IntentFilter import android.os.PowerManager +import android.os.powerManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.keyguard.userActivityNotifier +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture @@ -50,10 +54,10 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) class PowerRepositoryImplTest : SysuiTestCase() { - + private val kosmos = testKosmos() private val systemClock = FakeSystemClock() - @Mock private lateinit var manager: PowerManager + val manager: PowerManager = kosmos.powerManager @Mock private lateinit var dispatcher: BroadcastDispatcher @Captor private lateinit var receiverCaptor: ArgumentCaptor<BroadcastReceiver> @Captor private lateinit var filterCaptor: ArgumentCaptor<IntentFilter> @@ -74,6 +78,7 @@ class PowerRepositoryImplTest : SysuiTestCase() { context.applicationContext, systemClock, dispatcher, + kosmos.userActivityNotifier, ) } @@ -198,13 +203,14 @@ class PowerRepositoryImplTest : SysuiTestCase() { systemClock.setUptimeMillis(345000) underTest.userTouch() + kosmos.fakeExecutor.runAllReady() val flagsCaptor = argumentCaptor<Int>() verify(manager) .userActivity( eq(345000L), eq(PowerManager.USER_ACTIVITY_EVENT_TOUCH), - capture(flagsCaptor) + capture(flagsCaptor), ) assertThat(flagsCaptor.value).isNotEqualTo(PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS) assertThat(flagsCaptor.value).isNotEqualTo(PowerManager.USER_ACTIVITY_FLAG_INDIRECT) @@ -215,13 +221,14 @@ class PowerRepositoryImplTest : SysuiTestCase() { systemClock.setUptimeMillis(345000) underTest.userTouch(noChangeLights = true) + kosmos.fakeExecutor.runAllReady() val flagsCaptor = argumentCaptor<Int>() verify(manager) .userActivity( eq(345000L), eq(PowerManager.USER_ACTIVITY_EVENT_TOUCH), - capture(flagsCaptor) + capture(flagsCaptor), ) assertThat(flagsCaptor.value).isEqualTo(PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt index ae8b52aa6553..c21570928bde 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt @@ -49,6 +49,7 @@ import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.ui.compose.CommunalContent import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.util.CommunalColors +import com.android.systemui.communal.util.userTouchActivityNotifier import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.keyguardInteractor @@ -56,6 +57,7 @@ import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInterac import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.keyguard.userActivityNotifier import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest @@ -63,6 +65,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.controller.keyguardMediaController +import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.res.R import com.android.systemui.scene.shared.model.sceneDataSourceDelegator import com.android.systemui.shade.domain.interactor.shadeInteractor @@ -136,7 +139,9 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { notificationStackScrollLayoutController, keyguardMediaController, lockscreenSmartspaceController, + userTouchActivityNotifier, logcatLogBuffer("GlanceableHubContainerControllerTest"), + kosmos.userActivityNotifier, ) } @@ -176,7 +181,9 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { notificationStackScrollLayoutController, keyguardMediaController, lockscreenSmartspaceController, + userTouchActivityNotifier, logcatLogBuffer("GlanceableHubContainerControllerTest"), + kosmos.userActivityNotifier, ) // First call succeeds. @@ -205,7 +212,9 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { notificationStackScrollLayoutController, keyguardMediaController, lockscreenSmartspaceController, + userTouchActivityNotifier, logcatLogBuffer("GlanceableHubContainerControllerTest"), + kosmos.userActivityNotifier, ) assertThat(controller.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED) @@ -230,7 +239,9 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { notificationStackScrollLayoutController, keyguardMediaController, lockscreenSmartspaceController, + userTouchActivityNotifier, logcatLogBuffer("GlanceableHubContainerControllerTest"), + kosmos.userActivityNotifier, ) // Only initView without attaching a view as we don't want the flows to start collecting @@ -534,6 +545,18 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } @Test + fun onTouchEvent_touchHandled_notifyUserActivity() = + kosmos.runTest { + // Communal is open. + goToScene(CommunalScenes.Communal) + + // Touch event is sent to the container view. + assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue() + verify(containerView).onTouchEvent(DOWN_EVENT) + assertThat(fakePowerRepository.userTouchRegistered).isTrue() + } + + @Test fun onTouchEvent_editActivityShowing_touchesConsumedButNotDispatched() = kosmos.runTest { // Communal is open. diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java index eae23e70027b..e70ce53e74cb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java @@ -83,7 +83,6 @@ import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.notification.headsup.PinnedStatus; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi; -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded; import com.android.systemui.statusbar.notification.row.ExpandableView.OnHeightChangedListener; import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; @@ -948,7 +947,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test - @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME}) + @EnableFlags(PromotedNotificationUi.FLAG_NAME) @DisableFlags(NotificationBundleUi.FLAG_NAME) public void isExpanded_sensitivePromotedNotification_notExpanded() throws Exception { // GIVEN @@ -965,7 +964,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test - @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME}) + @EnableFlags(PromotedNotificationUi.FLAG_NAME) @DisableFlags(NotificationBundleUi.FLAG_NAME) public void isExpanded_promotedNotificationNotOnKeyguard_expanded() throws Exception { // GIVEN @@ -981,7 +980,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test - @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME}) + @EnableFlags(PromotedNotificationUi.FLAG_NAME) @DisableFlags(NotificationBundleUi.FLAG_NAME) public void isExpanded_promotedNotificationAllowOnKeyguard_expanded() throws Exception { // GIVEN @@ -997,7 +996,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test - @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME}) + @EnableFlags(PromotedNotificationUi.FLAG_NAME) @DisableFlags(NotificationBundleUi.FLAG_NAME) public void isExpanded_promotedNotificationIgnoreLockscreenConstraints_expanded() throws Exception { @@ -1035,7 +1034,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test - @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME}) + @EnableFlags(PromotedNotificationUi.FLAG_NAME) @DisableFlags(NotificationBundleUi.FLAG_NAME) public void isExpanded_promotedNotificationSaveSpaceOnLockScreen_notExpanded() throws Exception { @@ -1053,7 +1052,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test - @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME}) + @EnableFlags(PromotedNotificationUi.FLAG_NAME) @DisableFlags(NotificationBundleUi.FLAG_NAME) public void isExpanded_promotedNotificationNotSaveSpaceOnLockScreen_expanded() throws Exception { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java index 2c800bd87ef5..a515c3f6ed6e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java @@ -171,6 +171,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase { private ConversationIconFactory mIconFactory; @Mock private Notification.BubbleMetadata mBubbleMetadata; + @Mock + private View.OnClickListener mCloseListener; private Handler mTestHandler; @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -298,7 +300,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { true, mTestHandler, mTestHandler, null, Optional.of(mBubblesManager), - mShadeController); + mShadeController, true, mCloseListener); } @Test @@ -402,7 +404,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { true, mTestHandler, mTestHandler, null, Optional.of(mBubblesManager), - mShadeController); + mShadeController, true, null); final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name); assertEquals(VISIBLE, nameView.getVisibility()); assertTrue(nameView.getText().toString().contains("Proxied")); @@ -442,7 +444,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { true, mTestHandler, mTestHandler, null, Optional.of(mBubblesManager), - mShadeController); + mShadeController, true, null); final View feedback = mNotificationInfo.findViewById(R.id.feedback); assertEquals(VISIBLE, feedback.getVisibility()); @@ -484,7 +486,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { true, mTestHandler, mTestHandler, null, Optional.of(mBubblesManager), - mShadeController); + mShadeController, true, null); final View settingsButton = mNotificationInfo.findViewById(R.id.info); settingsButton.performClick(); @@ -524,7 +526,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { false, mTestHandler, mTestHandler, null, Optional.of(mBubblesManager), - mShadeController); + mShadeController, true, null); final View settingsButton = mNotificationInfo.findViewById(R.id.info); assertTrue(settingsButton.getVisibility() != View.VISIBLE); } @@ -601,7 +603,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { true, mTestHandler, mTestHandler, null, Optional.of(mBubblesManager), - mShadeController); + mShadeController, true, null); assertThat(((TextView) mNotificationInfo.findViewById(R.id.priority_summary)).getText()) .isEqualTo(mContext.getString( R.string.notification_channel_summary_priority_dnd)); @@ -633,7 +635,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { true, mTestHandler, mTestHandler, null, Optional.of(mBubblesManager), - mShadeController); + mShadeController, true, null); assertThat(((TextView) mNotificationInfo.findViewById(R.id.priority_summary)).getText()) .isEqualTo(mContext.getString( R.string.notification_channel_summary_priority_baseline)); @@ -1018,4 +1020,19 @@ public class NotificationConversationInfoTest extends SysuiTestCase { // THEN the user is not presented with the People Tile pinning request verify(mPeopleSpaceWidgetManager, never()).requestPinAppWidget(eq(mShortcutInfo), any()); } + + + @Test + public void testDismiss() throws Exception { + doStandardBind(); + + View dismiss = mNotificationInfo.findViewById(R.id.inline_dismiss); + dismiss.performClick(); + mTestableLooper.processAllMessages(); + + // Verify action performed on button click + verify(mCloseListener).onClick(any()); + + } + } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt index d0357603665d..4d9f20c0038f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt @@ -20,7 +20,7 @@ import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi import com.android.systemui.statusbar.notification.row.shared.IconData import com.android.systemui.statusbar.notification.row.shared.ImageModel import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider.ImageSizeClass.SmallSquare @@ -31,7 +31,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) -@EnableFlags(PromotedNotificationUiAod.FLAG_NAME) +@EnableFlags(PromotedNotificationUi.FLAG_NAME) class RowImageInflaterTest : SysuiTestCase() { private lateinit var rowImageInflater: RowImageInflater diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt index 574b2c010a37..fc33db6e57e6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt @@ -445,27 +445,30 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { @Test @EnableFlags(ShadeWindowGoesAround.FLAG_NAME) - fun onTouch_actionDown_propagatesToDisplayPolicy() { + fun onInterceptTouchEvent_actionDown_propagatesToDisplayPolicy() { val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0) - controller.onTouch(event) + + view.onInterceptTouchEvent(event) verify(statusBarTouchShadeDisplayPolicy).onStatusBarTouched(eq(event), any()) } @Test @EnableFlags(ShadeWindowGoesAround.FLAG_NAME) - fun onTouch_actionUp_notPropagatesToDisplayPolicy() { + fun onInterceptTouchEvent_actionUp_notPropagatesToDisplayPolicy() { val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0f, 0f, 0) - controller.onTouch(event) + + view.onInterceptTouchEvent(event) verify(statusBarTouchShadeDisplayPolicy, never()).onStatusBarTouched(any(), any()) } @Test @DisableFlags(ShadeWindowGoesAround.FLAG_NAME) - fun onTouch_shadeWindowGoesAroundDisabled_notPropagatesToDisplayPolicy() { + fun onInterceptTouchEvent_shadeWindowGoesAroundDisabled_notPropagatesToDisplayPolicy() { val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0) - controller.onTouch(event) + + view.onInterceptTouchEvent(event) verify(statusBarTouchShadeDisplayPolicy, never()).onStatusBarTouched(eq(event), any()) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java index 4a0445d5543a..5b29bffc5148 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java @@ -67,6 +67,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.testing.UiEventLoggerFake; import com.android.systemui.Dependency; +import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.AnimatorTestRule; import com.android.systemui.flags.FakeFeatureFlags; @@ -409,9 +410,14 @@ public class RemoteInputViewTest extends SysuiTestCase { // fast forward to end of animation mAnimatorTestRule.advanceTimeBy(1); - // assert that fadeOutView's alpha is reset to 1f after the animation (hidden behind - // RemoteInputView) - assertEquals(1f, fadeOutView.getAlpha()); + if (Flags.notificationRowTransparency()) { + // With transparent rows, fadeOutView should be hidden after the animation. + assertEquals(0f, fadeOutView.getAlpha()); + } else { + // assert that fadeOutView's alpha is reset to 1f after the animation (hidden behind + // RemoteInputView) + assertEquals(1f, fadeOutView.getAlpha()); + } assertFalse(view.isAnimatingAppearance()); assertEquals(View.VISIBLE, view.getVisibility()); assertEquals(1f, view.getAlpha()); diff --git a/packages/SystemUI/tests/utils/src/android/hardware/display/FakeAmbientDisplayConfiguration.kt b/packages/SystemUI/tests/utils/src/android/hardware/display/FakeAmbientDisplayConfiguration.kt index cdd0ff7c38f7..9d3d7e6ee292 100644 --- a/packages/SystemUI/tests/utils/src/android/hardware/display/FakeAmbientDisplayConfiguration.kt +++ b/packages/SystemUI/tests/utils/src/android/hardware/display/FakeAmbientDisplayConfiguration.kt @@ -4,12 +4,13 @@ import android.content.Context class FakeAmbientDisplayConfiguration(context: Context) : AmbientDisplayConfiguration(context) { var fakePulseOnNotificationEnabled = true + var fakePickupGestureEnabled = true override fun pulseOnNotificationEnabled(user: Int) = fakePulseOnNotificationEnabled override fun pulseOnNotificationAvailable() = TODO("Not yet implemented") - override fun pickupGestureEnabled(user: Int) = TODO("Not yet implemented") + override fun pickupGestureEnabled(user: Int) = fakePickupGestureEnabled override fun dozePickupSensorAvailable() = TODO("Not yet implemented") diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCaseExt.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCaseExt.kt index c86ba6ccf47f..ebf89e9e3889 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCaseExt.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCaseExt.kt @@ -19,14 +19,40 @@ package com.android.systemui import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testCase import com.android.systemui.kosmos.useStandardTestDispatcher +import com.android.systemui.kosmos.useUnconfinedTestDispatcher -fun SysuiTestCase.testKosmos(): Kosmos = Kosmos().apply { testCase = this@testKosmos } +/** + * This definition, which uses standard dispatcher, is eventually going away. + * + * If you are calling this method, and want the new default behavior, call `testKosmosNew`, and you + * will be migrated to the new behavior (unconfined dispatcher). If you want to maintain the old + * behavior, directly call testKosmosNew().useStandardTestDispatcher(). + * + * The migration will proceed in multiple steps: + * 1. All calls to testKosmos will be converted to testKosmosLegacy, maybe over several CLs. + * 2. When there are zero references to testKosmos, it will be briefly deleted + * 3. A new testKosmos will be introduced that uses unconfined test dispatcher + * 4. All callers to testKosmosNew that have been introduced since step 1 will be migrated to this + * new definition of testKosmos + * 5. testKosmosNew will be deleted + * 6. Over time, test authors will be encouraged to migrate away from testKosmosLegacy + * + * For details, see go/thetiger + */ +// TODO(b/342622417) +fun SysuiTestCase.testKosmos(): Kosmos = testKosmosLegacy() + +/** + * Create a new Kosmos instance using the unconfined test dispatcher. See migration notes on + * [testKosmos] + */ +fun SysuiTestCase.testKosmosNew(): Kosmos = + Kosmos().apply { testCase = this@testKosmosNew }.useUnconfinedTestDispatcher() /** * This should not be called directly. Instead, you can use: - * - testKosmos() to use the default dispatcher (which will soon be unconfined, see go/thetiger) - * - testKosmos().useStandardTestDispatcher() to explicitly choose the standard dispatcher - * - testKosmos().useUnconfinedTestDispatcher() to explicitly choose the unconfined dispatcher + * - testKosmosNew().useStandardTestDispatcher() to explicitly choose the standard dispatcher + * - testKosmosNew() to explicitly choose the unconfined dispatcher (which is the new sysui default) * * For details, see go/thetiger */ diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt index 3f35bb9f3520..9d3b983b3c86 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt @@ -1,5 +1,6 @@ package com.android.systemui.communal.data.repository +import android.content.res.Configuration import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.TransitionKey @@ -9,6 +10,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn @@ -30,10 +32,6 @@ class FakeCommunalSceneRepository( } } - override suspend fun showHubFromPowerButton() { - snapToScene(CommunalScenes.Communal) - } - private val defaultTransitionState = ObservableTransitionState.Idle(CommunalScenes.Default) private val _transitionState = MutableStateFlow<Flow<ObservableTransitionState>?>(null) override val transitionState: StateFlow<ObservableTransitionState> = @@ -48,4 +46,13 @@ class FakeCommunalSceneRepository( override fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) { _transitionState.value = transitionState } + + private val _communalContainerOrientation = + MutableStateFlow(Configuration.ORIENTATION_UNDEFINED) + override val communalContainerOrientation: StateFlow<Int> = + _communalContainerOrientation.asStateFlow() + + override fun setCommunalContainerOrientation(orientation: Int) { + _communalContainerOrientation.value = orientation + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt index 209d1636e380..6b629e4c0472 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt @@ -20,14 +20,18 @@ import com.android.systemui.communal.data.repository.communalSceneRepository import com.android.systemui.communal.shared.log.communalSceneLogger import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.statusbar.policy.keyguardStateController val Kosmos.communalSceneInteractor: CommunalSceneInteractor by Kosmos.Fixture { CommunalSceneInteractor( applicationScope = applicationCoroutineScope, + mainImmediateDispatcher = testDispatcher, repository = communalSceneRepository, logger = communalSceneLogger, sceneInteractor = sceneInteractor, + keyguardStateController = keyguardStateController, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt index 75c4b6f5366b..e90758715ad8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt @@ -22,6 +22,7 @@ import com.android.systemui.keyguard.domain.interactor.keyguardInteractor 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.power.domain.interactor.powerInteractor val Kosmos.communalSceneTransitionInteractor: CommunalSceneTransitionInteractor by @@ -35,5 +36,6 @@ val Kosmos.communalSceneTransitionInteractor: CommunalSceneTransitionInteractor repository = communalSceneTransitionRepository, keyguardInteractor = keyguardInteractor, powerInteractor = powerInteractor, + mainImmediateDispatcher = testDispatcher, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/util/UserTouchActivityNotifierKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/util/UserTouchActivityNotifierKosmos.kt new file mode 100644 index 000000000000..3452d097d3da --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/util/UserTouchActivityNotifierKosmos.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.util + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.backgroundScope +import com.android.systemui.power.domain.interactor.powerInteractor + +val Kosmos.userTouchActivityNotifier by + Kosmos.Fixture { + UserTouchActivityNotifier(backgroundScope, powerInteractor, USER_TOUCH_ACTIVITY_RATE_LIMIT) + } + +const val USER_TOUCH_ACTIVITY_RATE_LIMIT = 100 diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/cursorposition/domain/data/repository/TestCursorPositionRepositoryInstanceProvider.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/cursorposition/domain/data/repository/TestCursorPositionRepositoryInstanceProvider.kt new file mode 100644 index 000000000000..9f998f3a4e62 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/cursorposition/domain/data/repository/TestCursorPositionRepositoryInstanceProvider.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.cursorposition.domain.data.repository + +import android.os.Handler +import com.android.app.displaylib.PerDisplayInstanceProviderWithTeardown +import com.android.systemui.cursorposition.data.repository.InputEventListenerBuilder +import com.android.systemui.cursorposition.data.repository.InputMonitorBuilder +import com.android.systemui.cursorposition.data.repository.SingleDisplayCursorPositionRepository +import com.android.systemui.cursorposition.data.repository.SingleDisplayCursorPositionRepositoryImpl + +class TestCursorPositionRepositoryInstanceProvider( + private val handler: Handler, + private val listenerBuilder: InputEventListenerBuilder, + private val inputMonitorBuilder: InputMonitorBuilder, +) : PerDisplayInstanceProviderWithTeardown<SingleDisplayCursorPositionRepository> { + + override fun destroyInstance(instance: SingleDisplayCursorPositionRepository) { + instance.destroy() + } + + override fun createInstance(displayId: Int): SingleDisplayCursorPositionRepository { + return SingleDisplayCursorPositionRepositoryImpl( + displayId, + handler, + listenerBuilder, + inputMonitorBuilder, + ) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/ui/viewmodel/UdfpsAccessibilityOverlayViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/ui/viewmodel/UdfpsAccessibilityOverlayViewModelKosmos.kt index 2a46437ed33e..2a3bd335bf98 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/ui/viewmodel/UdfpsAccessibilityOverlayViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/ui/viewmodel/UdfpsAccessibilityOverlayViewModelKosmos.kt @@ -18,6 +18,8 @@ package com.android.systemui.deviceentry.data.ui.viewmodel import com.android.systemui.accessibility.domain.interactor.accessibilityInteractor import com.android.systemui.biometrics.domain.interactor.udfpsOverlayInteractor +import com.android.systemui.biometrics.udfpsUtils +import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccessibilityOverlayViewModel import com.android.systemui.deviceentry.ui.viewmodel.DeviceEntryUdfpsAccessibilityOverlayViewModel import com.android.systemui.keyguard.ui.viewmodel.deviceEntryForegroundIconViewModel import com.android.systemui.keyguard.ui.viewmodel.deviceEntryIconViewModel @@ -30,5 +32,15 @@ val Kosmos.deviceEntryUdfpsAccessibilityOverlayViewModel by accessibilityInteractor = accessibilityInteractor, deviceEntryIconViewModel = deviceEntryIconViewModel, deviceEntryFgIconViewModel = deviceEntryForegroundIconViewModel, + udfpsUtils = udfpsUtils, + ) + } + +val Kosmos.alternateBouncerUdfpsAccessibilityOverlayViewModel by + Kosmos.Fixture { + AlternateBouncerUdfpsAccessibilityOverlayViewModel( + udfpsOverlayInteractor = udfpsOverlayInteractor, + accessibilityInteractor = accessibilityInteractor, + udfpsUtils = udfpsUtils, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractorKosmos.kt index 015d4ddcd54e..19c42260d957 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractorKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.deviceentry.domain.interactor +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos val Kosmos.authRippleInteractor by @@ -23,5 +24,6 @@ val Kosmos.authRippleInteractor by AuthRippleInteractor( deviceEntrySourceInteractor = deviceEntrySourceInteractor, deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor, + keyguardInteractor = keyguardInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt index cd4b09c5267a..91c3da4f616f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.biometrics.data.repository.fingerprintPropertyReposi import com.android.systemui.dump.dumpManager import com.android.systemui.keyevent.domain.interactor.keyEventInteractor import com.android.systemui.keyguard.data.repository.biometricSettingsRepository +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.util.time.systemClock @@ -36,6 +37,7 @@ val Kosmos.deviceEntryHapticsInteractor by keyEventInteractor = keyEventInteractor, logger = biometricUnlockLogger, powerInteractor = powerInteractor, + keyguardInteractor = keyguardInteractor, systemClock = systemClock, dumpManager = dumpManager, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/WakeGestureMonitorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/WakeGestureMonitorKosmos.kt new file mode 100644 index 000000000000..7a433281cc7b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/WakeGestureMonitorKosmos.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams + +import android.hardware.display.ambientDisplayConfiguration +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.backgroundCoroutineContext +import com.android.systemui.user.domain.interactor.selectedUserInteractor +import com.android.systemui.util.sensors.asyncSensorManager +import com.android.systemui.util.settings.fakeSettings + +val Kosmos.wakeGestureMonitor by + Kosmos.Fixture { + WakeGestureMonitor( + ambientDisplayConfiguration = ambientDisplayConfiguration, + asyncSensorManager = asyncSensorManager, + bgContext = backgroundCoroutineContext, + secureSettings = fakeSettings, + selectedUserInteractor = selectedUserInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt index 739f6c2af2b4..8465345a0bdd 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt @@ -20,8 +20,10 @@ import android.app.role.mockRoleManager import android.content.applicationContext import android.content.res.mainResources import android.hardware.input.fakeInputManager +import android.os.fakeExecutorHandler import android.view.windowManager import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.keyboard.shortcut.data.repository.AppLaunchDataRepository import com.android.systemui.keyboard.shortcut.data.repository.CustomInputGesturesRepository import com.android.systemui.keyboard.shortcut.data.repository.CustomShortcutCategoriesRepository @@ -29,9 +31,11 @@ import com.android.systemui.keyboard.shortcut.data.repository.DefaultShortcutCat import com.android.systemui.keyboard.shortcut.data.repository.InputGestureDataAdapter import com.android.systemui.keyboard.shortcut.data.repository.InputGestureMaps import com.android.systemui.keyboard.shortcut.data.repository.ShortcutCategoriesUtils +import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperCustomizationModeRepository import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperInputDeviceRepository import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperStateRepository import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperTestHelper +import com.android.systemui.keyboard.shortcut.data.repository.UserVisibleAppsRepository import com.android.systemui.keyboard.shortcut.data.source.AccessibilityShortcutsSource import com.android.systemui.keyboard.shortcut.data.source.AppCategoriesShortcutsSource import com.android.systemui.keyboard.shortcut.data.source.CurrentAppShortcutsSource @@ -41,7 +45,9 @@ import com.android.systemui.keyboard.shortcut.data.source.MultitaskingShortcutsS import com.android.systemui.keyboard.shortcut.data.source.SystemShortcutsSource import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutCustomizationInteractor import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperCategoriesInteractor +import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperCustomizationModeInteractor import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperStateInteractor +import com.android.systemui.keyboard.shortcut.fakes.FakeLauncherApps import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperExclusions import com.android.systemui.keyboard.shortcut.ui.ShortcutCustomizationDialogStarter import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutCustomizationViewModel @@ -196,6 +202,14 @@ val Kosmos.shortcutHelperCategoriesInteractor by } } +val Kosmos.shortcutHelperCustomizationModeRepository by + Kosmos.Fixture { ShortcutHelperCustomizationModeRepository(shortcutHelperStateRepository) } + +val Kosmos.shortcutHelperCustomizationModeInteractor by + Kosmos.Fixture { + ShortcutHelperCustomizationModeInteractor(shortcutHelperCustomizationModeRepository) + } + val Kosmos.shortcutHelperViewModel by Kosmos.Fixture { ShortcutHelperViewModel( @@ -206,6 +220,7 @@ val Kosmos.shortcutHelperViewModel by testDispatcher, shortcutHelperStateInteractor, shortcutHelperCategoriesInteractor, + shortcutHelperCustomizationModeInteractor, ) } @@ -236,3 +251,15 @@ val Kosmos.shortcutCustomizationViewModelFactory by } } } + +val Kosmos.fakeLauncherApps by Kosmos.Fixture { FakeLauncherApps() } + +val Kosmos.userVisibleAppsRepository by + Kosmos.Fixture { + UserVisibleAppsRepository( + userTracker, + fakeExecutor, + fakeExecutorHandler, + fakeLauncherApps.launcherApps, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/fakes/FakeLauncherApps.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/fakes/FakeLauncherApps.kt new file mode 100644 index 000000000000..f0c4a357b974 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/fakes/FakeLauncherApps.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.shortcut.fakes + +import android.content.ComponentName +import android.content.pm.LauncherActivityInfo +import android.content.pm.LauncherApps +import android.os.UserHandle +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.mock + +class FakeLauncherApps { + + private val activityListPerUser: MutableMap<Int, MutableList<LauncherActivityInfo>> = + mutableMapOf() + private val callbacks: MutableList<LauncherApps.Callback> = mutableListOf() + + val launcherApps: LauncherApps = mock { + on { getActivityList(anyOrNull(), any()) } + .then { + val userHandle = it.getArgument<UserHandle>(1) + + activityListPerUser.getOrDefault(userHandle.identifier, emptyList()) + } + on { registerCallback(any(), any()) } + .then { + val callback = it.getArgument<LauncherApps.Callback>(0) + + callbacks.add(callback) + } + on { unregisterCallback(any()) } + .then { + val callback = it.getArgument<LauncherApps.Callback>(0) + + callbacks.remove(callback) + } + } + + fun installPackageForUser(packageName: String, className: String, userHandle: UserHandle) { + val launcherActivityInfo: LauncherActivityInfo = mock { + on { componentName } + .thenReturn(ComponentName(/* pkg= */ packageName, /* cls= */ className)) + } + + if (!activityListPerUser.containsKey(userHandle.identifier)) { + activityListPerUser[userHandle.identifier] = mutableListOf() + } + + activityListPerUser[userHandle.identifier]!!.add(launcherActivityInfo) + + callbacks.forEach { it.onPackageAdded(/* pkg= */ packageName, userHandle) } + } + + fun uninstallPackageForUser(packageName: String, className: String, userHandle: UserHandle) { + activityListPerUser[userHandle.identifier]?.removeIf { + it.componentName.packageName == packageName && it.componentName.className == className + } + + callbacks.forEach { it.onPackageRemoved(/* pkg= */ packageName, userHandle) } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/UserActivityNotifierKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/UserActivityNotifierKosmos.kt new file mode 100644 index 000000000000..91d15017f75e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/UserActivityNotifierKosmos.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard + +import android.os.powerManager +import com.android.keyguard.UserActivityNotifier +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.kosmos.Kosmos + +var Kosmos.userActivityNotifier by + Kosmos.Fixture { + UserActivityNotifier(uiBgExecutor = fakeExecutor, powerManager = powerManager) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt index 1698a5078038..3d684b893e35 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.domain.interactor import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope @@ -38,5 +39,6 @@ val Kosmos.fromGoneTransitionInteractor by communalSceneInteractor = communalSceneInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, keyguardShowWhileAwakeInteractor = keyguardShowWhileAwakeInteractor, + communalSettingsInteractor = communalSettingsInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt index 406b5cb11bb4..19aaa285b6a7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt @@ -22,14 +22,14 @@ import com.android.systemui.keyguard.data.repository.keyguardBlueprintRepository import com.android.systemui.keyguard.data.repository.keyguardSmartspaceSection import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope -import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.domain.interactor.shadeModeInteractor val Kosmos.keyguardBlueprintInteractor by Kosmos.Fixture { KeyguardBlueprintInteractor( keyguardBlueprintRepository = keyguardBlueprintRepository, applicationScope = applicationCoroutineScope, - shadeInteractor = shadeInteractor, + shadeModeInteractor = shadeModeInteractor, configurationInteractor = configurationInteractor, fingerprintPropertyInteractor = fingerprintPropertyInteractor, smartspaceSection = keyguardSmartspaceSection, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModelKosmos.kt index bc35dc8052ec..be5431c3d0d7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModelKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.systemui.biometrics.domain.interactor.udfpsOverlayInteractor import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor @@ -27,5 +28,6 @@ val Kosmos.accessibilityActionsViewModelKosmos by Fixture { communalInteractor = communalInteractor, keyguardTransitionInteractor = keyguardTransitionInteractor, keyguardInteractor = keyguardInteractor, + udfpsOverlayInteractor = udfpsOverlayInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerViewModelKosmos.kt new file mode 100644 index 000000000000..625e751c8b65 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerViewModelKosmos.kt @@ -0,0 +1,29 @@ +/* + * 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.viewmodel + +import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow +import com.android.systemui.keyguard.ui.transitions.blurConfig +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture + +val Kosmos.dreamingToPrimaryBouncerViewModel by Fixture { + DreamingToPrimaryBouncerTransitionViewModel( + animationFlow = keyguardTransitionAnimationFlow, + blurConfig = blurConfig, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt index 530981c489e8..02e63a42d87d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt @@ -17,15 +17,21 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.common.ui.domain.interactor.configurationInteractor +import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.keyguard.ui.glanceableHubBlurComponentFactory import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.applicationCoroutineScope val Kosmos.glanceableHubToLockscreenTransitionViewModel by Fixture { GlanceableHubToLockscreenTransitionViewModel( + applicationScope = applicationCoroutineScope, configurationInteractor = configurationInteractor, animationFlow = keyguardTransitionAnimationFlow, + communalSceneInteractor = communalSceneInteractor, + communalSettingsInteractor = communalSettingsInteractor, blurFactory = glanceableHubBlurComponentFactory, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModelKosmos.kt new file mode 100644 index 000000000000..0198214de0a9 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModelKosmos.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture + +var Kosmos.goneToGlanceableHubTransitionViewModel by Fixture { + GoneToGlanceableHubTransitionViewModel(animationFlow = keyguardTransitionAnimationFlow) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt index a9aa8cd5a7f9..d0f6ae39cc43 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt @@ -71,6 +71,7 @@ val Kosmos.keyguardRootViewModel by Fixture { goneToDozingTransitionViewModel = goneToDozingTransitionViewModel, goneToDreamingTransitionViewModel = goneToDreamingTransitionViewModel, goneToLockscreenTransitionViewModel = goneToLockscreenTransitionViewModel, + goneToGlanceableHubTransitionViewModel = goneToGlanceableHubTransitionViewModel, lockscreenToAodTransitionViewModel = lockscreenToAodTransitionViewModel, lockscreenToDozingTransitionViewModel = lockscreenToDozingTransitionViewModel, lockscreenToDreamingTransitionViewModel = lockscreenToDreamingTransitionViewModel, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelKosmos.kt index 76e2cc8b7bd0..384a071b5155 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelKosmos.kt @@ -19,7 +19,7 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.keyguard.domain.interactor.keyguardSmartspaceInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope -import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.domain.interactor.shadeModeInteractor import com.android.systemui.util.mockito.mock val Kosmos.keyguardSmartspaceViewModel by @@ -29,6 +29,6 @@ val Kosmos.keyguardSmartspaceViewModel by smartspaceController = mock(), keyguardClockViewModel = keyguardClockViewModel, smartspaceInteractor = keyguardSmartspaceInteractor, - shadeInteractor = shadeInteractor, + shadeModeInteractor = shadeModeInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModelKosmos.kt new file mode 100644 index 000000000000..c53bf9556aca --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModelKosmos.kt @@ -0,0 +1,29 @@ +/* + * 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.viewmodel + +import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow +import com.android.systemui.keyguard.ui.transitions.blurConfig +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture + +val Kosmos.primaryBouncerToDreamingTransitionViewModel by Fixture { + PrimaryBouncerToDreamingTransitionViewModel( + animationFlow = keyguardTransitionAnimationFlow, + blurConfig = blurConfig, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt index 1397d974cbc5..20d3682c6964 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt @@ -21,6 +21,7 @@ import android.window.WindowContext import com.android.systemui.common.ui.data.repository.configurationRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope +import com.android.systemui.log.logcatLogBuffer import com.android.systemui.shade.ShadeDisplayChangeLatencyTracker import com.android.systemui.shade.ShadeWindowLayoutParams import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository @@ -60,5 +61,6 @@ val Kosmos.shadeDisplaysInteractor by notificationRebindingTracker, notificationStackRebindingHider, configurationController, + logcatLogBuffer("ShadeDisplaysInteractor"), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt index 32a30502a370..d06d4ca5597d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt @@ -69,7 +69,6 @@ val Kosmos.shadeInteractorImpl by userSetupRepository = userSetupRepository, userSwitcherInteractor = userSwitcherInteractor, baseShadeInteractor = baseShadeInteractor, - shadeModeInteractor = shadeModeInteractor, ) } var Kosmos.notificationElement: NotificationShadeElement by diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt index 55e35f2b2703..90e23290e9e9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt @@ -23,7 +23,6 @@ import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlay import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.disableflags.domain.interactor.disableFlagsInteractor -import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModelFactory val Kosmos.notificationsShadeOverlayContentViewModel: @@ -35,6 +34,5 @@ val Kosmos.notificationsShadeOverlayContentViewModel: shadeInteractor = shadeInteractor, disableFlagsInteractor = disableFlagsInteractor, mediaCarouselInteractor = mediaCarouselInteractor, - activeNotificationsInteractor = activeNotificationsInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelKosmos.kt index ab61a3ef4c3f..553e876bbb26 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelKosmos.kt @@ -22,6 +22,7 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.plugins.activityStarter import com.android.systemui.statusbar.chips.call.domain.interactor.callChipInteractor import com.android.systemui.statusbar.chips.statusBarChipsLogger +import com.android.systemui.statusbar.chips.uievents.statusBarChipsUiEventLogger import com.android.systemui.util.time.fakeSystemClock val Kosmos.callChipViewModel: CallChipViewModel by @@ -33,5 +34,6 @@ val Kosmos.callChipViewModel: CallChipViewModel by systemClock = fakeSystemClock, activityStarter = activityStarter, logger = statusBarChipsLogger, + uiEventLogger = statusBarChipsUiEventLogger, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt index c7dfd5cc93b9..7e8dfa7e585c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel import android.content.applicationContext -import com.android.systemui.animation.dialogTransitionAnimator import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope @@ -25,6 +24,7 @@ import com.android.systemui.statusbar.chips.casttootherdevice.domain.interactor. import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.mediaProjectionChipInteractor import com.android.systemui.statusbar.chips.mediaprojection.ui.view.endMediaProjectionDialogHelper import com.android.systemui.statusbar.chips.statusBarChipsLogger +import com.android.systemui.statusbar.chips.uievents.statusBarChipsUiEventLogger import com.android.systemui.util.time.fakeSystemClock val Kosmos.castToOtherDeviceChipViewModel: CastToOtherDeviceChipViewModel by @@ -38,5 +38,6 @@ val Kosmos.castToOtherDeviceChipViewModel: CastToOtherDeviceChipViewModel by endMediaProjectionDialogHelper = endMediaProjectionDialogHelper, dialogTransitionAnimator = mockDialogTransitionAnimator, logger = statusBarChipsLogger, + uiEventLogger = statusBarChipsUiEventLogger, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt index c2a6f7d91eb0..0cd83d348f9a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt @@ -24,6 +24,7 @@ import com.android.systemui.statusbar.chips.mediaprojection.ui.view.endMediaProj import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.screenRecordChipInteractor import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.shareToAppChipViewModel import com.android.systemui.statusbar.chips.statusBarChipsLogger +import com.android.systemui.statusbar.chips.uievents.statusBarChipsUiEventLogger import com.android.systemui.util.time.fakeSystemClock val Kosmos.screenRecordChipViewModel: ScreenRecordChipViewModel by @@ -37,5 +38,6 @@ val Kosmos.screenRecordChipViewModel: ScreenRecordChipViewModel by dialogTransitionAnimator = mockDialogTransitionAnimator, systemClock = fakeSystemClock, logger = statusBarChipsLogger, + uiEventLogger = statusBarChipsUiEventLogger, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt index 0770009f9998..c8d2b5d4c8f7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt @@ -23,6 +23,7 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.mediaProjectionChipInteractor import com.android.systemui.statusbar.chips.mediaprojection.ui.view.endMediaProjectionDialogHelper import com.android.systemui.statusbar.chips.statusBarChipsLogger +import com.android.systemui.statusbar.chips.uievents.statusBarChipsUiEventLogger import com.android.systemui.util.time.fakeSystemClock val Kosmos.shareToAppChipViewModel: ShareToAppChipViewModel by @@ -35,5 +36,6 @@ val Kosmos.shareToAppChipViewModel: ShareToAppChipViewModel by endMediaProjectionDialogHelper = endMediaProjectionDialogHelper, dialogTransitionAnimator = mockDialogTransitionAnimator, logger = statusBarChipsLogger, + uiEventLogger = statusBarChipsUiEventLogger, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/uievents/StatusBarChipsUiEventLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/uievents/StatusBarChipsUiEventLoggerKosmos.kt new file mode 100644 index 000000000000..a3ba29b66b5c --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/uievents/StatusBarChipsUiEventLoggerKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.chips.uievents + +import com.android.internal.logging.uiEventLoggerFake +import com.android.systemui.kosmos.Kosmos + +val Kosmos.statusBarChipsUiEventLogger: StatusBarChipsUiEventLogger by + Kosmos.Fixture { StatusBarChipsUiEventLogger(uiEventLoggerFake) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt index 4af4e804ff10..db27e7f81e64 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.data.model import android.app.PendingIntent import android.graphics.drawable.Icon +import com.android.internal.logging.InstanceId import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel @@ -42,7 +43,7 @@ fun activeNotificationModel( statusBarIcon: Icon? = null, statusBarChipIcon: StatusBarIconView? = null, uid: Int = 0, - instanceId: Int? = null, + instanceId: InstanceId? = null, isGroupSummary: Boolean = false, packageName: String = "pkg", appName: String = "appName", diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt index c4542c4e709b..00b26c944b90 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt @@ -19,7 +19,7 @@ package com.android.systemui.statusbar.notification.promoted import android.app.Notification import android.content.applicationContext import com.android.systemui.kosmos.Kosmos -import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE +import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.row.RowImageInflater import com.android.systemui.statusbar.notification.row.shared.skeletonImageTransform @@ -40,7 +40,7 @@ fun Kosmos.setPromotedContent(entry: NotificationEntry) { promotedNotificationContentExtractor.extractContent( entry, Notification.Builder.recoverBuilder(applicationContext, entry.sbn.notification), - REDACTION_TYPE_NONE, + REDACTION_TYPE_PUBLIC, RowImageInflater.newInstance(previousIndex = null, reinflating = false) .useForContentModel(), ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt index fcd484353011..ea459a95728a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt @@ -17,12 +17,16 @@ package com.android.systemui.statusbar.notification.promoted.domain.interactor import com.android.systemui.dump.dumpManager +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.policy.domain.interactor.sensitiveNotificationProtectionInteractor val Kosmos.aodPromotedNotificationInteractor by Kosmos.Fixture { AODPromotedNotificationInteractor( promotedNotificationsInteractor = promotedNotificationsInteractor, + keyguardInteractor = keyguardInteractor, + sensitiveNotificationProtectionInteractor = sensitiveNotificationProtectionInteractor, dumpManager = dumpManager, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt index e5e1a830231e..bc0bb753b1d9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.phone.ongoingcall.shared.model import android.app.PendingIntent +import com.android.internal.logging.InstanceId import com.android.systemui.activity.data.repository.activityManagerRepository import com.android.systemui.activity.data.repository.fake import com.android.systemui.kosmos.Kosmos @@ -41,6 +42,7 @@ fun inCallModel( appName: String = "", promotedContent: PromotedNotificationContentModels? = null, isAppVisible: Boolean = false, + instanceId: InstanceId? = null, ) = OngoingCallModel.InCall( startTimeMs, @@ -50,6 +52,7 @@ fun inCallModel( appName, promotedContent, isAppVisible, + instanceId, ) object OngoingCallTestHelper { @@ -82,6 +85,7 @@ object OngoingCallTestHelper { uid: Int = DEFAULT_UID, appName: String = "Fake name", isAppVisible: Boolean = false, + instanceId: InstanceId? = null, ) { if (StatusBarChipsModernization.isEnabled) { activityManagerRepository.fake.startingIsAppVisibleValue = isAppVisible @@ -95,6 +99,7 @@ object OngoingCallTestHelper { promotedContent = promotedContent, uid = uid, appName = appName, + instanceId = instanceId, ) ) } else { @@ -107,6 +112,7 @@ object OngoingCallTestHelper { appName = appName, promotedContent = promotedContent, isAppVisible = isAppVisible, + instanceId = instanceId, ) ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt index 8fa82cad5c32..72f9d550eb4d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt @@ -26,7 +26,6 @@ import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow class FakeMobileIconsInteractor( mobileMappings: MobileMappingsProxy, @@ -76,7 +75,7 @@ class FakeMobileIconsInteractor( override val icons: MutableStateFlow<List<MobileIconInteractor>> = MutableStateFlow(emptyList()) - override val isStackable: StateFlow<Boolean> = MutableStateFlow(false) + override val isStackable: MutableStateFlow<Boolean> = MutableStateFlow(false) private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING) override val defaultMobileIconMapping = _defaultMobileIconMapping diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt index 5c4deaadffd5..0c088cc00dec 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt @@ -29,6 +29,7 @@ import com.android.systemui.shade.domain.interactor.shadeDisplaysInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.shareToAppChipViewModel import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel +import com.android.systemui.statusbar.chips.uievents.statusBarChipsUiEventLogger import com.android.systemui.statusbar.events.domain.interactor.systemStatusEventAnimationInteractor import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.statusBarPopupChipsViewModelFactory import com.android.systemui.statusbar.layout.ui.viewmodel.multiDisplayStatusBarContentInsetsViewModelStore @@ -69,6 +70,7 @@ var Kosmos.homeStatusBarViewModelFactory: (Int) -> HomeStatusBarViewModel by backgroundScope, testDispatcher, { shadeDisplaysInteractor }, + uiEventLogger = statusBarChipsUiEventLogger, ) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/SensitiveNotificationProtectionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/SensitiveNotificationProtectionInteractorKosmos.kt new file mode 100644 index 000000000000..ba4410b51b75 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/SensitiveNotificationProtectionInteractorKosmos.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.policy.sensitiveNotificationProtectionController + +var Kosmos.sensitiveNotificationProtectionInteractor: SensitiveNotificationProtectionInteractor by + Kosmos.Fixture { + SensitiveNotificationProtectionInteractor(sensitiveNotificationProtectionController) + } diff --git a/ravenwood/README.md b/ravenwood/README.md index 9c4fda7a50a6..62f2ffae56ba 100644 --- a/ravenwood/README.md +++ b/ravenwood/README.md @@ -4,25 +4,4 @@ Ravenwood is an officially-supported lightweight unit testing environment for An Ravenwood’s focus on Android platform use-cases, improved maintainability, and device consistency distinguishes it from Robolectric, which remains a popular choice for app testing. -## Background - -Executing tests on a typical Android device has substantial overhead, such as flashing the build, waiting for the boot to complete, and retrying tests that fail due to general flakiness. - -In contrast, defining a lightweight unit testing environment mitigates these issues by running directly from build artifacts (no flashing required), runs immediately (no booting required), and runs in an isolated environment (less flakiness). - -## Guiding principles -Here’s a summary of the guiding principles for Ravenwood, aimed at addressing Robolectric design concerns and better supporting Android platform developers: - -* **API support for Ravenwood is opt-in.** Teams that own APIs decide exactly what, and how, they support their API functionality being available to tests. When an API hasn’t opted-in, the API signatures remain available for tests to compile against and/or mock, but they throw when called under a Ravenwood environment. - * _Contrasted with Robolectric which attempts to run API implementations as-is, causing maintenance pains as teams maintain or redesign their API internals._ -* **API support and customizations for Ravenwood appear directly inline with relevant code.** This improves maintenance of APIs by providing awareness of what code runs under Ravenwood, including the ability to replace code at a per-method level when Ravenwood-specific customization is needed. - * _Contrasted with Robolectric which maintains customized behavior in separate “Shadow” classes that are difficult for maintainers to be aware of._ -* **APIs supported under Ravenwood are tested to remain consistent with physical devices.** As teams progressively opt-in supporting APIs under Ravenwood, we’re requiring they bring along “bivalent” tests (such as the relevant CTS) to validate that Ravenwood behaves just like a physical device. - * _Contrasted with Robolectric, which has limited (and forked) testing of their environment, increasing their risk of accidental divergence over time and misleading “passing” signals._ -* **Ravenwood aims to support more “real” code.** As API owners progressively opt-in their code, they have the freedom to provide either a limited “fake” that is a faithful emulation of how a device behaves, or they can bring more “real” code that runs on physical devices. - * _Contrasted with Robolectric, where support for “real” code ends at the app process boundary, such as a call into `system_server`._ - -## More details - -* [Ravenwood for Test Authors](test-authors.md) -* [Ravenwood for API Maintainers](api-maintainers.md) +Documents have been moved to go/ravenwood. diff --git a/ravenwood/api-maintainers.md b/ravenwood/api-maintainers.md deleted file mode 100644 index 4b2f96804c97..000000000000 --- a/ravenwood/api-maintainers.md +++ /dev/null @@ -1,94 +0,0 @@ -# Ravenwood for API Maintainers - -By default, Android APIs aren’t opted-in to Ravenwood, and they default to throwing when called under the Ravenwood environment. - -To opt-in to supporting an API under Ravenwood, you can use the inline annotations documented below to customize your API behavior when running under Ravenwood. Because these annotations are inline in the relevant platform source code, they serve as valuable reminders to future API maintainers of Ravenwood support expectations. - -> **Note:** to ensure that API teams are well-supported during early Ravenwood onboarding, the Ravenwood team is manually maintaining an allow-list of classes that are able to use Ravenwood annotations. Please reach out to ravenwood@ so we can offer design advice and allow-list your APIs. - -These Ravenwood-specific annotations have no bearing on the status of an API being public, `@SystemApi`, `@TestApi`, `@hide`, etc. Ravenwood annotations are an orthogonal concept that are only consumed by the internal `hoststubgen` tool during a post-processing step that generates the Ravenwood runtime environment. Teams that own APIs can continue to refactor opted-in `@hide` implementation details, as long as the test-visible behavior continues passing. - -As described in our Guiding Principles, when a team opts-in an API, we’re requiring that they bring along “bivalent” tests (such as the relevant CTS) to validate that Ravenwood behaves just like a physical device. At the moment this means adding the bivalent tests to relevant `TEST_MAPPING` files to ensure they remain consistently passing over time. These bivalent tests are important because they progressively provide the foundation on which higher-level unit tests place their trust. - -## Opt-in to supporting a single method while other methods remained opt-out - -``` -@RavenwoodKeepPartialClass -public class MyManager { - @RavenwoodKeep - public static String modeToString(int mode) { - // This method implementation runs as-is on both devices and Ravenwood - } - - public static void doComplex() { - // This method implementation runs as-is on devices, but because there - // is no method-level annotation, and the class-level default is - // “keep partial”, this method is not supported under Ravenwood and - // will throw - } -} -``` - -## Opt-in an entire class with opt-out of specific methods - -``` -@RavenwoodKeepWholeClass -public class MyStruct { - public void doSimple() { - // This method implementation runs as-is on both devices and Ravenwood, - // implicitly inheriting the class-level annotation - } - - @RavenwoodThrow - public void doComplex() { - // This method implementation runs as-is on devices, but the - // method-level annotation overrides the class-level annotation, so - // this method is not supported under Ravenwood and will throw - } -} -``` - -## Replace a complex method when under Ravenwood - -``` -@RavenwoodKeepWholeClass -public class MyStruct { - @RavenwoodReplace - public void doComplex() { - // This method implementation runs as-is on devices, but the - // implementation is replaced/substituted by the - // doComplex$ravenwood() method implementation under Ravenwood - } - - public void doComplex$ravenwood() { - // This method implementation only runs under Ravenwood - } -} -``` - -## General strategies for side-stepping tricky dependencies - -The “replace” strategy described above is quite powerful, and can be used in creative ways to sidestep tricky underlying dependencies that aren’t ready yet. - -For example, consider a constructor or static initializer that relies on unsupported functionality from another team. By factoring the unsupported logic into a dedicated method, that method can then be replaced under Ravenwood to offer baseline functionality. - -## Strategies for JNI - -At the moment, JNI isn't yet supported under Ravenwood, but you may still want to support APIs that are partially implemented with JNI. The current approach is to use the “replace” strategy to offer a pure-Java alternative implementation for any JNI-provided logic. - -Since this approach requires potentially complex re-implementation, it should only be considered for core infrastructure that is critical to unblocking widespread testing use-cases. Other less-common usages of JNI should instead wait for offical JNI support in the Ravenwood environment. - -When a pure-Java implementation grows too large or complex to host within the original class, the `@RavenwoodNativeSubstitutionClass` annotation can be used to host it in a separate source file: - -``` -@RavenwoodKeepWholeClass -@RavenwoodNativeSubstitutionClass("com.android.platform.test.ravenwood.nativesubstitution.MyComplexClass_host") -public class MyComplexClass { - private static native void nativeDoThing(long nativePtr); -... - -public class MyComplexClass_host { - public static void nativeDoThing(long nativePtr) { - // ... - } -``` diff --git a/ravenwood/scripts/ravenwood-stats-collector.sh b/ravenwood/scripts/ravenwood-stats-collector.sh index c2bf8d82e272..3b323411fd91 100755 --- a/ravenwood/scripts/ravenwood-stats-collector.sh +++ b/ravenwood/scripts/ravenwood-stats-collector.sh @@ -114,7 +114,7 @@ collect_apis() { collect_stats $stats " (import it as 'ravenwood_stats')" -collect_apis $apis " (import it as 'ravenwood_supported_apis')" +collect_apis $apis " (import it as 'ravenwood_supported_apis2')" cp *keep_all.txt $keep_all_dir echo "Keep all files created at:" @@ -122,4 +122,4 @@ find $keep_all_dir -type f cp *dump.txt $dump_dir echo "Dump files created at:" -find $dump_dir -type f
\ No newline at end of file +find $dump_dir -type f diff --git a/ravenwood/test-authors.md b/ravenwood/test-authors.md deleted file mode 100644 index 6d82a744bc4f..000000000000 --- a/ravenwood/test-authors.md +++ /dev/null @@ -1,193 +0,0 @@ -# Ravenwood for Test Authors - -The Ravenwood testing environment runs inside a single Java process on the host side, and provides a limited yet growing set of Android API functionality. - -Ravenwood explicitly does not support “large” integration tests that expect a fully booted Android OS. Instead, it’s more suited for “small” and “medium” tests where your code-under-test has been factored to remove dependencies on a fully booted device. - -When writing tests under Ravenwood, all Android API symbols associated with your declared `sdk_version` are available to link against using, but unsupported APIs will throw an exception. This design choice enables mocking of unsupported APIs, and supports sharing of test code to build “bivalent” test suites that run against either Ravenwood or a traditional device. - -## Manually running tests - -To run all Ravenwood tests, use: - -``` -./frameworks/base/ravenwood/scripts/run-ravenwood-tests.sh -``` - -To run a specific test, use "atest" as normal, selecting the test from a Ravenwood suite such as: - -``` -atest CtsOsTestCasesRavenwood:ParcelTest\#testSetDataCapacityNegative -``` - -## Typical test structure - -Below are the typical steps needed to add a straightforward “small” unit test: - -* Define an `android_ravenwood_test` rule in your `Android.bp` file: - -``` -android_ravenwood_test { - name: "MyTestsRavenwood", - static_libs: [ - "androidx.annotation_annotation", - "androidx.test.ext.junit", - "androidx.test.rules", - ], - srcs: [ - "src/com/example/MyCode.java", - "tests/src/com/example/MyCodeTest.java", - ], - sdk_version: "test_current", - auto_gen_config: true, -} -``` - -* Write your unit test just like you would for an Android device: - -``` -import android.platform.test.annotations.DisabledOnRavenwood; -import android.platform.test.ravenwood.RavenwoodRule; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -@RunWith(AndroidJUnit4.class) -public class MyCodeTest { - @Test - public void testSimple() { - // ... - } -} -``` - -* APIs available under Ravenwood are stateless by default. If your test requires explicit states (such as defining the UID you’re running under, or requiring a main `Looper` thread), add a `RavenwoodRule` to declare that: - -``` -import android.platform.test.annotations.DisabledOnRavenwood; -import android.platform.test.ravenwood.RavenwoodRule; - -import androidx.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -@RunWith(AndroidJUnit4.class) -public class MyCodeTest { - @Rule - public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() - .setProcessApp() - .setProvideMainThread(true) - .build(); -``` - -Once you’ve defined your test, you can use typical commands to execute it locally: - -``` -$ atest --host MyTestsRavenwood -``` - -> **Note:** There's a known bug #312525698 where `atest` currently requires a connected device to run Ravenwood tests, but that device isn't used for testing. Using the `--host` argument above is a way to bypass this requirement until the bug is fixed. - -You can also run your new tests automatically via `TEST_MAPPING` rules like this: - -``` -{ - "ravenwood-presubmit": [ - { - "name": "MyTestsRavenwood", - "host": true - } - ] -} -``` - -> **Note:** There's a known bug #308854804 where `TEST_MAPPING` is not being applied, so we're currently planning to run all Ravenwood tests unconditionally in presubmit for changes to `frameworks/base/` and `cts/` until there is a better path forward. - -## Strategies for migration/bivalent tests - -Ravenwood aims to support tests that are written in a “bivalent” way, where the same test code can be dual-compiled to run on both a real Android device and under a Ravenwood environment. - -In situations where a test method depends on API functionality not yet available under Ravenwood, we provide an annotation to quietly “ignore” that test under Ravenwood, while continuing to validate that test on real devices. The annotation can be applied to either individual methods or to an entire test class. Please note that your test class must declare a `RavenwoodRule` for the annotation to take effect. - -Test authors are encouraged to provide a `blockedBy` or `reason` argument to help future maintainers understand why a test is being ignored, and under what conditions it might be supported in the future. - -``` -@RunWith(AndroidJUnit4.class) -public class MyCodeTest { - @Rule - public final RavenwoodRule mRavenwood = new RavenwoodRule(); - - @Test - public void testSimple() { - // Simple test that runs on both devices and Ravenwood - } - - @Test - @DisabledOnRavenwood(blockedBy = PackageManager.class) - public void testComplex() { - // Complex test that runs on devices, but is ignored under Ravenwood - } -} -``` - -At the moment, the `android.content.res.Resources` subsystem isn't yet supported under Ravenwood, but you may still want to dual-compile test suites that depend on references to resources. Below is a strategy for supporting dual-compiliation, where you can "borrow" the generated resource symbols from your traditional `android_test` target: - -``` -android_test { - name: "MyTestsDevice", - resource_dirs: ["res"], -... - -android_ravenwood_test { - name: "MyTestsRavenwood", - srcs: [ - ":MyTestsDevice{.aapt.srcjar}", -... -``` - -## Strategies for unsupported APIs - -As you write tests against Ravenwood, you’ll likely discover API dependencies that aren’t supported yet. Here’s a few strategies that can help you make progress: - -* Your code-under-test may benefit from subtle dependency refactoring to reduce coupling. (For example, providing a specific `File` argument instead of deriving paths internally from a `Context` or `Environment`.) - * One common use-case is providing a directory for your test to store temporary files, which can easily be accomplished using the `Files.createTempDirectory()` API which works on both physical devices and under Ravenwood: - -``` -import java.nio.file.Files; - -@RunWith(AndroidJUnit4.class) -public class MyTest { - @Before - public void setUp() throws Exception { - File tempDir = Files.createTempDirectory("MyTest").toFile(); -... -``` - -* Although mocking code that your team doesn’t own is a generally discouraged testing practice, it can be a valuable pressure relief valve when a dependency isn’t yet supported. - -## Strategies for debugging test development - -When writing tests you may encounter odd or hard to debug behaviors. One good place to start is at the beginning of the logs stored by atest: - -``` -$ atest MyTestsRavenwood -... -Test Logs have saved in /tmp/atest_result/20231128_094010_0e90t8v8/log -Run 'atest --history' to review test result history. -``` - -The most useful logs are in the `isolated-java-logs` text file, which can typically be tab-completed by copy-pasting the logs path mentioned in the atest output: - -``` -$ less /tmp/atest_result/20231128_133105_h9al__79/log/i*/i*/isolated-java-logs* -``` - -Here are some common known issues and recommended workarounds: - -* Some code may unconditionally interact with unsupported APIs, such as via static initializers. One strategy is to shift the logic into `@Before` methods and make it conditional by testing `RavenwoodRule.isUnderRavenwood()`. -* Some code may reference API symbols not yet present in the Ravenwood runtime, such as ART or ICU internals, or APIs from Mainline modules. One strategy is to refactor to avoid these internal dependencies, but Ravenwood aims to better support them soon. - * This may also manifest as very odd behavior, such as test not being executed at all, tracked by bug #312517322 - * This may also manifest as an obscure Mockito error claiming “Mockito can only mock non-private & non-final classes” diff --git a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt index 7462cc2f384a..c035688a8c84 100644 --- a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt +++ b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt @@ -323,6 +323,10 @@ com.android.internal.graphics.cam.Frame com.android.internal.graphics.cam.HctSolver com.android.internal.graphics.ColorUtils +com.android.internal.protolog.common.LogLevel +com.android.internal.protolog.LogcatOnlyProtoLogImpl +com.android.internal.protolog.ProtoLog + com.android.internal.util.BitUtils com.android.internal.util.BitwiseInputStream com.android.internal.util.BitwiseOutputStream diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenStats.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenStats.kt index ea8c25b6833c..4cfc205d5912 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenStats.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenStats.kt @@ -24,13 +24,9 @@ import org.objectweb.asm.Opcodes import java.io.PrintWriter /** - * TODO This is for the legacy API coverage stats CSV that shows how many APIs are "supported" - * in each class with some heuristics. We created [ApiDumper] later, which dumpps all methods - * with the "supported" status. We should update the coverage dashboard to use the [ApiDumper] - * output and remove this class, once we port all the heuristics to [ApiDumper] as well. - * (For example, this class ignores non-public and/or abstract methods, but [ApiDumper] shows - * all of them in the same way. We should probably mark them as "Boring" or maybe "Ignore" - * for [ApiDumper]) + * This class is no longer used. It was used for the old ravenwood dashboard. (b/402797626) + * + * TODO: Delete the class. */ open class HostStubGenStats(val classes: ClassNodes) { data class Stats( diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt index 112ef01e20cb..741abe3df638 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt @@ -377,6 +377,10 @@ fun MethodNode.isPublic(): Boolean { return (this.access and Opcodes.ACC_PUBLIC) != 0 } +fun MethodNode.isAbstract(): Boolean { + return (this.access and Opcodes.ACC_ABSTRACT) != 0 +} + fun MethodNode.isNative(): Boolean { return (this.access and Opcodes.ACC_NATIVE) != 0 } diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/dumper/ApiDumper.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/dumper/ApiDumper.kt index bb8cdccafaa6..6ece17ffa6c2 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/dumper/ApiDumper.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/dumper/ApiDumper.kt @@ -20,6 +20,8 @@ import com.android.hoststubgen.asm.CTOR_NAME import com.android.hoststubgen.asm.ClassNodes import com.android.hoststubgen.asm.getClassNameFromFullClassName import com.android.hoststubgen.asm.getPackageNameFromFullClassName +import com.android.hoststubgen.asm.isAbstract +import com.android.hoststubgen.asm.isPublic import com.android.hoststubgen.asm.toHumanReadableClassName import com.android.hoststubgen.csvEscape import com.android.hoststubgen.filters.FilterPolicy @@ -27,8 +29,8 @@ import com.android.hoststubgen.filters.FilterPolicyWithReason import com.android.hoststubgen.filters.OutputFilter import com.android.hoststubgen.filters.StatsLabel import com.android.hoststubgen.log -import org.objectweb.asm.Type import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.MethodNode import java.io.PrintWriter /** @@ -45,19 +47,14 @@ class ApiDumper( val descriptor: String, ) - private val javaStandardApiPolicy = FilterPolicy.Keep.withReason( - "Java standard API", - StatsLabel.Supported, - ) - private val shownMethods = mutableSetOf<MethodKey>() /** * Do the dump. */ fun dump() { - pw.printf("PackageName,ClassName,FromSubclass,DeclareClass,MethodName,MethodDesc" + - ",Supported,Policy,Reason,SupportedLabel\n") + pw.printf("PackageName,ClassName,Inherited,DeclareClass,MethodName,MethodDesc" + + ",Supported,Policy,Reason,Boring\n") classes.forEach { classNode -> shownMethods.clear() @@ -72,32 +69,21 @@ class ApiDumper( methodClassName: String, methodName: String, methodDesc: String, - classPolicy: FilterPolicyWithReason, + computedMethodLabel: StatsLabel, methodPolicy: FilterPolicyWithReason, ) { - if (methodPolicy.statsLabel == StatsLabel.Ignored) { - return - } - // Label hack -- if the method is supported, but the class is boring, then the - // method is boring too. - var methodLabel = methodPolicy.statsLabel - if (methodLabel == StatsLabel.SupportedButBoring - && classPolicy.statsLabel == StatsLabel.SupportedButBoring) { - methodLabel = classPolicy.statsLabel - } - pw.printf( - "%s,%s,%d,%s,%s,%s,%d,%s,%s,%s\n", + "%s,%s,%d,%s,%s,%s,%d,%s,%s,%d\n", csvEscape(classPackage), csvEscape(className), if (isSuperClass) { 1 } else { 0 }, csvEscape(methodClassName), csvEscape(methodName), - csvEscape(methodDesc), - methodLabel.statValue, + csvEscape(methodName + methodDesc), + if (computedMethodLabel.isSupported) { 1 } else { 0 }, methodPolicy.policy, csvEscape(methodPolicy.reason), - methodLabel, + if (computedMethodLabel == StatsLabel.SupportedButBoring) { 1 } else { 0 }, ) } @@ -111,6 +97,42 @@ class ApiDumper( return false } + private fun getClassLabel(cn: ClassNode, classPolicy: FilterPolicyWithReason): StatsLabel { + if (!classPolicy.statsLabel.isSupported) { + return classPolicy.statsLabel + } + if (cn.name.endsWith("Proto") + || cn.name.endsWith("ProtoEnums") + || cn.name.endsWith("LogTags") + || cn.name.endsWith("StatsLog")) { + return StatsLabel.SupportedButBoring + } + + return classPolicy.statsLabel + } + + private fun resolveMethodLabel( + mn: MethodNode, + methodPolicy: FilterPolicyWithReason, + classLabel: StatsLabel, + ): StatsLabel { + // Class label will override the method label + if (!classLabel.isSupported) { + return classLabel + } + // If method isn't supported, just use it as-is. + if (!methodPolicy.statsLabel.isSupported) { + return methodPolicy.statsLabel + } + + // Use heuristics to override the label. + if (!mn.isPublic() || mn.isAbstract()) { + return StatsLabel.SupportedButBoring + } + + return methodPolicy.statsLabel + } + private fun dump( dumpClass: ClassNode, methodClass: ClassNode, @@ -120,9 +142,11 @@ class ApiDumper( return } log.d("Class ${dumpClass.name} -- policy $classPolicy") + val classLabel = getClassLabel(dumpClass, classPolicy) - val pkg = getPackageNameFromFullClassName(dumpClass.name).toHumanReadableClassName() - val cls = getClassNameFromFullClassName(dumpClass.name).toHumanReadableClassName() + val humanReadableClassName = dumpClass.name.toHumanReadableClassName() + val pkg = getPackageNameFromFullClassName(humanReadableClassName) + val cls = getClassNameFromFullClassName(humanReadableClassName) val isSuperClass = dumpClass != methodClass @@ -150,8 +174,12 @@ class ApiDumper( val renameTo = filter.getRenameTo(methodClass.name, method.name, method.desc) - dumpMethod(pkg, cls, isSuperClass, methodClass.name.toHumanReadableClassName(), - renameTo ?: method.name, method.desc, classPolicy, methodPolicy) + val methodLabel = resolveMethodLabel(method, methodPolicy, classLabel) + + if (methodLabel != StatsLabel.Ignored) { + dumpMethod(pkg, cls, isSuperClass, methodClass.name.toHumanReadableClassName(), + renameTo ?: method.name, method.desc, methodLabel, methodPolicy) + } } // Dump super class methods. @@ -178,51 +206,6 @@ class ApiDumper( dump(dumpClass, methodClass) return } - - // Dump overriding methods from Java standard classes, except for the Object methods, - // which are obvious. - if (methodClassName.startsWith("java/") || methodClassName.startsWith("javax/")) { - if (methodClassName != "java/lang/Object") { - dumpStandardClass(dumpClass, methodClassName) - } - return - } log.w("Super class or interface $methodClassName (used by ${dumpClass.name}) not found.") } - - /** - * Dump methods from Java standard classes. - */ - private fun dumpStandardClass( - dumpClass: ClassNode, - methodClassName: String, - ) { - val pkg = getPackageNameFromFullClassName(dumpClass.name).toHumanReadableClassName() - val cls = getClassNameFromFullClassName(dumpClass.name).toHumanReadableClassName() - - val methodClassName = methodClassName.toHumanReadableClassName() - - try { - val clazz = Class.forName(methodClassName) - - // Method.getMethods() returns only public methods, but with inherited ones. - // Method.getDeclaredMethods() returns private methods too, but no inherited methods. - // - // Since we're only interested in public ones, just use getMethods(). - clazz.methods.forEach { method -> - val methodName = method.name - val methodDesc = Type.getMethodDescriptor(method) - - // If we already printed the method from a subclass, don't print it. - if (shownAlready(methodName, methodDesc)) { - return@forEach - } - - dumpMethod(pkg, cls, true, methodClassName, - methodName, methodDesc, javaStandardApiPolicy, javaStandardApiPolicy) - } - } catch (e: ClassNotFoundException) { - log.w("JVM type $methodClassName (used by ${dumpClass.name}) not found.") - } - } } diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/FilterPolicyWithReason.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/FilterPolicyWithReason.kt index e082bbb0a119..f135c60947b3 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/FilterPolicyWithReason.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/FilterPolicyWithReason.kt @@ -32,7 +32,15 @@ enum class StatsLabel(val statValue: Int, val label: String) { SupportedButBoring(1, "Boring"), /** Entry should be shown as "supported" */ - Supported(2, "Supported"), + Supported(2, "Supported"); + + val isSupported: Boolean + get() { + return when (this) { + SupportedButBoring, Supported -> true + else -> false + } + } } /** diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 39c1fa73b7ce..b4c45aba0131 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -1823,11 +1823,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub public void notifyQuickSettingsTilesChanged( @UserIdInt int userId, @NonNull List<ComponentName> tileComponentNames) { notifyQuickSettingsTilesChanged_enforcePermission(); - if (DEBUG) { - Slog.d(LOG_TAG, TextUtils.formatSimple( - "notifyQuickSettingsTilesChanged userId: %d, tileComponentNames: %s", - userId, tileComponentNames)); - } + + Slog.d(LOG_TAG, String.format( + "notifyQuickSettingsTilesChanged userId: %s, tileComponentNames: %s", + userId, tileComponentNames)); final Set<ComponentName> newTileComponentNames = new ArraySet<>(tileComponentNames); final Set<ComponentName> addedTiles; final Set<ComponentName> removedTiles; @@ -2131,6 +2130,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub parsedAccessibilityShortcutInfos = parseAccessibilityShortcutInfos(userId); synchronized (mLock) { if (mCurrentUserId == userId && mInitialized) { + Slog.w(LOG_TAG, String.format("userId: %d is already initialized", userId)); return; } @@ -3309,10 +3309,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub * @param forceUpdate whether to force an update of the app Clients. */ private void onUserStateChangedLocked(AccessibilityUserState userState, boolean forceUpdate) { - if (DEBUG) { - Slog.v(LOG_TAG, "onUserStateChangedLocked for user " + userState.mUserId + " with " - + "forceUpdate: " + forceUpdate); - } + Slog.v(LOG_TAG, String.format("onUserStateChangedLocked for userId: %d, forceUpdate: %s", + userState.mUserId, forceUpdate)); + // TODO: Remove this hack mInitialized = true; updateLegacyCapabilitiesLocked(userState); @@ -4361,6 +4360,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private void enableShortcutForTargets( boolean enable, @UserShortcutType int shortcutType, @NonNull List<String> shortcutTargets, @UserIdInt int userId) { + Slog.d(LOG_TAG, String.format( + "enableShortcutForTargets: enable %s, shortcutType: %s, shortcutTargets: %s, " + + "userId: %s", + enable, shortcutType, shortcutTargets, userId)); if (shortcutType == UserShortcutType.GESTURE && !android.provider.Flags.a11yStandaloneGestureEnabled()) { Slog.w(LOG_TAG, @@ -4418,6 +4421,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } if (currentTargets.equals(validNewTargets)) { + Slog.d(LOG_TAG, + String.format( + "shortcutTargets are the same: skip modifying: target: %s, " + + "shortcutType: %s", + validNewTargets, shortcutType)); return; } persistColonDelimitedSetToSettingLocked( @@ -4491,6 +4499,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private void updateA11yTileServicesInQuickSettingsPanel( Set<String> newQsTargets, Set<String> currentQsTargets, @UserIdInt int userId) { + Slog.d(LOG_TAG, + String.format( + "updateA11yTileServicesInQuickSettingsPanel: newQsTargets: %s , " + + "currentQsTargets: %s, userId: %s", + newQsTargets, currentQsTargets, userId)); // Call StatusBarManager to add/remove tiles final StatusBarManagerInternal statusBarManagerInternal = LocalServices.getService(StatusBarManagerInternal.class); diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java index 60343e9e81e5..99febd6de60f 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java @@ -81,10 +81,16 @@ import com.android.server.accessibility.Flags; public class AutoclickController extends BaseEventStreamTransformation { private static final String LOG_TAG = AutoclickController.class.getSimpleName(); + // TODO(b/393559560): Finalize scroll amount. + private static final float SCROLL_AMOUNT = 1.0f; private final AccessibilityTraceManager mTrace; private final Context mContext; private final int mUserId; + @VisibleForTesting + float mLastCursorX; + @VisibleForTesting + float mLastCursorY; // Lazily created on the first mouse motion event. @VisibleForTesting ClickScheduler mClickScheduler; @@ -315,8 +321,58 @@ public class AutoclickController extends BaseEventStreamTransformation { /** * Handles scroll operations in the specified direction. */ - public void handleScroll(@AutoclickScrollPanel.ScrollDirection int direction) { - // TODO(b/388845721): Perform actual scroll. + private void handleScroll(@AutoclickScrollPanel.ScrollDirection int direction) { + final long now = SystemClock.uptimeMillis(); + + // Create pointer properties. + PointerProperties[] pointerProps = new PointerProperties[1]; + pointerProps[0] = new PointerProperties(); + pointerProps[0].id = 0; + pointerProps[0].toolType = MotionEvent.TOOL_TYPE_MOUSE; + + // Create pointer coordinates at the last cursor position. + PointerCoords[] pointerCoords = new PointerCoords[1]; + pointerCoords[0] = new PointerCoords(); + pointerCoords[0].x = mLastCursorX; + pointerCoords[0].y = mLastCursorY; + + // Set scroll values based on direction. + switch (direction) { + case AutoclickScrollPanel.DIRECTION_UP: + pointerCoords[0].setAxisValue(MotionEvent.AXIS_VSCROLL, SCROLL_AMOUNT); + break; + case AutoclickScrollPanel.DIRECTION_DOWN: + pointerCoords[0].setAxisValue(MotionEvent.AXIS_VSCROLL, -SCROLL_AMOUNT); + break; + case AutoclickScrollPanel.DIRECTION_LEFT: + pointerCoords[0].setAxisValue(MotionEvent.AXIS_HSCROLL, SCROLL_AMOUNT); + break; + case AutoclickScrollPanel.DIRECTION_RIGHT: + pointerCoords[0].setAxisValue(MotionEvent.AXIS_HSCROLL, -SCROLL_AMOUNT); + break; + case AutoclickScrollPanel.DIRECTION_EXIT: + case AutoclickScrollPanel.DIRECTION_NONE: + default: + return; + } + + // Get device ID from last motion event if possible. + int deviceId = mClickScheduler != null && mClickScheduler.mLastMotionEvent != null + ? mClickScheduler.mLastMotionEvent.getDeviceId() : 0; + + // Create a scroll event. + MotionEvent scrollEvent = MotionEvent.obtain( + /* downTime= */ now, /* eventTime= */ now, + MotionEvent.ACTION_SCROLL, /* pointerCount= */ 1, pointerProps, + pointerCoords, /* metaState= */ 0, /* actionButton= */ 0, /* xPrecision= */ + 1.0f, /* yPrecision= */ 1.0f, deviceId, /* edgeFlags= */ 0, + InputDevice.SOURCE_MOUSE, /* flags= */ 0); + + // Send the scroll event. + super.onMotionEvent(scrollEvent, scrollEvent, mClickScheduler.mEventPolicyFlags); + + // Clean up. + scrollEvent.recycle(); } /** @@ -823,13 +879,19 @@ public class AutoclickController extends BaseEventStreamTransformation { // If exit button is hovered, exit scroll mode after countdown and return early. if (mHoveredDirection == AutoclickScrollPanel.DIRECTION_EXIT) { exitScrollMode(); + return; } - return; } // Handle scroll type specially, show scroll panel instead of sending click events. if (mActiveClickType == AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL) { if (mAutoclickScrollPanel != null) { + // Save the last cursor position at the moment when sendClick() is called. + if (mClickScheduler != null && mClickScheduler.mLastMotionEvent != null) { + final int pointerIndex = mClickScheduler.mLastMotionEvent.getActionIndex(); + mLastCursorX = mClickScheduler.mLastMotionEvent.getX(pointerIndex); + mLastCursorY = mClickScheduler.mLastMotionEvent.getY(pointerIndex); + } mAutoclickScrollPanel.show(); } return; diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java index c71443149687..025423078da1 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java @@ -179,7 +179,7 @@ public class AutoclickScrollPanel { private WindowManager.LayoutParams getLayoutParams() { final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; - layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; + layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; layoutParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; layoutParams.setFitInsetsTypes(WindowInsets.Type.statusBars()); layoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; diff --git a/services/art-profile-extra b/services/art-profile-extra index 54362411e5ea..9cbc03903904 100644 --- a/services/art-profile-extra +++ b/services/art-profile-extra @@ -1 +1,9 @@ HSPLcom/android/server/am/ActivityManagerService$LocalService;->checkContentProviderAccess(Ljava/lang/String;I)Ljava/lang/String; +HSPLcom/android/server/am/ActivityManagerService$LocalService;->updateDeviceIdleTempAllowlist([IIZJIILjava/lang/String;I)V +HSPLcom/android/server/am/OomAdjuster;->setUidTempAllowlistStateLSP(IZ)V +HSPLcom/android/server/am/BatteryStatsService;->setBatteryState(IIIIIIIIJ)V +HSPLcom/android/server/pm/PackageManagerService$IPackageManagerImpl;->setComponentEnabledSetting(Landroid/content/ComponentName;IIILjava/lang/String;)V +HSPLcom/android/server/am/ActiveServices;->bindServiceLocked(Landroid/app/IApplicationThread;Landroid/os/IBinder;Landroid/content/Intent;Ljava/lang/String;Landroid/app/IServiceConnection;JLjava/lang/String;ZILjava/lang/String;Landroid/app/IApplicationThread;Ljava/lang/String;I)I +HSPLcom/android/server/accessibility/AccessibilityManagerService;->onServiceInfoChangedLocked(Lcom/android/server/accessibility/AccessibilityUserState;)V +HSPLcom/android/server/clipboard/ClipboardService$ClipboardImpl;->checkAndSetPrimaryClip(Landroid/content/ClipData;Ljava/lang/String;Ljava/lang/String;IILjava/lang/String;)V +HSPLcom/android/server/clipboard/ClipboardService$ClipboardImpl;->getPrimaryClip(Ljava/lang/String;Ljava/lang/String;II)Landroid/content/ClipData; diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index b39b5b1a7660..9b735d70abb5 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java @@ -26,6 +26,7 @@ import static android.view.autofill.AutofillManager.FLAG_ADD_CLIENT_ENABLED_FOR_ import static android.view.autofill.AutofillManager.NO_SESSION; import static android.view.autofill.AutofillManager.RECEIVER_FLAG_SESSION_FOR_AUGMENTED_AUTOFILL_ONLY; +import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; import static com.android.server.autofill.Helper.sDebug; import static com.android.server.autofill.Helper.sVerbose; @@ -761,20 +762,16 @@ final class AutofillManagerServiceImpl return false; } - @GuardedBy("mLock") - void removeSessionLocked(int sessionId) { - mSessions.remove(sessionId); - if (Flags.autofillSessionDestroyed()) { - if (sVerbose) { - Slog.v( - TAG, - "removeSessionLocked(): removed " + sessionId); - } + void callOnSessionDestroyed(int sessionId) { + if (sVerbose) { + Slog.v(TAG, "removeSessionLocked(): removed " + sessionId); + } + synchronized (mLock) { FillEventHistory history = null; if (AutofillFeatureFlags.isMultipleFillEventHistoryEnabled() - && mFillHistories != null) { + && mFillHistories != null) { history = mFillHistories.get(sessionId); mFillHistories.delete(sessionId); } @@ -806,6 +803,16 @@ final class AutofillManagerServiceImpl } } + @GuardedBy("mLock") + void removeSessionLocked(int sessionId) { + mSessions.remove(sessionId); + if (Flags.autofillSessionDestroyed()) { + mHandler.sendMessage( + obtainMessage( + AutofillManagerServiceImpl::callOnSessionDestroyed, this, sessionId)); + } + } + /** * Ges the previous sessions asked to be kept alive in a given activity task. * diff --git a/services/companion/java/com/android/server/companion/utils/MetricUtils.java b/services/companion/java/com/android/server/companion/utils/MetricUtils.java index cfa7cb00dfac..91f7a3f23a1b 100644 --- a/services/companion/java/com/android/server/companion/utils/MetricUtils.java +++ b/services/companion/java/com/android/server/companion/utils/MetricUtils.java @@ -21,7 +21,7 @@ import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PRO import static android.companion.AssociationRequest.DEVICE_PROFILE_COMPUTER; import static android.companion.AssociationRequest.DEVICE_PROFILE_GLASSES; import static android.companion.AssociationRequest.DEVICE_PROFILE_NEARBY_DEVICE_STREAMING; -import static android.companion.AssociationRequest.DEVICE_PROFILE_SENSOR_DEVICE_STREAMING; +import static android.companion.AssociationRequest.DEVICE_PROFILE_VIRTUAL_DEVICE; import static android.companion.AssociationRequest.DEVICE_PROFILE_WATCH; import static android.companion.AssociationRequest.DEVICE_PROFILE_WEARABLE_SENSING; @@ -33,8 +33,8 @@ import static com.android.internal.util.FrameworkStatsLog.CDM_ASSOCIATION_ACTION import static com.android.internal.util.FrameworkStatsLog.CDM_ASSOCIATION_ACTION__DEVICE_PROFILE__DEVICE_PROFILE_COMPUTER; import static com.android.internal.util.FrameworkStatsLog.CDM_ASSOCIATION_ACTION__DEVICE_PROFILE__DEVICE_PROFILE_GLASSES; import static com.android.internal.util.FrameworkStatsLog.CDM_ASSOCIATION_ACTION__DEVICE_PROFILE__DEVICE_PROFILE_NEARBY_DEVICE_STREAMING; -import static com.android.internal.util.FrameworkStatsLog.CDM_ASSOCIATION_ACTION__DEVICE_PROFILE__DEVICE_PROFILE_SENSOR_DEVICE_STREAMING; import static com.android.internal.util.FrameworkStatsLog.CDM_ASSOCIATION_ACTION__DEVICE_PROFILE__DEVICE_PROFILE_NULL; +import static com.android.internal.util.FrameworkStatsLog.CDM_ASSOCIATION_ACTION__DEVICE_PROFILE__DEVICE_PROFILE_VIRTUAL_DEVICE; import static com.android.internal.util.FrameworkStatsLog.CDM_ASSOCIATION_ACTION__DEVICE_PROFILE__DEVICE_PROFILE_WATCH; import static com.android.internal.util.FrameworkStatsLog.CDM_ASSOCIATION_ACTION__DEVICE_PROFILE__DEVICE_PROFILE_WEARABLE_SENSING; import static com.android.internal.util.FrameworkStatsLog.write; @@ -76,8 +76,8 @@ public final class MetricUtils { CDM_ASSOCIATION_ACTION__DEVICE_PROFILE__DEVICE_PROFILE_NEARBY_DEVICE_STREAMING ); map.put( - DEVICE_PROFILE_SENSOR_DEVICE_STREAMING, - CDM_ASSOCIATION_ACTION__DEVICE_PROFILE__DEVICE_PROFILE_SENSOR_DEVICE_STREAMING + DEVICE_PROFILE_VIRTUAL_DEVICE, + CDM_ASSOCIATION_ACTION__DEVICE_PROFILE__DEVICE_PROFILE_VIRTUAL_DEVICE ); map.put( DEVICE_PROFILE_WEARABLE_SENSING, diff --git a/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java b/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java index f4128b820d8f..7157795d8998 100644 --- a/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java +++ b/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java @@ -28,7 +28,7 @@ import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PRO import static android.companion.AssociationRequest.DEVICE_PROFILE_COMPUTER; import static android.companion.AssociationRequest.DEVICE_PROFILE_GLASSES; import static android.companion.AssociationRequest.DEVICE_PROFILE_NEARBY_DEVICE_STREAMING; -import static android.companion.AssociationRequest.DEVICE_PROFILE_SENSOR_DEVICE_STREAMING; +import static android.companion.AssociationRequest.DEVICE_PROFILE_VIRTUAL_DEVICE; import static android.companion.AssociationRequest.DEVICE_PROFILE_WATCH; import static android.companion.AssociationRequest.DEVICE_PROFILE_WEARABLE_SENSING; import static android.content.pm.PackageManager.PERMISSION_GRANTED; @@ -87,8 +87,8 @@ public final class PermissionsUtils { map.put(DEVICE_PROFILE_GLASSES, Manifest.permission.REQUEST_COMPANION_PROFILE_GLASSES); map.put(DEVICE_PROFILE_NEARBY_DEVICE_STREAMING, Manifest.permission.REQUEST_COMPANION_PROFILE_NEARBY_DEVICE_STREAMING); - map.put(DEVICE_PROFILE_SENSOR_DEVICE_STREAMING, - Manifest.permission.REQUEST_COMPANION_PROFILE_SENSOR_DEVICE_STREAMING); + map.put(DEVICE_PROFILE_VIRTUAL_DEVICE, + Manifest.permission.REQUEST_COMPANION_PROFILE_VIRTUAL_DEVICE); DEVICE_PROFILE_TO_PERMISSION = unmodifiableMap(map); } diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index caf535ce7a40..b90f910cf759 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -105,7 +105,7 @@ public class VirtualDeviceManagerService extends SystemService { AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION, AssociationRequest.DEVICE_PROFILE_APP_STREAMING, AssociationRequest.DEVICE_PROFILE_NEARBY_DEVICE_STREAMING, - AssociationRequest.DEVICE_PROFILE_SENSOR_DEVICE_STREAMING); + AssociationRequest.DEVICE_PROFILE_VIRTUAL_DEVICE); /** Enable default device camera access for apps running on virtual devices. */ @ChangeId diff --git a/services/core/Android.bp b/services/core/Android.bp index cf85dd957b3f..8a983f9e071d 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -263,6 +263,7 @@ java_library_static { "profiling_flags_lib", "android.adpf.sessionmanager_aidl-java", "uprobestats_flags_java_lib", + "clipboard_flags_lib", ], javac_shard_size: 50, javacflags: [ diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java index 1d914c89c570..6ac2180176ce 100644 --- a/services/core/java/com/android/server/BinaryTransparencyService.java +++ b/services/core/java/com/android/server/BinaryTransparencyService.java @@ -85,6 +85,8 @@ import com.android.internal.os.IBinaryTransparencyService; import com.android.internal.util.FrameworkStatsLog; import com.android.modules.expresslog.Histogram; import com.android.server.pm.ApexManager; +import com.android.server.pm.BackgroundInstallControlCallbackHelper; +import com.android.server.pm.BackgroundInstallControlService; import com.android.server.pm.pkg.AndroidPackage; import com.android.server.pm.pkg.AndroidPackageSplit; import com.android.server.pm.pkg.PackageState; @@ -101,9 +103,6 @@ import java.util.Map; import java.util.concurrent.Executors; import java.util.stream.Collectors; -import com.android.server.pm.BackgroundInstallControlService; -import com.android.server.pm.BackgroundInstallControlCallbackHelper; - /** * @hide */ @@ -1577,19 +1576,17 @@ public class BinaryTransparencyService extends SystemService { Slog.d(TAG, String.format("VBMeta Digest: %s", mVbmetaDigest)); FrameworkStatsLog.write(FrameworkStatsLog.VBMETA_DIGEST_REPORTED, mVbmetaDigest); - if (android.security.Flags.binaryTransparencySepolicyHash()) { - IoThread.getExecutor().execute(() -> { - byte[] sepolicyHash = PackageUtils.computeSha256DigestForLargeFileAsBytes( - "/sys/fs/selinux/policy", PackageUtils.createLargeFileBuffer()); - String sepolicyHashEncoded = null; - if (sepolicyHash != null) { - sepolicyHashEncoded = HexEncoding.encodeToString(sepolicyHash, false); - Slog.d(TAG, "sepolicy hash: " + sepolicyHashEncoded); - } - FrameworkStatsLog.write(FrameworkStatsLog.BOOT_INTEGRITY_INFO_REPORTED, - sepolicyHashEncoded, mVbmetaDigest); - }); - } + IoThread.getExecutor().execute(() -> { + byte[] sepolicyHash = PackageUtils.computeSha256DigestForLargeFileAsBytes( + "/sys/fs/selinux/policy", PackageUtils.createLargeFileBuffer()); + String sepolicyHashEncoded = null; + if (sepolicyHash != null) { + sepolicyHashEncoded = HexEncoding.encodeToString(sepolicyHash, false); + Slog.d(TAG, "sepolicy hash: " + sepolicyHashEncoded); + } + FrameworkStatsLog.write(FrameworkStatsLog.BOOT_INTEGRITY_INFO_REPORTED, + sepolicyHashEncoded, mVbmetaDigest); + }); } /** diff --git a/services/core/java/com/android/server/GestureLauncherService.java b/services/core/java/com/android/server/GestureLauncherService.java index 87222a60d82d..28258ae47a65 100644 --- a/services/core/java/com/android/server/GestureLauncherService.java +++ b/services/core/java/com/android/server/GestureLauncherService.java @@ -19,7 +19,6 @@ package com.android.server; import static android.service.quickaccesswallet.Flags.launchWalletOptionOnPowerDoubleTap; import static android.service.quickaccesswallet.Flags.launchWalletViaSysuiCallbacks; -import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow; import static com.android.internal.R.integer.config_defaultMinEmergencyGestureTapDurationMillis; import android.app.ActivityManager; @@ -635,46 +634,6 @@ public class GestureLauncherService extends SystemService { } /** - * Processes a power key event in GestureLauncherService without performing an action. This - * method is called on every KEYCODE_POWER ACTION_DOWN event and ensures that, even if - * KEYCODE_POWER events are passed to and handled by the app, the GestureLauncherService still - * keeps track of all running KEYCODE_POWER events for its gesture detection and relevant - * actions. - */ - public void processPowerKeyDown(KeyEvent event) { - if (mEmergencyGestureEnabled && mEmergencyGesturePowerButtonCooldownPeriodMs >= 0 - && event.getEventTime() - mLastEmergencyGestureTriggered - < mEmergencyGesturePowerButtonCooldownPeriodMs) { - return; - } - if (event.isLongPress()) { - return; - } - - final long powerTapInterval; - - synchronized (this) { - powerTapInterval = event.getEventTime() - mLastPowerDown; - mLastPowerDown = event.getEventTime(); - if (powerTapInterval >= POWER_SHORT_TAP_SEQUENCE_MAX_INTERVAL_MS) { - // Tap too slow, reset consecutive tap counts. - mFirstPowerDown = event.getEventTime(); - mPowerButtonConsecutiveTaps = 1; - mPowerButtonSlowConsecutiveTaps = 1; - } else if (powerTapInterval >= POWER_DOUBLE_TAP_MAX_TIME_MS) { - // Tap too slow for shortcuts - mFirstPowerDown = event.getEventTime(); - mPowerButtonConsecutiveTaps = 1; - mPowerButtonSlowConsecutiveTaps++; - } else if (!overridePowerKeyBehaviorInFocusedWindow() || powerTapInterval > 0) { - // Fast consecutive tap - mPowerButtonConsecutiveTaps++; - mPowerButtonSlowConsecutiveTaps++; - } - } - } - - /** * Attempts to intercept power key down event by detecting certain gesture patterns * * @param interactive true if the event's policy contains {@code FLAG_INTERACTIVE} @@ -721,7 +680,7 @@ public class GestureLauncherService extends SystemService { mFirstPowerDown = event.getEventTime(); mPowerButtonConsecutiveTaps = 1; mPowerButtonSlowConsecutiveTaps++; - } else if (powerTapInterval > 0) { + } else { // Fast consecutive tap mPowerButtonConsecutiveTaps++; mPowerButtonSlowConsecutiveTaps++; diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java index 03d6c8b695a0..71fa8cdfeb37 100644 --- a/services/core/java/com/android/server/SystemConfig.java +++ b/services/core/java/com/android/server/SystemConfig.java @@ -1697,14 +1697,12 @@ public class SystemConfig { } } break; case "require-strict-signature": { - if (android.security.Flags.extendVbChainToUpdatedApk()) { - String packageName = parser.getAttributeValue(null, "package"); - if (TextUtils.isEmpty(packageName)) { - Slog.w(TAG, "<" + name + "> without valid package in " + permFile - + " at " + parser.getPositionDescription()); - } else { - mPreinstallPackagesWithStrictSignatureCheck.add(packageName); - } + String packageName = parser.getAttributeValue(null, "package"); + if (TextUtils.isEmpty(packageName)) { + Slog.w(TAG, "<" + name + "> without valid package in " + permFile + + " at " + parser.getPositionDescription()); + } else { + mPreinstallPackagesWithStrictSignatureCheck.add(packageName); } } break; case "oem-defined-uid": { diff --git a/services/core/java/com/android/server/TradeInModeService.java b/services/core/java/com/android/server/TradeInModeService.java index 1a9e02c86560..6cf9b7b33674 100644 --- a/services/core/java/com/android/server/TradeInModeService.java +++ b/services/core/java/com/android/server/TradeInModeService.java @@ -44,7 +44,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; - public final class TradeInModeService extends SystemService { private static final String TAG = "TradeInModeService"; @@ -129,7 +128,7 @@ public final class TradeInModeService extends SystemService { @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE) public boolean start() { mContext.enforceCallingOrSelfPermission("android.permission.ENTER_TRADE_IN_MODE", - "Cannot enter trade-in mode foyer"); + "Cannot enter trade-in mode foyer"); final int state = getTradeInModeState(); if (state == TIM_STATE_FOYER) { return true; @@ -168,7 +167,7 @@ public final class TradeInModeService extends SystemService { @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE) public boolean enterEvaluationMode() { mContext.enforceCallingOrSelfPermission("android.permission.ENTER_TRADE_IN_MODE", - "Cannot enter trade-in evaluation mode"); + "Cannot enter trade-in evaluation mode"); final int state = getTradeInModeState(); if (state != TIM_STATE_FOYER) { Slog.e(TAG, "Cannot enter evaluation mode in state: " + state); @@ -199,7 +198,7 @@ public final class TradeInModeService extends SystemService { @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE) public boolean isEvaluationModeAllowed() { mContext.enforceCallingOrSelfPermission("android.permission.ENTER_TRADE_IN_MODE", - "Cannot test for trade-in evaluation mode allowed"); + "Cannot test for trade-in evaluation mode allowed"); return !isFrpActive(); } @@ -246,7 +245,7 @@ public final class TradeInModeService extends SystemService { private void enforceTestingPermissions() { mContext.enforceCallingOrSelfPermission("android.permission.ENTER_TRADE_IN_MODE", - "Caller must have ENTER_TRADE_IN_MODE permission"); + "Caller must have ENTER_TRADE_IN_MODE permission"); if (!isDebuggable()) { throw new SecurityException("ro.debuggable must be set to 1"); } @@ -302,7 +301,7 @@ public final class TradeInModeService extends SystemService { private boolean scheduleTradeInModeWipe() { try (FileWriter fw = new FileWriter(WIPE_INDICATOR_FILE, - StandardCharsets.US_ASCII)) { + StandardCharsets.US_ASCII)) { fw.write("0"); } catch (IOException e) { Slog.e(TAG, "Failed to write " + WIPE_INDICATOR_FILE, e); @@ -339,8 +338,7 @@ public final class TradeInModeService extends SystemService { private boolean isFrpActive() { try { - PersistentDataBlockManager pdb = - mContext.getSystemService(PersistentDataBlockManager.class); + PersistentDataBlockManager pdb = mContext.getSystemService(PersistentDataBlockManager.class); if (pdb == null) { return false; } @@ -351,9 +349,9 @@ public final class TradeInModeService extends SystemService { } } - // This returns true if the device has progressed far enough into Setup Wizard that it no - // longer makes sense to enable trade-in mode. As a last stop, we check the SUW completion - // bits. + // This returns true if the device has progressed far enough into Setup Wizard + // that it no longer makes sense to enable trade-in mode. As a last stop, we + // check the SUW completion bits. private boolean isDeviceSetup() { final ContentResolver cr = mContext.getContentResolver(); try { @@ -395,14 +393,13 @@ public final class TradeInModeService extends SystemService { cr.registerContentObserver(deviceProvisioned, false, observer); } - private void watchForNetworkChange() { mConnectivityManager = mContext.getSystemService(ConnectivityManager.class); NetworkRequest networkRequest = new NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) - .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) - .build(); + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .build(); mNetworkCallback = new ConnectivityManager.NetworkCallback() { @Override diff --git a/services/core/java/com/android/server/am/AppProfiler.java b/services/core/java/com/android/server/am/AppProfiler.java index 225c7ca2ca9e..83db027e1b41 100644 --- a/services/core/java/com/android/server/am/AppProfiler.java +++ b/services/core/java/com/android/server/am/AppProfiler.java @@ -2264,8 +2264,8 @@ public class AppProfiler { final int idleTime = mProcessCpuTracker.getLastIdleTime(); bstats.addCpuStatsLocked(totalUTime, totalSTime, userTime, systemTime, iowaitTime, irqTime, softIrqTime, idleTime); + bstats.finishAddingCpuStatsLocked(); } - bstats.finishAddingCpuStatsLocked(); } if (mLastWriteTime < (now - BATTERY_STATS_TIME)) { diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index 5ff6999e40b3..d1225d4e6eed 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -211,7 +211,6 @@ public final class BatteryStatsService extends IBatteryStats.Stub private static final int POWER_STATS_QUERY_TIMEOUT_MILLIS = 2000; private static final String DEVICE_CONFIG_NAMESPACE = "backstage_power"; private static final String MIN_CONSUMED_POWER_THRESHOLD_KEY = "min_consumed_power_threshold"; - private static final String EMPTY = "Empty"; private final HandlerThread mHandlerThread; private final Handler mHandler; @@ -336,55 +335,6 @@ public final class BatteryStatsService extends IBatteryStats.Stub } } - @Override - public String getSubsystemLowPowerStats() { - synchronized (mPowerStatsLock) { - if (mPowerStatsInternal == null || mEntityNames.isEmpty() || mStateNames.isEmpty()) { - return EMPTY; - } - } - - final StateResidencyResult[] results; - try { - results = mPowerStatsInternal.getStateResidencyAsync(new int[0]) - .get(POWER_STATS_QUERY_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - } catch (Exception e) { - Slog.e(TAG, "Failed to getStateResidencyAsync", e); - return EMPTY; - } - - if (results == null || results.length == 0) return EMPTY; - - int charsLeft = MAX_LOW_POWER_STATS_SIZE; - StringBuilder builder = new StringBuilder("SubsystemPowerState"); - for (int i = 0; i < results.length; i++) { - final StateResidencyResult result = results[i]; - StringBuilder subsystemBuilder = new StringBuilder(); - subsystemBuilder.append(" subsystem_" + i); - subsystemBuilder.append(" name=" + mEntityNames.get(result.id)); - - for (int j = 0; j < result.stateResidencyData.length; j++) { - final StateResidency stateResidency = result.stateResidencyData[j]; - subsystemBuilder.append(" state_" + j); - subsystemBuilder.append(" name=" + mStateNames.get(result.id).get( - stateResidency.id)); - subsystemBuilder.append(" time=" + stateResidency.totalTimeInStateMs); - subsystemBuilder.append(" count=" + stateResidency.totalStateEntryCount); - subsystemBuilder.append(" last entry=" + stateResidency.lastEntryTimestampMs); - } - - if (subsystemBuilder.length() <= charsLeft) { - charsLeft -= subsystemBuilder.length(); - builder.append(subsystemBuilder); - } else { - Slog.e(TAG, "getSubsystemLowPowerStats: buffer not enough"); - break; - } - } - - return builder.toString(); - } - private ConnectivityManager.NetworkCallback mNetworkCallback = new ConnectivityManager.NetworkCallback() { @Override @@ -3418,6 +3368,14 @@ public final class BatteryStatsService extends IBatteryStats.Stub return; } else if ("-a".equals(arg)) { flags |= BatteryStats.DUMP_VERBOSE; + } else if ("--debug".equals(arg)) { + i++; + if (i >= args.length) { + pw.println("Missing time argument for --flags HEX"); + dumpHelp(pw); + return; + } + flags |= ParseUtils.parseIntWithBase(args[i], 16, 0); } else if (arg.length() > 0 && arg.charAt(0) == '-'){ pw.println("Unknown option: " + arg); dumpHelp(pw); diff --git a/services/core/java/com/android/server/am/BroadcastHistory.java b/services/core/java/com/android/server/am/BroadcastHistory.java index 700cf9c8deb8..9f7e5cdb7900 100644 --- a/services/core/java/com/android/server/am/BroadcastHistory.java +++ b/services/core/java/com/android/server/am/BroadcastHistory.java @@ -20,6 +20,9 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Intent; import android.os.Bundle; +import android.os.Trace; +import android.util.ArrayMap; +import android.util.SparseArray; import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; @@ -83,20 +86,56 @@ public class BroadcastHistory { final long[] mSummaryHistoryDispatchTime; final long[] mSummaryHistoryFinishTime; + /** + * Map of uids to number of pending broadcasts it sent. + */ + private final SparseArray<ArrayMap<String, Integer>> mPendingBroadcastCountsPerUid = + new SparseArray<>(); + void onBroadcastFrozenLocked(@NonNull BroadcastRecord r) { mFrozenBroadcasts.add(r); } void onBroadcastEnqueuedLocked(@NonNull BroadcastRecord r) { mFrozenBroadcasts.remove(r); - mPendingBroadcasts.add(r); + if (mPendingBroadcasts.add(r)) { + updatePendingBroadcastCounterAndLogToTrace(r, /* delta= */ 1); + } } void onBroadcastFinishedLocked(@NonNull BroadcastRecord r) { - mPendingBroadcasts.remove(r); + if (mPendingBroadcasts.remove(r)) { + updatePendingBroadcastCounterAndLogToTrace(r, /* delta= */ -1); + } addBroadcastToHistoryLocked(r); } + private void updatePendingBroadcastCounterAndLogToTrace(@NonNull BroadcastRecord r, + int delta) { + ArrayMap<String, Integer> pendingBroadcastCounts = + mPendingBroadcastCountsPerUid.get(r.callingUid); + if (pendingBroadcastCounts == null) { + pendingBroadcastCounts = new ArrayMap<>(); + mPendingBroadcastCountsPerUid.put(r.callingUid, pendingBroadcastCounts); + } + final String callerPackage = r.callerPackage == null ? "null" : r.callerPackage; + final Integer currentCount = pendingBroadcastCounts.get(callerPackage); + final int newCount = (currentCount == null ? 0 : currentCount) + delta; + if (newCount == 0) { + pendingBroadcastCounts.remove(callerPackage); + if (pendingBroadcastCounts.isEmpty()) { + mPendingBroadcastCountsPerUid.remove(r.callingUid); + } + } else { + pendingBroadcastCounts.put(callerPackage, newCount); + } + + Trace.instantForTrack(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Broadcasts pending per uid", + callerPackage + "/" + r.callingUid + ":" + newCount); + Trace.traceCounter(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Broadcasts pending", + mPendingBroadcasts.size()); + } + public void addBroadcastToHistoryLocked(@NonNull BroadcastRecord original) { // Note sometimes (only for sticky broadcasts?) we reuse BroadcastRecords, // So don't change the incoming record directly. diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java index ac30be99979e..e6bc36a3ed4c 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -668,12 +668,13 @@ public final class PendingIntentRecord extends IIntentSender.Stub { getBackgroundStartPrivilegesForActivitySender( mAllowBgActivityStartsForBroadcastSender, allowlistToken, options, callingUid); + final Bundle extras = createSafeActivityOptionsBundle(options); // If a completion callback has been requested, require // that the broadcast be delivered synchronously int sent = controller.mAmInternal.broadcastIntentInPackage(key.packageName, key.featureId, uid, callingUid, callingPid, finalIntent, resolvedType, finishedReceiverThread, finishedReceiver, code, null, - null, requiredPermission, options, (finishedReceiver != null), + extras, requiredPermission, options, (finishedReceiver != null), false, userId, backgroundStartPrivileges, null /* broadcastAllowList */); if (sent == ActivityManager.BROADCAST_SUCCESS) { @@ -716,6 +717,32 @@ public final class PendingIntentRecord extends IIntentSender.Stub { return res; } + /** + * Creates a safe ActivityOptions bundle with only the launchDisplayId set. + * + * <p>This prevents unintended data from being sent to the app process. The resulting bundle + * is then used by {@link ActivityThread#createDisplayContextIfNeeded} to create a display + * context for the {@link BroadcastReceiver}, ensuring that activities launched from the + * receiver's context are started on the correct display. + * + * @param optionsBundle The original ActivityOptions bundle. + * @return A new bundle containing only the launchDisplayId from the original options, or null + * if the original bundle is null. + */ + @Nullable + private Bundle createSafeActivityOptionsBundle(@Nullable Bundle optionsBundle) { + if (!com.android.window.flags.Flags.supportWidgetIntentsOnConnectedDisplay()) { + return null; + } + if (optionsBundle == null) { + return null; + } + final ActivityOptions options = ActivityOptions.fromBundle(optionsBundle); + return ActivityOptions.makeBasic() + .setLaunchDisplayId(options.getLaunchDisplayId()) + .toBundle(); + } + @VisibleForTesting BackgroundStartPrivileges getBackgroundStartPrivilegesForActivitySender( IBinder allowlistToken) { return mAllowBgActivityStartsForActivitySender.contains(allowlistToken) diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index c2ed4d557e69..4eadab27aa26 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -317,8 +317,6 @@ public class SettingsToPropertiesMapper { private final String[] mDeviceConfigScopes; - private final String[] mDeviceConfigAconfigScopes; - private final ContentResolver mContentResolver; @VisibleForTesting @@ -329,7 +327,6 @@ public class SettingsToPropertiesMapper { mContentResolver = contentResolver; mGlobalSettings = globalSettings; mDeviceConfigScopes = deviceConfigScopes; - mDeviceConfigAconfigScopes = deviceConfigAconfigScopes; } @VisibleForTesting @@ -375,36 +372,6 @@ public class SettingsToPropertiesMapper { return; } setProperty(propertyName, properties.getString(key, null)); - - // for legacy namespaces, they can also be used for trunk stable - // purposes. so push flag also into trunk stable slot in sys prop, - // later all legacy usage will be refactored and the sync to old - // sys prop slot can be removed. - String aconfigPropertyName = makeAconfigFlagPropertyName(scope, key); - if (aconfigPropertyName == null) { - logErr("unable to construct system property for " + scope + "/" - + key); - return; - } - setProperty(aconfigPropertyName, properties.getString(key, null)); - } - }); - } - - for (String deviceConfigAconfigScope : mDeviceConfigAconfigScopes) { - DeviceConfig.addOnPropertiesChangedListener( - deviceConfigAconfigScope, - AsyncTask.THREAD_POOL_EXECUTOR, - (DeviceConfig.Properties properties) -> { - String scope = properties.getNamespace(); - for (String key : properties.getKeyset()) { - String aconfigPropertyName = makeAconfigFlagPropertyName(scope, key); - if (aconfigPropertyName == null) { - logErr("unable to construct system property for " + scope + "/" - + key); - return; - } - setProperty(aconfigPropertyName, properties.getString(key, null)); } }); } @@ -420,34 +387,6 @@ public class SettingsToPropertiesMapper { stageFlagsInNewStorage(properties); return; } - - for (String flagName : properties.getKeyset()) { - String flagValue = properties.getString(flagName, null); - if (flagName == null || flagValue == null) { - continue; - } - - int idx = flagName.indexOf(NAMESPACE_REBOOT_STAGING_DELIMITER); - if (idx == -1 || idx == flagName.length() - 1 || idx == 0) { - logErr("invalid staged flag: " + flagName); - continue; - } - - String actualNamespace = flagName.substring(0, idx); - String actualFlagName = flagName.substring(idx+1); - String propertyName = "next_boot." + makeAconfigFlagPropertyName( - actualNamespace, actualFlagName); - - if (Flags.supportLocalOverridesSysprops()) { - // Don't propagate if there is a local override. - String overrideName = actualNamespace + ":" + actualFlagName; - if (DeviceConfig.getProperty(NAMESPACE_LOCAL_OVERRIDES, overrideName) != null) { - continue; - } - } - setProperty(propertyName, flagValue); - } - }); // add prop sync callback for flag local overrides @@ -459,42 +398,6 @@ public class SettingsToPropertiesMapper { setLocalOverridesInNewStorage(properties); return; } - - if (Flags.supportLocalOverridesSysprops()) { - String overridesNamespace = properties.getNamespace(); - for (String key : properties.getKeyset()) { - String realNamespace = key.split(":")[0]; - String realFlagName = key.split(":")[1]; - String aconfigPropertyName = - makeAconfigFlagPropertyName(realNamespace, realFlagName); - if (aconfigPropertyName == null) { - logErr("unable to construct system property for " + realNamespace + "/" - + key); - return; - } - - if (properties.getString(key, null) == null) { - String deviceConfigValue = - DeviceConfig.getProperty(realNamespace, realFlagName); - String stagedDeviceConfigValue = - DeviceConfig.getProperty(NAMESPACE_REBOOT_STAGING, - realNamespace + "*" + realFlagName); - - setProperty(aconfigPropertyName, deviceConfigValue); - if (stagedDeviceConfigValue == null) { - setProperty("next_boot." + aconfigPropertyName, deviceConfigValue); - } else { - setProperty("next_boot." + aconfigPropertyName, stagedDeviceConfigValue); - } - } else { - // Otherwise, propagate the override to sysprops. - setProperty(aconfigPropertyName, properties.getString(key, null)); - // If there's a staged value, make sure it's the override value. - setProperty("next_boot." + aconfigPropertyName, - properties.getString(key, null)); - } - } - } }); } @@ -822,28 +725,6 @@ public class SettingsToPropertiesMapper { sendAconfigdRequests(requests); } - /** - * system property name constructing rule for aconfig flags: - * "persist.device_config.aconfig_flags.[category_name].[flag_name]". - * If the name contains invalid characters or substrings for system property name, - * will return null. - * @param categoryName - * @param flagName - * @return - */ - @VisibleForTesting - static String makeAconfigFlagPropertyName(String categoryName, String flagName) { - String propertyName = SYSTEM_PROPERTY_PREFIX + "aconfig_flags." + - categoryName + "." + flagName; - - if (!propertyName.matches(SYSTEM_PROPERTY_VALID_CHARACTERS_REGEX) - || propertyName.contains(SYSTEM_PROPERTY_INVALID_SUBSTRING)) { - return null; - } - - return propertyName; - } - private void setProperty(String key, String value) { // Check if need to clear the property if (value == null) { diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 5ecac2253b49..2e229ca9d10f 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -55,7 +55,6 @@ import static android.app.AppOpsManager.SAMPLING_STRATEGY_RARELY_USED; import static android.app.AppOpsManager.SAMPLING_STRATEGY_UNIFORM; import static android.app.AppOpsManager.SAMPLING_STRATEGY_UNIFORM_OPS; import static android.app.AppOpsManager.SECURITY_EXCEPTION_ON_INVALID_ATTRIBUTION_TAG_CHANGE; -import static android.app.AppOpsManager.UID_STATE_NONEXISTENT; import static android.app.AppOpsManager.WATCH_FOREGROUND_CHANGES; import static android.app.AppOpsManager._NUM_OP; import static android.app.AppOpsManager.extractFlagsFromKey; @@ -464,7 +463,19 @@ public class AppOpsService extends IAppOpsService.Stub { Clock.SYSTEM_CLOCK, mConstants); mUidStateTracker.addUidStateChangedCallback(new HandlerExecutor(mHandler), - this::onUidStateChanged); + new AppOpsUidStateTracker.UidStateChangedCallback() { + @Override + public void onUidStateChanged(int uid, int uidState, + boolean foregroundModeMayChange) { + AppOpsService.this + .onUidStateChanged(uid, uidState, foregroundModeMayChange); + } + + @Override + public void onUidProcessDeath(int uid) { + AppOpsService.this.onUidProcessDeath(uid); + } + }); } return mUidStateTracker; } @@ -1500,9 +1511,6 @@ public class AppOpsService extends IAppOpsService.Stub { // The callback method from AppOpsUidStateTracker private void onUidStateChanged(int uid, int state, boolean foregroundModeMayChange) { synchronized (this) { - if (state == UID_STATE_NONEXISTENT) { - onUidProcessDeathLocked(uid); - } UidState uidState = getUidStateLocked(uid, false); boolean hasForegroundWatchers = false; @@ -1590,11 +1598,6 @@ public class AppOpsService extends IAppOpsService.Stub { } } - if (state == UID_STATE_NONEXISTENT) { - // For UID_STATE_NONEXISTENT, we don't call onUidStateChanged for AttributedOps - return; - } - if (uidState != null) { int numPkgs = uidState.pkgOps.size(); for (int pkgNum = 0; pkgNum < numPkgs; pkgNum++) { @@ -1619,31 +1622,32 @@ public class AppOpsService extends IAppOpsService.Stub { } } - @GuardedBy("this") - private void onUidProcessDeathLocked(int uid) { - if (!mUidStates.contains(uid) || !Flags.finishRunningOpsForKilledPackages()) { - return; - } - final SparseLongArray chainsToFinish = new SparseLongArray(); - doForAllAttributedOpsInUidLocked(uid, (attributedOp) -> { - attributedOp.doForAllInProgressStartOpEvents((event) -> { - if (event == null) { - return; - } - int chainId = event.getAttributionChainId(); - if (chainId != ATTRIBUTION_CHAIN_ID_NONE) { - long currentEarliestStartTime = - chainsToFinish.get(chainId, Long.MAX_VALUE); - if (event.getStartTime() < currentEarliestStartTime) { - // Store the earliest chain link we're finishing, so that we can go back - // and finish any links in the chain that started after this one - chainsToFinish.put(chainId, event.getStartTime()); + private void onUidProcessDeath(int uid) { + synchronized (this) { + if (!mUidStates.contains(uid) || !Flags.finishRunningOpsForKilledPackages()) { + return; + } + final SparseLongArray chainsToFinish = new SparseLongArray(); + doForAllAttributedOpsInUidLocked(uid, (attributedOp) -> { + attributedOp.doForAllInProgressStartOpEvents((event) -> { + if (event == null) { + return; } - } - attributedOp.finished(event.getClientId()); + int chainId = event.getAttributionChainId(); + if (chainId != ATTRIBUTION_CHAIN_ID_NONE) { + long currentEarliestStartTime = + chainsToFinish.get(chainId, Long.MAX_VALUE); + if (event.getStartTime() < currentEarliestStartTime) { + // Store the earliest chain link we're finishing, so that we can go back + // and finish any links in the chain that started after this one + chainsToFinish.put(chainId, event.getStartTime()); + } + } + attributedOp.finished(event.getClientId()); + }); }); - }); - finishChainsLocked(chainsToFinish); + finishChainsLocked(chainsToFinish); + } } @GuardedBy("this") diff --git a/services/core/java/com/android/server/appop/AppOpsUidStateTracker.java b/services/core/java/com/android/server/appop/AppOpsUidStateTracker.java index 268b286d8fe1..9bd72990f7b7 100644 --- a/services/core/java/com/android/server/appop/AppOpsUidStateTracker.java +++ b/services/core/java/com/android/server/appop/AppOpsUidStateTracker.java @@ -19,6 +19,7 @@ package com.android.server.appop; import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE; import static android.app.ActivityManager.PROCESS_STATE_BOUND_TOP; import static android.app.ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE; +import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT; import static android.app.ActivityManager.PROCESS_STATE_PERSISTENT_UI; import static android.app.ActivityManager.PROCESS_STATE_RECEIVER; import static android.app.ActivityManager.PROCESS_STATE_TOP; @@ -27,8 +28,10 @@ import static android.app.AppOpsManager.UID_STATE_BACKGROUND; import static android.app.AppOpsManager.UID_STATE_CACHED; import static android.app.AppOpsManager.UID_STATE_FOREGROUND; import static android.app.AppOpsManager.UID_STATE_FOREGROUND_SERVICE; +import static android.app.AppOpsManager.UID_STATE_NONEXISTENT; import static android.app.AppOpsManager.UID_STATE_PERSISTENT; import static android.app.AppOpsManager.UID_STATE_TOP; +import static android.permission.flags.Flags.finishRunningOpsForKilledPackages; import android.annotation.CallbackExecutor; import android.util.SparseArray; @@ -68,6 +71,14 @@ interface AppOpsUidStateTracker { return UID_STATE_BACKGROUND; } + if (finishRunningOpsForKilledPackages()) { + if (procState < PROCESS_STATE_NONEXISTENT) { + return UID_STATE_CACHED; + } + + return UID_STATE_NONEXISTENT; + } + // UID_STATE_NONEXISTENT is deliberately excluded here return UID_STATE_CACHED; } @@ -119,6 +130,8 @@ interface AppOpsUidStateTracker { * evaluated result may have changed. */ void onUidStateChanged(int uid, int uidState, boolean foregroundModeMayChange); + + void onUidProcessDeath(int uid); } void dumpUidState(PrintWriter pw, int uid, long nowElapsed); diff --git a/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java b/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java index 6f8c241a86ae..1a1077ad0e7b 100644 --- a/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java +++ b/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java @@ -21,7 +21,6 @@ import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_CAMERA; import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_LOCATION; import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_MICROPHONE; import static android.app.ActivityManager.PROCESS_CAPABILITY_NONE; -import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT; import static android.app.ActivityManager.ProcessCapability; import static android.app.AppOpsManager.MIN_PRIORITY_UID_STATE; import static android.app.AppOpsManager.MODE_ALLOWED; @@ -32,6 +31,7 @@ import static android.app.AppOpsManager.OP_CONTROL_AUDIO; import static android.app.AppOpsManager.OP_NONE; import static android.app.AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO; import static android.app.AppOpsManager.OP_RECORD_AUDIO; +import static android.app.AppOpsManager.UID_STATE_CACHED; import static android.app.AppOpsManager.UID_STATE_FOREGROUND_SERVICE; import static android.app.AppOpsManager.UID_STATE_MAX_LAST_NON_RESTRICTED; import static android.app.AppOpsManager.UID_STATE_NONEXISTENT; @@ -75,7 +75,6 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { private SparseBooleanArray mAppWidgetVisible = new SparseBooleanArray(); private SparseBooleanArray mPendingAppWidgetVisible = new SparseBooleanArray(); private SparseLongArray mPendingCommitTime = new SparseLongArray(); - private SparseBooleanArray mPendingGone = new SparseBooleanArray(); private ArrayMap<UidStateChangedCallback, Executor> mUidStateChangedCallbacks = new ArrayMap<>(); @@ -221,11 +220,12 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { public void updateUidProcState(int uid, int procState, int capability) { int uidState = processStateToUidState(procState); - int prevUidState = mUidStates.get(uid, AppOpsManager.MIN_PRIORITY_UID_STATE); + int prevUidState = mUidStates.get(uid, AppOpsManager.UID_STATE_NONEXISTENT); int prevCapability = mCapability.get(uid, PROCESS_CAPABILITY_NONE); - int pendingUidState = mPendingUidStates.get(uid, MIN_PRIORITY_UID_STATE); + int pendingUidState = mPendingUidStates.get(uid, UID_STATE_NONEXISTENT); int pendingCapability = mPendingCapability.get(uid, PROCESS_CAPABILITY_NONE); long pendingStateCommitTime = mPendingCommitTime.get(uid, 0); + if ((pendingStateCommitTime == 0 && (uidState != prevUidState || capability != prevCapability)) || (pendingStateCommitTime != 0 @@ -239,8 +239,7 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { boolean hasLostCapability = (prevCapability & ~capability) != 0; - if (procState == PROCESS_STATE_NONEXISTENT) { - mPendingGone.put(uid, true); + if (uidState == UID_STATE_NONEXISTENT) { commitUidPendingState(uid); } else if (uidState < prevUidState) { // We are moving to a more important state, or the new state may be in the @@ -342,7 +341,7 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { private void commitUidPendingState(int uid) { - int uidState = mUidStates.get(uid, MIN_PRIORITY_UID_STATE); + int uidState = mUidStates.get(uid, UID_STATE_NONEXISTENT); int capability = mCapability.get(uid, PROCESS_CAPABILITY_NONE); boolean appWidgetVisible = mAppWidgetVisible.get(uid, false); @@ -350,18 +349,23 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { int pendingCapability = mPendingCapability.get(uid, capability); boolean pendingAppWidgetVisible = mPendingAppWidgetVisible.get(uid, appWidgetVisible); - boolean foregroundChange = uidState <= UID_STATE_MAX_LAST_NON_RESTRICTED - != pendingUidState <= UID_STATE_MAX_LAST_NON_RESTRICTED + // UID_STATE_NONEXISTENT is a state that isn't used outside of this class, nonexistent + // processes have always been represented as CACHED + int externalUidState = Math.min(uidState, UID_STATE_CACHED); + int externalPendingUidState = Math.min(pendingUidState, UID_STATE_CACHED); + + boolean foregroundChange = externalUidState <= UID_STATE_MAX_LAST_NON_RESTRICTED + != externalPendingUidState <= UID_STATE_MAX_LAST_NON_RESTRICTED || capability != pendingCapability || appWidgetVisible != pendingAppWidgetVisible; - if (uidState != pendingUidState + if (externalUidState != externalPendingUidState || capability != pendingCapability || appWidgetVisible != pendingAppWidgetVisible) { if (foregroundChange) { // To save on memory usage, log only interesting changes. - mEventLog.logCommitUidState(uid, pendingUidState, pendingCapability, + mEventLog.logCommitUidState(uid, externalPendingUidState, pendingCapability, pendingAppWidgetVisible, appWidgetVisible != pendingAppWidgetVisible); } @@ -370,24 +374,23 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { Executor executor = mUidStateChangedCallbacks.valueAt(i); executor.execute(PooledLambda.obtainRunnable( - UidStateChangedCallback::onUidStateChanged, cb, uid, pendingUidState, - foregroundChange)); + UidStateChangedCallback::onUidStateChanged, cb, uid, + externalPendingUidState, foregroundChange)); } } - if (mPendingGone.get(uid, false)) { + if (pendingUidState == UID_STATE_NONEXISTENT && uidState != pendingUidState) { mUidStates.delete(uid); mCapability.delete(uid); mAppWidgetVisible.delete(uid); - mPendingGone.delete(uid); if (finishRunningOpsForKilledPackages()) { for (int i = 0; i < mUidStateChangedCallbacks.size(); i++) { UidStateChangedCallback cb = mUidStateChangedCallbacks.keyAt(i); Executor executor = mUidStateChangedCallbacks.valueAt(i); + // If foregroundness changed it should be handled in earlier callback invocation executor.execute(PooledLambda.obtainRunnable( - UidStateChangedCallback::onUidStateChanged, cb, uid, - UID_STATE_NONEXISTENT, foregroundChange)); + UidStateChangedCallback::onUidProcessDeath, cb, uid)); } } } else { diff --git a/services/core/java/com/android/server/audio/HardeningEnforcer.java b/services/core/java/com/android/server/audio/HardeningEnforcer.java index 9bb5160f108a..46693614e137 100644 --- a/services/core/java/com/android/server/audio/HardeningEnforcer.java +++ b/services/core/java/com/android/server/audio/HardeningEnforcer.java @@ -199,7 +199,9 @@ public class HardeningEnforcer { if (packageName.isEmpty()) { packageName = getPackNameForUid(callingUid); } - + // indicates would be blocked if audio capabilities were required + boolean blockedIfFull = !noteOp(AppOpsManager.OP_CONTROL_AUDIO, + callingUid, packageName, attributionTag); boolean blocked = true; // indicates the focus request was not blocked because of the SDK version boolean unblockedBySdk = false; @@ -213,22 +215,35 @@ public class HardeningEnforcer { Slog.i(TAG, "blockFocusMethod pack:" + packageName + " NOT blocking due to sdk=" + targetSdk); } - blocked = false; unblockedBySdk = true; } - metricsLogFocusReq(blocked, focusReqType, callingUid, unblockedBySdk); + boolean enforced = mShouldEnableAllHardening.get() || !unblockedBySdk; + boolean enforcedFull = mShouldEnableAllHardening.get(); - if (!blocked) { - return false; - } + metricsLogFocusReq(blocked && enforced, focusReqType, callingUid, unblockedBySdk); - String errorMssg = "Focus request DENIED for uid:" + callingUid - + " clientId:" + clientId + " req:" + focusReqType - + " procState:" + mActivityManager.getUidProcessState(callingUid); - mEventLogger.enqueueAndSlog(errorMssg, EventLogger.Event.ALOGI, TAG); + if (blocked) { + String msg = "AudioHardening focus request for req " + + focusReqType + + (!enforced ? " would be " : " ") + + "ignored for " + + packageName + " (" + callingUid + "), " + + clientId + + ", level: partial"; + mEventLogger.enqueueAndSlog(msg, EventLogger.Event.ALOGW, TAG); + } else if (blockedIfFull) { + String msg = "AudioHardening focus request for req " + + focusReqType + + (!enforcedFull ? " would be " : " ") + + "ignored for " + + packageName + " (" + callingUid + "), " + + clientId + + ", level: full"; + mEventLogger.enqueueAndSlog(msg, EventLogger.Event.ALOGW, TAG); + } - return true; + return blocked && enforced || (blockedIfFull && enforcedFull); } /** diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStats.java b/services/core/java/com/android/server/biometrics/AuthenticationStats.java index 3c1cc006ffda..461ddf86ff71 100644 --- a/services/core/java/com/android/server/biometrics/AuthenticationStats.java +++ b/services/core/java/com/android/server/biometrics/AuthenticationStats.java @@ -32,14 +32,20 @@ public class AuthenticationStats { private int mTotalAttempts; private int mRejectedAttempts; private int mEnrollmentNotifications; + + private long mLastEnrollmentTime; + private long mLastFrrNotificationTime; private final int mModality; public AuthenticationStats(final int userId, int totalAttempts, int rejectedAttempts, - int enrollmentNotifications, final int modality) { + int enrollmentNotifications, long lastEnrollmentTime, long lastFrrNotificationTime, + final int modality) { mUserId = userId; mTotalAttempts = totalAttempts; mRejectedAttempts = rejectedAttempts; mEnrollmentNotifications = enrollmentNotifications; + mLastEnrollmentTime = lastEnrollmentTime; + mLastFrrNotificationTime = lastFrrNotificationTime; mModality = modality; } @@ -48,6 +54,8 @@ public class AuthenticationStats { mTotalAttempts = 0; mRejectedAttempts = 0; mEnrollmentNotifications = 0; + mLastEnrollmentTime = 0; + mLastFrrNotificationTime = 0; mModality = modality; } @@ -71,6 +79,14 @@ public class AuthenticationStats { return mModality; } + public long getLastEnrollmentTime() { + return mLastEnrollmentTime; + } + + public long getLastFrrNotificationTime() { + return mLastFrrNotificationTime; + } + /** Calculate FRR. */ public float getFrr() { if (mTotalAttempts > 0) { @@ -100,6 +116,16 @@ public class AuthenticationStats { mEnrollmentNotifications++; } + /** Updates last enrollment time */ + public void updateLastEnrollmentTime(long lastEnrollmentTime) { + mLastEnrollmentTime = lastEnrollmentTime; + } + + /** Updates frr notification time */ + public void updateLastFrrNotificationTime(long lastFrrNotificationTime) { + mLastFrrNotificationTime = lastFrrNotificationTime; + } + @Override public boolean equals(Object obj) { if (this == obj) { @@ -118,6 +144,10 @@ public class AuthenticationStats { == target.getRejectedAttempts() && this.getEnrollmentNotifications() == target.getEnrollmentNotifications() + && this.getLastEnrollmentTime() + == target.getLastEnrollmentTime() + && this.getLastFrrNotificationTime() + == target.getLastFrrNotificationTime() && this.getModality() == target.getModality(); } diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStatsBroadcastReceiver.java b/services/core/java/com/android/server/biometrics/AuthenticationStatsBroadcastReceiver.java index 832d73fd5d2d..54348403914a 100644 --- a/services/core/java/com/android/server/biometrics/AuthenticationStatsBroadcastReceiver.java +++ b/services/core/java/com/android/server/biometrics/AuthenticationStatsBroadcastReceiver.java @@ -27,6 +27,7 @@ import android.util.Slog; import com.android.server.biometrics.sensors.BiometricNotificationImpl; +import java.time.Clock; import java.util.function.Consumer; /** @@ -62,7 +63,7 @@ public class AuthenticationStatsBroadcastReceiver extends BroadcastReceiver { mCollectorConsumer.accept( new AuthenticationStatsCollector(context, mModality, - new BiometricNotificationImpl())); + new BiometricNotificationImpl(), Clock.systemUTC())); context.unregisterReceiver(this); } diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java b/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java index 526264d67318..b79bab99f681 100644 --- a/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java +++ b/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java @@ -23,6 +23,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; +import android.hardware.biometrics.BiometricsProtoEnums; import android.hardware.face.FaceManager; import android.hardware.fingerprint.FingerprintManager; import android.os.UserHandle; @@ -32,6 +33,9 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.server.biometrics.sensors.BiometricNotification; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; import java.util.HashMap; import java.util.Map; @@ -50,7 +54,12 @@ public class AuthenticationStatsCollector { private static final int AUTHENTICATION_UPLOAD_INTERVAL = 50; // The maximum number of eligible biometric enrollment notification can be sent. @VisibleForTesting - static final int MAXIMUM_ENROLLMENT_NOTIFICATIONS = 1; + static final int MAXIMUM_ENROLLMENT_NOTIFICATIONS = Flags.frrDialogImprovement() ? 3 : 1; + @VisibleForTesting + static final Duration FRR_MINIMAL_DURATION = Duration.ofDays(7); + + public static final String ACTION_LAST_ENROLL_TIME_CHANGED = "last_enroll_time_changed"; + public static final String EXTRA_MODALITY = "modality"; @NonNull private final Context mContext; @NonNull private final PackageManager mPackageManager; @@ -64,6 +73,7 @@ public class AuthenticationStatsCollector { @NonNull private final Map<Integer, AuthenticationStats> mUserAuthenticationStatsMap; @NonNull private AuthenticationStatsPersister mAuthenticationStatsPersister; @NonNull private BiometricNotification mBiometricNotification; + @NonNull private final Clock mClock; private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override @@ -78,8 +88,24 @@ public class AuthenticationStatsCollector { } }; + private final BroadcastReceiver mEnrollTimeUpdatedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(@NonNull Context context, @NonNull Intent intent) { + int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL); + int modality = intent.getIntExtra(EXTRA_MODALITY, + BiometricsProtoEnums.MODALITY_UNKNOWN); + if (modality == mModality) { + updateAuthenticationStatsMapIfNeeded(userId); + AuthenticationStats authenticationStats = + mUserAuthenticationStatsMap.get(userId); + Slog.d(TAG, "Update enroll time for user: " + userId); + authenticationStats.updateLastEnrollmentTime(mClock.millis()); + } + } + }; + public AuthenticationStatsCollector(@NonNull Context context, int modality, - @NonNull BiometricNotification biometricNotification) { + @NonNull BiometricNotification biometricNotification, @NonNull Clock clock) { mContext = context; mEnabled = context.getResources().getBoolean(R.bool.config_biometricFrrNotificationEnabled); mThreshold = context.getResources() @@ -87,6 +113,7 @@ public class AuthenticationStatsCollector { mUserAuthenticationStatsMap = new HashMap<>(); mModality = modality; mBiometricNotification = biometricNotification; + mClock = clock; mPackageManager = context.getPackageManager(); mFaceManager = mContext.getSystemService(FaceManager.class); @@ -100,6 +127,11 @@ public class AuthenticationStatsCollector { IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_USER_REMOVED); context.registerReceiver(mBroadcastReceiver, intentFilter); + + IntentFilter enrollTimeChangedFilter = new IntentFilter(); + enrollTimeChangedFilter.addAction(ACTION_LAST_ENROLL_TIME_CHANGED); + context.registerReceiver(mEnrollTimeUpdatedReceiver, enrollTimeChangedFilter, + Context.RECEIVER_NOT_EXPORTED); } private void initializeUserAuthenticationStatsMap() { @@ -123,19 +155,9 @@ public class AuthenticationStatsCollector { return; } - // SharedPreference is not ready when starting system server, initialize - // mUserAuthenticationStatsMap in authentication to ensure SharedPreference - // is ready for application use. - if (mUserAuthenticationStatsMap.isEmpty()) { - initializeUserAuthenticationStatsMap(); - } - // Check if this is a new user. - if (!mUserAuthenticationStatsMap.containsKey(userId)) { - mUserAuthenticationStatsMap.put(userId, new AuthenticationStats(userId, mModality)); - } + updateAuthenticationStatsMapIfNeeded(userId); AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId); - if (authenticationStats.getEnrollmentNotifications() >= MAXIMUM_ENROLLMENT_NOTIFICATIONS) { return; } @@ -147,34 +169,91 @@ public class AuthenticationStatsCollector { persistDataIfNeeded(userId); } + private void updateAuthenticationStatsMapIfNeeded(int userId) { + // SharedPreference is not ready when starting system server, initialize + // mUserAuthenticationStatsMap in authentication to ensure SharedPreference + // is ready for application use. + if (mUserAuthenticationStatsMap.isEmpty()) { + initializeUserAuthenticationStatsMap(); + } + // Check if this is a new user. + if (!mUserAuthenticationStatsMap.containsKey(userId)) { + mUserAuthenticationStatsMap.put(userId, new AuthenticationStats(userId, mModality)); + } + } + /** Check if a notification should be sent after a calculation cycle. */ private void sendNotificationIfNeeded(int userId) { AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId); if (authenticationStats.getTotalAttempts() < MINIMUM_ATTEMPTS) { return; } + + boolean showFrr; + if (Flags.frrDialogImprovement()) { + long lastFrrOrEnrollTime = Math.max(authenticationStats.getLastEnrollmentTime(), + authenticationStats.getLastFrrNotificationTime()); + showFrr = authenticationStats.getEnrollmentNotifications() + < MAXIMUM_ENROLLMENT_NOTIFICATIONS + && authenticationStats.getFrr() >= mThreshold + && isFrrMinimalDurationPassed(lastFrrOrEnrollTime); + } else { + showFrr = authenticationStats.getEnrollmentNotifications() + < MAXIMUM_ENROLLMENT_NOTIFICATIONS + && authenticationStats.getFrr() >= mThreshold; + } + // Don't send notification if FRR below the threshold. - if (authenticationStats.getEnrollmentNotifications() >= MAXIMUM_ENROLLMENT_NOTIFICATIONS - || authenticationStats.getFrr() < mThreshold) { + if (!showFrr) { authenticationStats.resetData(); return; } - authenticationStats.resetData(); + if (Flags.frrDialogImprovement() + && mModality == BiometricsProtoEnums.MODALITY_FINGERPRINT) { + boolean sent = mBiometricNotification.sendCustomizeFpFrrNotification(mContext); + if (sent) { + authenticationStats.updateLastFrrNotificationTime(mClock.millis()); + authenticationStats.updateNotificationCounter(); + return; + } + } + final boolean hasEnrolledFace = hasEnrolledFace(userId); final boolean hasEnrolledFingerprint = hasEnrolledFingerprint(userId); if (hasEnrolledFace && !hasEnrolledFingerprint) { mBiometricNotification.sendFpEnrollNotification(mContext); + authenticationStats.updateLastFrrNotificationTime(mClock.millis()); authenticationStats.updateNotificationCounter(); } else if (!hasEnrolledFace && hasEnrolledFingerprint) { mBiometricNotification.sendFaceEnrollNotification(mContext); + authenticationStats.updateLastFrrNotificationTime(mClock.millis()); authenticationStats.updateNotificationCounter(); } } + private boolean isFrrMinimalDurationPassed(long previousMillis) { + Instant previous = Instant.ofEpochMilli(previousMillis); + long nowMillis = mClock.millis(); + Instant now = Instant.ofEpochMilli(nowMillis); + + if (now.isAfter(previous)) { + Duration between = Duration.between(/* startInclusive= */ previous, + /* endExclusive= */ now); + if (between.compareTo(FRR_MINIMAL_DURATION) > 0) { + return true; + } else { + Slog.d(TAG, "isFrrMinimalDurationPassed, duration too short"); + } + } else { + Slog.d(TAG, "isFrrMinimalDurationPassed, date not match"); + } + return false; + } + private void persistDataIfNeeded(int userId) { AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId); if (authenticationStats.getTotalAttempts() % AUTHENTICATION_UPLOAD_INTERVAL == 0) { @@ -182,6 +261,8 @@ public class AuthenticationStatsCollector { authenticationStats.getTotalAttempts(), authenticationStats.getRejectedAttempts(), authenticationStats.getEnrollmentNotifications(), + authenticationStats.getLastEnrollmentTime(), + authenticationStats.getLastFrrNotificationTime(), authenticationStats.getModality()); } } diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStatsPersister.java b/services/core/java/com/android/server/biometrics/AuthenticationStatsPersister.java index 5625bfd21e76..746d00909900 100644 --- a/services/core/java/com/android/server/biometrics/AuthenticationStatsPersister.java +++ b/services/core/java/com/android/server/biometrics/AuthenticationStatsPersister.java @@ -48,8 +48,13 @@ public class AuthenticationStatsPersister { private static final String USER_ID = "user_id"; private static final String FACE_ATTEMPTS = "face_attempts"; private static final String FACE_REJECTIONS = "face_rejections"; + private static final String FACE_LAST_ENROLL_TIME = "face_last_enroll_time"; + private static final String FACE_LAST_FRR_NOTIFICATION_TIME = "face_last_notification_time"; private static final String FINGERPRINT_ATTEMPTS = "fingerprint_attempts"; private static final String FINGERPRINT_REJECTIONS = "fingerprint_rejections"; + private static final String FINGERPRINT_LAST_ENROLL_TIME = "fingerprint_last_enroll_time"; + private static final String FINGERPRINT_LAST_FRR_NOTIFICATION_TIME = + "fingerprint_last_notification_time"; private static final String ENROLLMENT_NOTIFICATIONS = "enrollment_notifications"; private static final String KEY = "frr_stats"; private static final String THRESHOLD_KEY = "frr_threshold"; @@ -73,21 +78,10 @@ public class AuthenticationStatsPersister { try { JSONObject frrStatsJson = new JSONObject(frrStats); if (modality == BiometricsProtoEnums.MODALITY_FACE) { - authenticationStatsList.add(new AuthenticationStats( - getIntValue(frrStatsJson, USER_ID, - UserHandle.USER_NULL /* defaultValue */), - getIntValue(frrStatsJson, FACE_ATTEMPTS), - getIntValue(frrStatsJson, FACE_REJECTIONS), - getIntValue(frrStatsJson, ENROLLMENT_NOTIFICATIONS), - modality)); + authenticationStatsList.add(getFaceAuthenticationStatsFromJson(frrStatsJson)); } else if (modality == BiometricsProtoEnums.MODALITY_FINGERPRINT) { - authenticationStatsList.add(new AuthenticationStats( - getIntValue(frrStatsJson, USER_ID, - UserHandle.USER_NULL /* defaultValue */), - getIntValue(frrStatsJson, FINGERPRINT_ATTEMPTS), - getIntValue(frrStatsJson, FINGERPRINT_REJECTIONS), - getIntValue(frrStatsJson, ENROLLMENT_NOTIFICATIONS), - modality)); + authenticationStatsList.add( + getFingerprintAuthenticationStatsFromJson(frrStatsJson)); } } catch (JSONException e) { Slog.w(TAG, String.format("Unable to resolve authentication stats JSON: %s", @@ -97,6 +91,33 @@ public class AuthenticationStatsPersister { return authenticationStatsList; } + @NonNull + AuthenticationStats getFaceAuthenticationStatsFromJson(JSONObject json) throws JSONException { + return new AuthenticationStats( + /* userId */ getIntValue(json, USER_ID, UserHandle.USER_NULL), + /* totalAttempts */ getIntValue(json, FACE_ATTEMPTS), + /* rejectedAttempts */ getIntValue(json, FACE_REJECTIONS), + /* enrollmentNotifications */ getIntValue(json, ENROLLMENT_NOTIFICATIONS), + /* lastEnrollmentTime */ getLongValue(json, FACE_LAST_ENROLL_TIME), + /* lastFrrNotificationTime */getLongValue(json, FACE_LAST_FRR_NOTIFICATION_TIME), + /* modality */ BiometricsProtoEnums.MODALITY_FACE); + } + + @NonNull + AuthenticationStats getFingerprintAuthenticationStatsFromJson(JSONObject json) + throws JSONException { + return new AuthenticationStats( + /* userId */ getIntValue(json, USER_ID, UserHandle.USER_NULL), + /* totalAttempts */ getIntValue(json, FINGERPRINT_ATTEMPTS), + /* rejectedAttempts */ getIntValue(json, FINGERPRINT_REJECTIONS), + /* enrollmentNotifications */ getIntValue(json, ENROLLMENT_NOTIFICATIONS), + /* lastEnrollmentTime */ getLongValue(json, + FINGERPRINT_LAST_ENROLL_TIME), + /* lastFrrNotificationTime */ getLongValue(json, + FINGERPRINT_LAST_FRR_NOTIFICATION_TIME), + /* modality */ BiometricsProtoEnums.MODALITY_FINGERPRINT); + } + /** * Remove frr data for a specific user. */ @@ -124,7 +145,8 @@ public class AuthenticationStatsPersister { * Persist frr data for a specific user. */ public void persistFrrStats(int userId, int totalAttempts, int rejectedAttempts, - int enrollmentNotifications, int modality) { + int enrollmentNotifications, long lastEnrollmentTime, long lastFrrNotificationTime, + int modality) { try { // Copy into a new HashSet to allow modification. Set<String> frrStatsSet = new HashSet<>(readFrrStats()); @@ -147,7 +169,8 @@ public class AuthenticationStatsPersister { frrStatJson = new JSONObject().put(USER_ID, userId); } frrStatsSet.add(buildFrrStats(frrStatJson, totalAttempts, rejectedAttempts, - enrollmentNotifications, modality)); + enrollmentNotifications, lastEnrollmentTime, lastFrrNotificationTime, + modality)); Slog.d(TAG, "frrStatsSet to persist: " + frrStatsSet); @@ -171,18 +194,24 @@ public class AuthenticationStatsPersister { // Update frr stats for existing frrStats JSONObject and build the new string. private String buildFrrStats(JSONObject frrStats, int totalAttempts, int rejectedAttempts, - int enrollmentNotifications, int modality) throws JSONException { + int enrollmentNotifications, long lastEnrollmentTime, long lastFrrNotificationTime, + int modality) + throws JSONException { if (modality == BiometricsProtoEnums.MODALITY_FACE) { return frrStats .put(FACE_ATTEMPTS, totalAttempts) .put(FACE_REJECTIONS, rejectedAttempts) .put(ENROLLMENT_NOTIFICATIONS, enrollmentNotifications) + .put(FACE_LAST_ENROLL_TIME, lastEnrollmentTime) + .put(FACE_LAST_FRR_NOTIFICATION_TIME, lastFrrNotificationTime) .toString(); } else if (modality == BiometricsProtoEnums.MODALITY_FINGERPRINT) { return frrStats .put(FINGERPRINT_ATTEMPTS, totalAttempts) .put(FINGERPRINT_REJECTIONS, rejectedAttempts) .put(ENROLLMENT_NOTIFICATIONS, enrollmentNotifications) + .put(FINGERPRINT_LAST_ENROLL_TIME, lastEnrollmentTime) + .put(FINGERPRINT_LAST_FRR_NOTIFICATION_TIME, lastFrrNotificationTime) .toString(); } else { return frrStats.toString(); @@ -201,4 +230,13 @@ public class AuthenticationStatsPersister { throws JSONException { return jsonObject.has(key) ? jsonObject.getInt(key) : defaultValue; } + + private long getLongValue(JSONObject jsonObject, String key) throws JSONException { + return getLongValue(jsonObject, key, 0 /* defaultValue */); + } + + private long getLongValue(JSONObject jsonObject, String key, long defaultValue) + throws JSONException { + return jsonObject.has(key) ? jsonObject.getLong(key) : defaultValue; + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricNotification.java b/services/core/java/com/android/server/biometrics/sensors/BiometricNotification.java index 90e18604d945..9fb25f429020 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricNotification.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricNotification.java @@ -33,4 +33,9 @@ public interface BiometricNotification { * Sends a fingerprint enrollment notification. */ void sendFpEnrollNotification(@NonNull Context context); + + /** + * Sends a customized fingerprint frr notification. + */ + boolean sendCustomizeFpFrrNotification(@NonNull Context context); } diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationImpl.java b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationImpl.java index 7b420468f628..3ab157082c0b 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationImpl.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationImpl.java @@ -35,4 +35,9 @@ public class BiometricNotificationImpl implements BiometricNotification { public void sendFpEnrollNotification(@NonNull Context context) { BiometricNotificationUtils.showFingerprintEnrollNotification(context); } + + @Override + public boolean sendCustomizeFpFrrNotification(@NonNull Context context) { + return BiometricNotificationUtils.showCustomizeFpFrrNotification(context); + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java index 27f9cc88e28f..3bad3c2a3f8f 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java @@ -17,12 +17,16 @@ package com.android.server.biometrics.sensors; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricsProtoEnums; import android.hardware.face.FaceEnrollOptions; @@ -30,6 +34,7 @@ import android.hardware.fingerprint.FingerprintEnrollOptions; import android.os.SystemClock; import android.os.UserHandle; import android.text.BidiFormatter; +import android.text.TextUtils; import android.util.Slog; import com.android.internal.R; @@ -56,6 +61,7 @@ public class BiometricNotificationUtils { private static final String FACE_ENROLL_CHANNEL = "FaceEnrollNotificationChannel"; private static final String FACE_RE_ENROLL_CHANNEL = "FaceReEnrollNotificationChannel"; private static final String FINGERPRINT_ENROLL_CHANNEL = "FingerprintEnrollNotificationChannel"; + private static final String FINGERPRINT_FRR_CHANNEL = "FingerprintFrrNotificationChannel"; private static final String FINGERPRINT_RE_ENROLL_CHANNEL = "FingerprintReEnrollNotificationChannel"; private static final String FINGERPRINT_BAD_CALIBRATION_CHANNEL = @@ -69,6 +75,7 @@ public class BiometricNotificationUtils { public static final int NOTIFICATION_ID = 1; public static final String FACE_ENROLL_NOTIFICATION_TAG = "FaceEnroll"; public static final String FINGERPRINT_ENROLL_NOTIFICATION_TAG = "FingerprintEnroll"; + public static final String FINGERPRINT_FRR_NOTIFICATION_TAG = "FingerprintFrr"; /** * Shows a face re-enrollment notification. */ @@ -151,6 +158,65 @@ public class BiometricNotificationUtils { } /** + * Shows a customized fingerprint frr notification. + * + * @return true if notification shows + */ + public static boolean showCustomizeFpFrrNotification(@NonNull Context context) { + final String name = + context.getString(R.string.device_unlock_notification_name); + final String title = + context.getString(R.string.fingerprint_frr_notification_title); + final String content = + context.getString(R.string.fingerprint_frr_notification_msg); + + Intent intent = getIntentFromFpFrrComponentNameStringRes(context); + Slog.d(TAG, "Showing Customize Fingerprint Frr Notification result:" + (intent != null)); + if (intent == null) { + return false; + } + + final PendingIntent pendingIntent = PendingIntent.getActivityAsUser(context, + 0 /* requestCode */, intent, PendingIntent.FLAG_IMMUTABLE /* flags */, + null /* options */, UserHandle.CURRENT); + + showNotificationHelper(context, name, title, content, pendingIntent, + Notification.CATEGORY_RECOMMENDATION, FINGERPRINT_FRR_CHANNEL, + FINGERPRINT_FRR_NOTIFICATION_TAG, Notification.VISIBILITY_PUBLIC, true); + + return true; + } + + @Nullable + private static Intent getIntentFromFpFrrComponentNameStringRes(@NonNull Context context) { + String componentNameString = context.getResources().getString( + R.string.config_fingerprintFrrTargetComponent); + if (TextUtils.isEmpty(componentNameString)) { + return null; + } + + ComponentName componentName = ComponentName.unflattenFromString(componentNameString); + if (componentName == null) { + return null; + } + + PackageManager packageManager = context.getPackageManager(); + Intent intent = new Intent(); + intent.setComponent(componentName); + intent.setAction(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + + ResolveInfo resolveInfo = packageManager.resolveActivity(intent, + PackageManager.MATCH_DEFAULT_ONLY); + if (resolveInfo != null) { + return intent; + } else { + Slog.d(TAG, "Component for " + componentNameString + " not found"); + return null; + } + } + + /** * Shows a fingerprint notification for loss of enrollment */ public static void showFingerprintLoeNotification(@NonNull Context context) { diff --git a/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java index 38bf99932838..1632e0d7ca6f 100644 --- a/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java @@ -18,6 +18,7 @@ package com.android.server.biometrics.sensors; import android.annotation.NonNull; import android.content.Context; +import android.content.Intent; import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricRequestConstants; import android.hardware.face.FaceEnrollOptions; @@ -26,6 +27,7 @@ import android.os.IBinder; import android.os.RemoteException; import android.util.Slog; +import com.android.server.biometrics.AuthenticationStatsCollector; import com.android.server.biometrics.BiometricsProto; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; @@ -159,4 +161,13 @@ public abstract class EnrollClient<T> extends AcquisitionClient<T> implements En default -> BiometricRequestConstants.REASON_UNKNOWN; }; } + + protected void notifyLastEnrollmentTime(int modality) { + // Notify the last enrollment time to re-count authentication stats for frr. + final Intent intent = new Intent( + AuthenticationStatsCollector.ACTION_LAST_ENROLL_TIME_CHANGED); + intent.putExtra(Intent.EXTRA_USER_HANDLE, getTargetUserId()); + intent.putExtra(AuthenticationStatsCollector.EXTRA_MODALITY, modality); + getContext().sendBroadcast(intent); + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java index d4ec573e1667..e7b2d41024a4 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java @@ -24,9 +24,11 @@ import static android.hardware.face.FaceManager.getErrorString; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricFaceConstants; import android.hardware.biometrics.BiometricSourceType; +import android.hardware.biometrics.BiometricsProtoEnums; import android.hardware.biometrics.common.ICancellationSignal; import android.hardware.biometrics.events.AuthenticationErrorInfo; import android.hardware.biometrics.events.AuthenticationHelpInfo; @@ -171,6 +173,14 @@ public class FaceEnrollClient extends EnrollClient<AidlSession> { onAcquiredInternal(acquireInfo, vendorCode, shouldSend); } + @Override + public void onEnrollResult(BiometricAuthenticator.Identifier identifier, int remaining) { + super.onEnrollResult(identifier, remaining); + if (remaining == 0) { + notifyLastEnrollmentTime(BiometricsProtoEnums.MODALITY_FACE); + } + } + /** * Called each time a new frame is received during face enrollment. * diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java index 993a68fd6ff8..776435d5abc8 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java @@ -30,6 +30,7 @@ import android.hardware.biometrics.BiometricFingerprintConstants; import android.hardware.biometrics.BiometricFingerprintConstants.FingerprintAcquired; import android.hardware.biometrics.BiometricSourceType; import android.hardware.biometrics.BiometricStateListener; +import android.hardware.biometrics.BiometricsProtoEnums; import android.hardware.biometrics.common.ICancellationSignal; import android.hardware.biometrics.events.AuthenticationAcquiredInfo; import android.hardware.biometrics.events.AuthenticationErrorInfo; @@ -156,8 +157,8 @@ public class FingerprintEnrollClient extends EnrollClient<AidlSession> implement BiometricSourceType.FINGERPRINT, getRequestReasonFromFingerprintEnrollReason(mEnrollReason)).build() ); + notifyLastEnrollmentTime(BiometricsProtoEnums.MODALITY_FINGERPRINT); } - } @Override diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java index 05fc6bc869ca..c2500c8ae7fa 100644 --- a/services/core/java/com/android/server/camera/CameraServiceProxy.java +++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java @@ -559,6 +559,9 @@ public class CameraServiceProxy extends SystemService @Override public void onKeepClearAreasChanged(int displayId, List<Rect> restricted, List<Rect> unrestricted) { } + + @Override + public void onDesktopModeEligibleChanged(int displayId) { } } diff --git a/services/core/java/com/android/server/clipboard/Android.bp b/services/core/java/com/android/server/clipboard/Android.bp new file mode 100644 index 000000000000..6905fc157a9a --- /dev/null +++ b/services/core/java/com/android/server/clipboard/Android.bp @@ -0,0 +1,18 @@ +aconfig_declarations { + name: "clipboard_flags", + package: "com.android.server.clipboard", + container: "system", + srcs: ["*.aconfig"], +} + +java_aconfig_library { + name: "clipboard_flags_lib", + aconfig_declarations: "clipboard_flags", +} + +java_aconfig_library { + name: "clipboard_flags_host_lib", + host_supported: true, + libs: ["fake_device_config"], + aconfig_declarations: "clipboard_flags", +} diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java index 6122fdaafe77..40136c3e03ec 100644 --- a/services/core/java/com/android/server/clipboard/ClipboardService.java +++ b/services/core/java/com/android/server/clipboard/ClipboardService.java @@ -19,9 +19,27 @@ package com.android.server.clipboard; import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY; import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD; +import static android.content.ClipDescription.MIMETYPE_UNKNOWN; +import static android.content.ClipDescription.MIMETYPE_TEXT_PLAIN; +import static android.content.ClipDescription.MIMETYPE_TEXT_HTML; +import static android.content.ClipDescription.MIMETYPE_TEXT_URILIST; +import static android.content.ClipDescription.MIMETYPE_TEXT_INTENT; +import static android.content.ClipDescription.MIMETYPE_APPLICATION_ACTIVITY; +import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT; +import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK; import static android.content.Context.DEVICE_ID_DEFAULT; import static android.content.Context.DEVICE_ID_INVALID; +import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_UNKNOWN; +import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_PLAIN; +import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_HTML; +import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_URILIST; +import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_INTENT; +import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_APPLICATION_ACTIVITY; +import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_APPLICATION_SHORTCUT; +import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_APPLICATION_TASK; +import static com.android.server.clipboard.Flags.clipboardGetEventLogging; + import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; @@ -70,6 +88,7 @@ import android.provider.Settings; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.IntArray; import android.util.Pair; import android.util.SafetyProtectionUtils; import android.util.Slog; @@ -98,6 +117,7 @@ import com.android.server.wm.WindowManagerInternal; import java.util.HashSet; import java.util.List; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; /** @@ -132,6 +152,10 @@ public class ClipboardService extends SystemService { private static final String PROPERTY_MAX_CLASSIFICATION_LENGTH = "max_classification_length"; private static final int DEFAULT_MAX_CLASSIFICATION_LENGTH = 400; + private static final int[] CLIP_DATA_TYPES_UNKNOWN = { + CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_UNKNOWN + }; + private final ActivityManagerInternal mAmInternal; private final IUriGrantsManager mUgm; private final UriGrantsManagerInternal mUgmInternal; @@ -657,6 +681,8 @@ public class ClipboardService extends SystemService { pkg, intendingUid, intendingUserId, clipboard, deviceId); notifyTextClassifierLocked(clipboard, pkg, intendingUid); if (clipboard.primaryClip != null) { + scheduleWriteClipDataStats(clipboard.primaryClip, + clipboard.primaryClipUid, intendingUid); scheduleAutoClear(userId, intendingUid, intendingDeviceId); } return clipboard.primaryClip; @@ -1600,4 +1626,65 @@ public class ClipboardService extends SystemService { Context context = getContext().createContextAsUser(UserHandle.of(userId), /* flags= */ 0); return context.getSystemService(TextClassificationManager.class); } + + private static int mimeTypeToClipDataType(@NonNull String mimeType) { + switch (mimeType) { + case MIMETYPE_TEXT_PLAIN: + return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_PLAIN; + case MIMETYPE_TEXT_HTML: + return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_HTML; + case MIMETYPE_TEXT_URILIST: + return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_URILIST; + case MIMETYPE_TEXT_INTENT: + return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_INTENT; + case MIMETYPE_APPLICATION_ACTIVITY: + return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_APPLICATION_ACTIVITY; + case MIMETYPE_APPLICATION_SHORTCUT: + return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_APPLICATION_SHORTCUT; + case MIMETYPE_APPLICATION_TASK: + return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_APPLICATION_TASK; + case MIMETYPE_UNKNOWN: + // fallthrough + default: + return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_UNKNOWN; + } + } + + private void scheduleWriteClipDataStats(@NonNull ClipData clipData, + int sourceUid, int intendingUid) { + if (!clipboardGetEventLogging()) { + return; + } + final ClipDescription description = clipData.getDescription(); + if (description != null) { + final IntArray mimeTypes = new IntArray(); + final int secondsSinceSet = (int) TimeUnit.MILLISECONDS.toSeconds( + System.currentTimeMillis() - description.getTimestamp()); + for (int i = description.getMimeTypeCount() - 1; i >= 0; i--) { + final String mimeType = description.getMimeType(i); + if (mimeType != null) { + final int clipDataType = mimeTypeToClipDataType(mimeType); + if (!mimeTypes.contains(clipDataType)) { + mimeTypes.add(clipDataType); + } + } + } + // The getUidProcessState() will hit AMS lock which might be slow, while getting the + // clip data might be on the critical UI path. So post to the work thread. + // There could be race conditions where the UID state might have been changed + // between now and the work thread execution time, but this should be acceptable. + mWorkerHandler.post(() -> FrameworkStatsLog.write( + FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED, + sourceUid, intendingUid, + mAmInternal.getUidProcessState(intendingUid), + mimeTypes.toArray(), + secondsSinceSet)); + } else { + mWorkerHandler.post(() -> FrameworkStatsLog.write( + FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED, + sourceUid, intendingUid, + mAmInternal.getUidProcessState(intendingUid), + CLIP_DATA_TYPES_UNKNOWN, 0)); + } + } } diff --git a/services/core/java/com/android/server/clipboard/flags.aconfig b/services/core/java/com/android/server/clipboard/flags.aconfig new file mode 100644 index 000000000000..964242d794a4 --- /dev/null +++ b/services/core/java/com/android/server/clipboard/flags.aconfig @@ -0,0 +1,9 @@ +package: "com.android.server.clipboard" +container: "system" + +flag { + name: "clipboard_get_event_logging" + namespace: "backstage_power" + description: "Log the clipboard retrieval event in statsd" + bug: "402542624" +} diff --git a/services/core/java/com/android/server/connectivity/PacProxyService.java b/services/core/java/com/android/server/connectivity/PacProxyService.java index 2e90a3d86161..c8c1eddd53e7 100644 --- a/services/core/java/com/android/server/connectivity/PacProxyService.java +++ b/services/core/java/com/android/server/connectivity/PacProxyService.java @@ -36,6 +36,7 @@ import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; +import android.os.IServiceManager; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.ServiceManager; @@ -355,7 +356,9 @@ public class PacProxyService extends IPacProxyManager.Stub { } catch (RemoteException e1) { Log.e(TAG, "Remote Exception", e1); } - ServiceManager.addService(PAC_SERVICE_NAME, binder); + // Do not cache the service, otherwise it will crash com.android.pacprocessor + ServiceManager.addService(PAC_SERVICE_NAME, binder, /* allowIsolated */ false, + IServiceManager.FLAG_IS_LAZY_SERVICE); mProxyService = IProxyService.Stub.asInterface(binder); if (mProxyService == null) { Log.e(TAG, "No proxy service"); diff --git a/services/core/java/com/android/server/health/HealthServiceWrapper.java b/services/core/java/com/android/server/health/HealthServiceWrapper.java index 9c14b5b079b1..1b123e8663e8 100644 --- a/services/core/java/com/android/server/health/HealthServiceWrapper.java +++ b/services/core/java/com/android/server/health/HealthServiceWrapper.java @@ -28,15 +28,21 @@ import com.android.internal.annotations.VisibleForTesting; import java.util.NoSuchElementException; /** - * HealthServiceWrapper wraps the internal IHealth service and refreshes the service when necessary. - * This is essentially a wrapper over IHealth that is useful for BatteryService. + * HealthServiceWrapper wraps the internal IHealth service and refreshes the + * service when necessary. * - * <p>The implementation may be backed by a HIDL or AIDL HAL. + * This is essentially a wrapper over IHealth that is useful for BatteryService, + * and TradeInModeService. * - * <p>On new registration of IHealth service, the internal service is refreshed. On death of an - * existing IHealth service, the internal service is NOT cleared to avoid race condition between - * death notification and new service notification. Hence, a caller must check for transaction - * errors when calling into the service. + * <p> + * The implementation may be backed by a HIDL or AIDL HAL. + * + * <p> + * On new registration of IHealth service, the internal service is refreshed. On + * death of an existing IHealth service, the internal service is NOT cleared to + * avoid race condition between death notification and new service notification. + * Hence, a caller must check for transaction errors when calling into the + * service. * * @hide Should only be used internally. */ @@ -46,7 +52,8 @@ public abstract class HealthServiceWrapper { abstract HandlerThread getHandlerThread(); /** - * Calls into get*() functions in the health HAL. This reads into the kernel interfaces + * Calls into get*() functions in the health HAL. This reads into the kernel + * interfaces * directly. * * @see IBatteryPropertiesRegistrar#getProperty @@ -61,11 +68,14 @@ public abstract class HealthServiceWrapper { public abstract void scheduleUpdate() throws RemoteException; /** - * Calls into getHealthInfo() in the health HAL. This returns a cached value in the health HAL + * Calls into getHealthInfo() in the health HAL. This returns a cached value in + * the health HAL * implementation. * - * @return health info. {@code null} if no health HAL service. {@code null} if any - * service-specific error when calling {@code getHealthInfo}, e.g. it is unsupported. + * @return health info. {@code null} if no health HAL service. {@code null} if + * any + * service-specific error when calling {@code getHealthInfo}, e.g. it is + * unsupported. * @throws RemoteException for any transaction-level errors */ public abstract android.hardware.health.HealthInfo getHealthInfo() throws RemoteException; @@ -77,7 +87,7 @@ public abstract class HealthServiceWrapper { * this one. * * @return battery health data. {@code null} if no health HAL service. - * {@code null} if any service-specific error when calling {@code + * {@code null} if any service-specific error when calling {@code * getBatteryHealthData}, e.g. it is unsupported. * @throws RemoteException for any transaction-level errors */ @@ -89,31 +99,40 @@ public abstract class HealthServiceWrapper { * Create a new HealthServiceWrapper instance. * * @param healthInfoCallback the callback to call when health info changes - * @return the new HealthServiceWrapper instance, which may be backed by HIDL or AIDL service. - * @throws RemoteException transaction errors + * @return the new HealthServiceWrapper instance, which may be backed by HIDL or + * AIDL service. + * @throws RemoteException transaction errors * @throws NoSuchElementException no HIDL or AIDL service is available */ public static HealthServiceWrapper create(@Nullable HealthInfoCallback healthInfoCallback) throws RemoteException, NoSuchElementException { return create( healthInfoCallback == null ? null : new HealthRegCallbackAidl(healthInfoCallback), - new HealthServiceWrapperAidl.ServiceManagerStub() {}, + new HealthServiceWrapperAidl.ServiceManagerStub() { + }, healthInfoCallback == null ? null : new HealthHalCallbackHidl(healthInfoCallback), - new HealthServiceWrapperHidl.IServiceManagerSupplier() {}, - new HealthServiceWrapperHidl.IHealthSupplier() {}); + new HealthServiceWrapperHidl.IServiceManagerSupplier() { + }, + new HealthServiceWrapperHidl.IHealthSupplier() { + }); } /** * Create a new HealthServiceWrapper instance for testing. * - * @param aidlRegCallback callback for AIDL service registration, or {@code null} if the client - * does not care about AIDL service registration notifications - * @param aidlServiceManager Stub for AIDL ServiceManager - * @param hidlRegCallback callback for HIDL service registration, or {@code null} if the client - * does not care about HIDL service registration notifications + * @param aidlRegCallback callback for AIDL service registration, or + * {@code null} if the client + * does not care about AIDL service + * registration notifications + * @param aidlServiceManager Stub for AIDL ServiceManager + * @param hidlRegCallback callback for HIDL service registration, or + * {@code null} if the client + * does not care about HIDL service + * registration notifications * @param hidlServiceManagerSupplier supplier of HIDL service manager - * @param hidlHealthSupplier supplier of HIDL health HAL - * @return the new HealthServiceWrapper instance, which may be backed by HIDL or AIDL service. + * @param hidlHealthSupplier supplier of HIDL health HAL + * @return the new HealthServiceWrapper instance, which may be backed by HIDL or + * AIDL service. */ @VisibleForTesting static @NonNull HealthServiceWrapper create( diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java index 1ace41cba364..2660db4afc5b 100644 --- a/services/core/java/com/android/server/input/InputManagerInternal.java +++ b/services/core/java/com/android/server/input/InputManagerInternal.java @@ -104,11 +104,16 @@ public abstract class InputManagerInternal { * @param fromChannelToken The channel token of a window that has an active touch gesture. * @param toChannelToken The channel token of the window that should receive the gesture in * place of the first. + * @param transferEntireGesture Whether the entire gesture (including subsequent POINTER_DOWN + * events) should be transferred. This should always be set to + * 'false' unless you have the permission from the input team to + * set it to true. This behaviour will be removed in future + * versions. * @return True if the transfer was successful. False if the specified windows don't exist, or * if the source window is not actively receiving a touch gesture at the time of the request. */ public abstract boolean transferTouchGesture(@NonNull IBinder fromChannelToken, - @NonNull IBinder toChannelToken); + @NonNull IBinder toChannelToken, boolean transferEntireGesture); /** * Gets the current position of the mouse cursor. diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 6af55300d0b3..76284fb81814 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -344,6 +344,9 @@ public class InputManagerService extends IInputManager.Stub // Manages battery state for input devices. private final BatteryController mBatteryController; + // Monitors any changes to the sysfs nodes when an input device is connected. + private final SysfsNodeMonitor mSysfsNodeMonitor; + @Nullable private final TouchpadDebugViewController mTouchpadDebugViewController; @@ -507,8 +510,7 @@ public class InputManagerService extends IInputManager.Stub KeyboardBacklightControllerInterface getKeyboardBacklightController( NativeInputManagerService nativeService) { - return new KeyboardBacklightController(mContext, nativeService, mLooper, - mUEventManager); + return new KeyboardBacklightController(mContext, nativeService, mLooper); } } @@ -536,6 +538,8 @@ public class InputManagerService extends IInputManager.Stub injector.getLooper(), this) : null; mBatteryController = new BatteryController(mContext, mNative, injector.getLooper(), injector.getUEventManager()); + mSysfsNodeMonitor = new SysfsNodeMonitor(mContext, mNative, injector.getLooper(), + injector.getUEventManager()); mKeyboardBacklightController = injector.getKeyboardBacklightController(mNative); mStickyModifierStateController = new StickyModifierStateController(); mInputDataStore = new InputDataStore(); @@ -665,6 +669,7 @@ public class InputManagerService extends IInputManager.Stub mKeyboardLayoutManager.systemRunning(); mBatteryController.systemRunning(); + mSysfsNodeMonitor.systemRunning(); mKeyboardBacklightController.systemRunning(); mKeyboardLedController.systemRunning(); mKeyRemapper.systemRunning(); @@ -1370,7 +1375,7 @@ public class InputManagerService extends IInputManager.Stub public boolean startDragAndDrop(@NonNull IBinder fromChannelToken, @NonNull IBinder dragAndDropChannelToken) { return mNative.transferTouchGesture(fromChannelToken, dragAndDropChannelToken, - true /* isDragDrop */); + true /* isDragDrop */, false /* transferEntireGesture */); } /** @@ -1394,11 +1399,11 @@ public class InputManagerService extends IInputManager.Stub * if the source window is not actively receiving a touch gesture at the time of the request. */ public boolean transferTouchGesture(@NonNull IBinder fromChannelToken, - @NonNull IBinder toChannelToken) { + @NonNull IBinder toChannelToken, boolean transferEntireGesture) { Objects.requireNonNull(fromChannelToken); Objects.requireNonNull(toChannelToken); return mNative.transferTouchGesture(fromChannelToken, toChannelToken, - false /* isDragDrop */); + false /* isDragDrop */, transferEntireGesture); } @Override // Binder call @@ -3703,8 +3708,9 @@ public class InputManagerService extends IInputManager.Stub @Override public boolean transferTouchGesture(@NonNull IBinder fromChannelToken, - @NonNull IBinder toChannelToken) { - return InputManagerService.this.transferTouchGesture(fromChannelToken, toChannelToken); + @NonNull IBinder toChannelToken, boolean transferEntireGesture) { + return InputManagerService.this.transferTouchGesture( + fromChannelToken, toChannelToken, transferEntireGesture); } @Override diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java index 5de432e5849b..b069a87480ad 100644 --- a/services/core/java/com/android/server/input/KeyGestureController.java +++ b/services/core/java/com/android/server/input/KeyGestureController.java @@ -762,7 +762,7 @@ final class KeyGestureController { if (!canceled) { handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0, - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS, + KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, focusedToken, /* flags = */0, /* appLaunchData = */null); } diff --git a/services/core/java/com/android/server/input/KeyboardBacklightController.java b/services/core/java/com/android/server/input/KeyboardBacklightController.java index 16368c7678d1..083c0006ad65 100644 --- a/services/core/java/com/android/server/input/KeyboardBacklightController.java +++ b/services/core/java/com/android/server/input/KeyboardBacklightController.java @@ -32,7 +32,6 @@ import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.os.SystemClock; -import android.os.UEventObserver; import android.sysprop.InputProperties; import android.text.TextUtils; import android.util.IndentingPrintWriter; @@ -83,8 +82,6 @@ final class KeyboardBacklightController implements private static final long TRANSITION_ANIMATION_DURATION_MILLIS = Duration.ofSeconds(1).toMillis(); - private static final String UEVENT_KEYBOARD_BACKLIGHT_TAG = "kbd_backlight"; - @VisibleForTesting static final int[] DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL = new int[DEFAULT_NUM_BRIGHTNESS_CHANGE_STEPS + 1]; @@ -93,7 +90,6 @@ final class KeyboardBacklightController implements private final NativeInputManagerService mNative; private final Handler mHandler; private final AnimatorFactory mAnimatorFactory; - private final UEventManager mUEventManager; // Always access on handler thread or need to lock this for synchronization. private final SparseArray<KeyboardBacklightState> mKeyboardBacklights = new SparseArray<>(1); // Maintains state if all backlights should be on or turned off @@ -124,19 +120,18 @@ final class KeyboardBacklightController implements } KeyboardBacklightController(Context context, NativeInputManagerService nativeService, - Looper looper, UEventManager uEventManager) { - this(context, nativeService, looper, ValueAnimator::ofInt, uEventManager); + Looper looper) { + this(context, nativeService, looper, ValueAnimator::ofInt); } @VisibleForTesting KeyboardBacklightController(Context context, NativeInputManagerService nativeService, - Looper looper, AnimatorFactory animatorFactory, UEventManager uEventManager) { + Looper looper, AnimatorFactory animatorFactory) { mContext = context; mNative = nativeService; mHandler = new Handler(looper, this::handleMessage); mAnimatorFactory = animatorFactory; mAmbientController = new AmbientKeyboardBacklightController(context, looper); - mUEventManager = uEventManager; Resources res = mContext.getResources(); mUserInactivityThresholdMs = res.getInteger( com.android.internal.R.integer.config_keyboardBacklightTimeoutMs); @@ -154,17 +149,6 @@ final class KeyboardBacklightController implements inputManager.getInputDeviceIds()); mHandler.sendMessage(msg); - // Observe UEvents for "kbd_backlight" sysfs nodes. - // We want to observe creation of such LED nodes since they might be created after device - // FD created and InputDevice creation logic doesn't initialize LED nodes which leads to - // backlight not working. - mUEventManager.addListener(new UEventManager.UEventListener() { - @Override - public void onUEvent(UEventObserver.UEvent event) { - onKeyboardBacklightUEvent(event); - } - }, UEVENT_KEYBOARD_BACKLIGHT_TAG); - // Start ambient backlight controller mAmbientController.systemRunning(); } @@ -414,17 +398,6 @@ final class KeyboardBacklightController implements } } - @VisibleForTesting - public void onKeyboardBacklightUEvent(UEventObserver.UEvent event) { - if ("ADD".equalsIgnoreCase(event.get("ACTION")) && "LEDS".equalsIgnoreCase( - event.get("SUBSYSTEM"))) { - final String devPath = event.get("DEVPATH"); - if (isValidBacklightNodePath(devPath)) { - mNative.sysfsNodeChanged("/sys" + devPath); - } - } - } - private void updateAmbientLightListener() { boolean needToListenAmbientLightSensor = false; for (int i = 0; i < mKeyboardBacklights.size(); i++) { diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index 32409d39db3b..de54cd81aa43 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -116,7 +116,7 @@ interface NativeInputManagerService { void setMinTimeBetweenUserActivityPokes(long millis); boolean transferTouchGesture(IBinder fromChannelToken, IBinder toChannelToken, - boolean isDragDrop); + boolean isDragDrop, boolean transferEntireGesture); /** * Transfer the current touch gesture to the window identified by 'destChannelToken' positioned @@ -272,6 +272,9 @@ interface NativeInputManagerService { /** Set whether showing a pointer icon for styluses is enabled. */ void setStylusPointerIconEnabled(boolean enabled); + /** Get the sysfs root path of an input device if known, otherwise return null. */ + @Nullable String getSysfsRootPath(int deviceId); + /** * Report sysfs node changes. This may result in recreation of the corresponding InputDevice. * The recreated device may contain new associated peripheral devices like Light, Battery, etc. @@ -420,7 +423,7 @@ interface NativeInputManagerService { @Override public native boolean transferTouchGesture(IBinder fromChannelToken, IBinder toChannelToken, - boolean isDragDrop); + boolean isDragDrop, boolean transferEntireGesture); @Override @Deprecated @@ -619,6 +622,9 @@ interface NativeInputManagerService { public native void setStylusPointerIconEnabled(boolean enabled); @Override + public native String getSysfsRootPath(int deviceId); + + @Override public native void sysfsNodeChanged(String sysfsNodePath); @Override diff --git a/services/core/java/com/android/server/input/SysfsNodeMonitor.java b/services/core/java/com/android/server/input/SysfsNodeMonitor.java new file mode 100644 index 000000000000..e55e1284d03c --- /dev/null +++ b/services/core/java/com/android/server/input/SysfsNodeMonitor.java @@ -0,0 +1,203 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.input; + +import android.content.Context; +import android.hardware.input.InputManager; +import android.os.Handler; +import android.os.Looper; +import android.os.UEventObserver; +import android.text.TextUtils; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; + +import java.util.Objects; + +/** + * A thread-safe component of {@link InputManagerService} responsible for monitoring the addition + * of kernel sysfs nodes for newly connected input devices. + * + * This class uses the {@link UEventObserver} to monitor for changes to an input device's sysfs + * nodes, and is responsible for requesting the native code to refresh its sysfs nodes when there + * is a change. This is necessary because the sysfs nodes may only be configured after an input + * device is already added, with no way for the native code to detect any changes afterwards. + */ +final class SysfsNodeMonitor { + private static final String TAG = SysfsNodeMonitor.class.getSimpleName(); + + // To enable these logs, run: + // 'adb shell setprop log.tag.SysfsNodeMonitor DEBUG' (requires restart) + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private static final long SYSFS_NODE_MONITORING_TIMEOUT_MS = 60_000; // 1 minute + + private final Context mContext; + private final NativeInputManagerService mNative; + private final Handler mHandler; + private final UEventManager mUEventManager; + + private InputManager mInputManager; + + private final SparseArray<SysfsNodeAddedListener> mUEventListenersByDeviceId = + new SparseArray<>(); + + SysfsNodeMonitor(Context context, NativeInputManagerService nativeService, Looper looper, + UEventManager uEventManager) { + mContext = context; + mNative = nativeService; + mHandler = new Handler(looper); + mUEventManager = uEventManager; + } + + public void systemRunning() { + mInputManager = Objects.requireNonNull(mContext.getSystemService(InputManager.class)); + mInputManager.registerInputDeviceListener(mInputDeviceListener, mHandler); + for (int deviceId : mInputManager.getInputDeviceIds()) { + mInputDeviceListener.onInputDeviceAdded(deviceId); + } + } + + private final InputManager.InputDeviceListener mInputDeviceListener = + new InputManager.InputDeviceListener() { + @Override + public void onInputDeviceAdded(int deviceId) { + startMonitoring(deviceId); + } + + @Override + public void onInputDeviceRemoved(int deviceId) { + stopMonitoring(deviceId); + } + + @Override + public void onInputDeviceChanged(int deviceId) { + } + }; + + private void startMonitoring(int deviceId) { + final var inputDevice = mInputManager.getInputDevice(deviceId); + if (inputDevice == null) { + return; + } + if (!inputDevice.isExternal()) { + if (DEBUG) { + Log.d(TAG, "Not listening to sysfs node changes for internal input device: " + + deviceId); + } + return; + } + final var sysfsRootPath = formatDevPath(mNative.getSysfsRootPath(deviceId)); + if (sysfsRootPath == null) { + if (DEBUG) { + Log.d(TAG, "Sysfs node not found for external input device: " + deviceId); + } + return; + } + if (DEBUG) { + Log.d(TAG, "Start listening to sysfs node changes for input device: " + deviceId + + ", node: " + sysfsRootPath); + } + final var listener = new SysfsNodeAddedListener(); + mUEventListenersByDeviceId.put(deviceId, listener); + + // We must synchronously start monitoring for changes to this device's path. + // Once monitoring starts, we need to trigger a native refresh of the sysfs nodes to + // catch any changes that happened between the input device's creation and the UEvent + // listener being added. + // NOTE: This relies on the fact that the following `addListener` call is fully synchronous. + mUEventManager.addListener(listener, sysfsRootPath); + mNative.sysfsNodeChanged(sysfsRootPath); + + // Always stop listening for new sysfs nodes after the timeout. + mHandler.postDelayed(() -> stopMonitoring(deviceId), SYSFS_NODE_MONITORING_TIMEOUT_MS); + } + + private static String formatDevPath(String path) { + // Remove the "/sys" prefix if it has one. + return path != null && path.startsWith("/sys") ? path.substring(4) : path; + } + + private void stopMonitoring(int deviceId) { + final var listener = mUEventListenersByDeviceId.removeReturnOld(deviceId); + if (listener == null) { + return; + } + if (DEBUG) { + Log.d(TAG, "Stop listening to sysfs node changes for input device: " + deviceId); + } + mUEventManager.removeListener(listener); + } + + class SysfsNodeAddedListener extends UEventManager.UEventListener { + + private boolean mHasReceivedRemovalNotification = false; + private boolean mHasReceivedPowerSupplyNotification = false; + + @Override + public void onUEvent(UEventObserver.UEvent event) { + // This callback happens on the UEventObserver's thread. + // Ensure we are processing on the handler thread. + mHandler.post(() -> handleUEvent(event)); + } + + private void handleUEvent(UEventObserver.UEvent event) { + if (DEBUG) { + Slog.d(TAG, "UEventListener: Received UEvent: " + event); + } + final var subsystem = event.get("SUBSYSTEM"); + final var devPath = "/sys" + Objects.requireNonNull( + TextUtils.nullIfEmpty(event.get("DEVPATH"))); + final var action = event.get("ACTION"); + + // NOTE: We must be careful to avoid reconfiguring sysfs nodes during device removal, + // because it might result in the device getting re-opened in native code during + // removal, resulting in unexpected states. If we see any removal action for this node, + // ensure we stop responding altogether. + if (mHasReceivedRemovalNotification || "REMOVE".equalsIgnoreCase(action)) { + mHasReceivedRemovalNotification = true; + return; + } + + if ("LEDS".equalsIgnoreCase(subsystem) && "ADD".equalsIgnoreCase(action)) { + // An LED node was added. Notify native code to reconfigure the sysfs node. + if (DEBUG) { + Slog.d(TAG, + "Reconfiguring sysfs node because 'leds' node was added: " + devPath); + } + mNative.sysfsNodeChanged(devPath); + return; + } + + if ("POWER_SUPPLY".equalsIgnoreCase(subsystem)) { + if (mHasReceivedPowerSupplyNotification) { + return; + } + // This is the first notification we received from the power_supply subsystem. + // Notify native code that the battery node may have been added. The power_supply + // subsystem does not seem to be sending ADD events, so use use the first event + // with any action as a proxy for a new power_supply node being created. + if (DEBUG) { + Slog.d(TAG, "Reconfiguring sysfs node because 'power_supply' node had action '" + + action + "': " + devPath); + } + mHasReceivedPowerSupplyNotification = true; + mNative.sysfsNodeChanged(devPath); + } + } + } +} diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java index 2c1d68e3dbda..8d664e848ef5 100644 --- a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java +++ b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java @@ -55,15 +55,17 @@ public class TouchpadDebugViewController implements InputManager.InputDeviceList @Override public void onInputDeviceAdded(int deviceId) { + if (!mTouchpadVisualizerEnabled) { + return; + } final InputManager inputManager = Objects.requireNonNull( mContext.getSystemService(InputManager.class)); InputDevice inputDevice = inputManager.getInputDevice(deviceId); - - if (Objects.requireNonNull(inputDevice).supportsSource( - InputDevice.SOURCE_TOUCHPAD | InputDevice.SOURCE_MOUSE) - && mTouchpadVisualizerEnabled) { - showDebugView(deviceId); + if (inputDevice == null || !inputDevice.supportsSource( + InputDevice.SOURCE_TOUCHPAD | InputDevice.SOURCE_MOUSE)) { + return; } + showDebugView(deviceId); } @Override diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java index 9d889839879b..c2873e8ee28e 100644 --- a/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java +++ b/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java @@ -256,9 +256,11 @@ final class IInputMethodClientInvoker { @AnyThread private void setImeVisibilityInternal(boolean visible, @Nullable ImeTracker.Token statsToken) { try { + ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_CLIENT_INVOKER); mTarget.setImeVisibility(visible, statsToken); } catch (RemoteException e) { logRemoteException(e); + ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_CLIENT_INVOKER); } } diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java index 0047ec20d691..3fcb6ce271e3 100644 --- a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java +++ b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java @@ -205,9 +205,11 @@ final class IInputMethodInvoker { boolean showSoftInput(IBinder showInputToken, @NonNull ImeTracker.Token statsToken, @InputMethod.ShowFlags int flags, ResultReceiver resultReceiver) { try { + ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_IME_INVOKER); mTarget.showSoftInput(showInputToken, statsToken, flags, resultReceiver); } catch (RemoteException e) { logRemoteException(e); + ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_IME_INVOKER); return false; } return true; @@ -218,9 +220,11 @@ final class IInputMethodInvoker { boolean hideSoftInput(IBinder hideInputToken, @NonNull ImeTracker.Token statsToken, int flags, ResultReceiver resultReceiver) { try { + ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_IME_INVOKER); mTarget.hideSoftInput(hideInputToken, statsToken, flags, resultReceiver); } catch (RemoteException e) { logRemoteException(e); + ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_IME_INVOKER); return false; } return true; diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 2066dbc87a0d..5e3224d1012e 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -1826,14 +1826,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @NonNull UserData userData) { final int userId = userData.mUserId; if (userData.mCurClient == client) { - if (Flags.refactorInsetsController()) { - final var statsToken = createStatsTokenForFocusedClient(false /* show */, - SoftInputShowHideReason.HIDE_REMOVE_CLIENT, userId); - setImeVisibilityOnFocusedWindowClient(false, userData, statsToken); - } else { - hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */, - SoftInputShowHideReason.HIDE_REMOVE_CLIENT, userId); - } + hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */, + SoftInputShowHideReason.HIDE_REMOVE_CLIENT, userId); if (userData.mBoundToMethod) { userData.mBoundToMethod = false; final var userBindingController = userData.mBindingController; @@ -2103,14 +2097,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } if (visibilityStateComputer.getImePolicy().isImeHiddenByDisplayPolicy()) { - if (Flags.refactorInsetsController()) { - final var statsToken = createStatsTokenForFocusedClient(false /* show */, - SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE, userId); - setImeVisibilityOnFocusedWindowClient(false, userData, statsToken); - } else { - hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */, - SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE, userId); - } + hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */, + SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE, userId); return InputBindResult.NO_IME; } @@ -3206,7 +3194,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } // TODO(b/353463205) check callers to see if we can make statsToken @NonNull - boolean showCurrentInputInternal(IBinder windowToken, @Nullable ImeTracker.Token statsToken) { + boolean showCurrentInputInternal(IBinder windowToken, @NonNull ImeTracker.Token statsToken) { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showCurrentInputInternal"); ImeTracing.getInstance().triggerManagerServiceDump( "InputMethodManagerService#showSoftInput", mDumper); @@ -3226,7 +3214,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } // TODO(b/353463205) check callers to see if we can make statsToken @NonNull - boolean hideCurrentInputInternal(IBinder windowToken, @Nullable ImeTracker.Token statsToken) { + boolean hideCurrentInputInternal(IBinder windowToken, @NonNull ImeTracker.Token statsToken) { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideCurrentInputInternal"); ImeTracing.getInstance().triggerManagerServiceDump( "InputMethodManagerService#hideSoftInput", mDumper); @@ -3867,17 +3855,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. Slog.w(TAG, "If you need to impersonate a foreground user/profile from" + " a background user, use EditorInfo.targetInputMethodUser with" + " INTERACT_ACROSS_USERS_FULL permission."); - - if (Flags.refactorInsetsController()) { - final var statsToken = createStatsTokenForFocusedClient( - false /* show */, SoftInputShowHideReason.HIDE_INVALID_USER, - userId); - setImeVisibilityOnFocusedWindowClient(false, userData, statsToken); - } else { - hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, - 0 /* flags */, SoftInputShowHideReason.HIDE_INVALID_USER, - userId); - } + hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, + 0 /* flags */, SoftInputShowHideReason.HIDE_INVALID_USER, userId); return InputBindResult.INVALID_USER; } @@ -5014,6 +4993,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. setImeVisibilityOnFocusedWindowClient(false, userData, null /* TODO(b/353463205) check statsToken */); } else { + hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */, reason, userId); } @@ -5800,7 +5780,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. return false; } } - return mInputManagerInternal.transferTouchGesture(sourceInputToken, curHostInputToken); + return mInputManagerInternal.transferTouchGesture( + sourceInputToken, curHostInputToken, /* transferEntireGesture */ false); } @Override @@ -6708,9 +6689,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); final var userData = getUserData(userId); if (Flags.refactorInsetsController()) { - final var statsToken = createStatsTokenForFocusedClient(false /* show */, - SoftInputShowHideReason.HIDE_RESET_SHELL_COMMAND, userId); - setImeVisibilityOnFocusedWindowClient(false, userData, statsToken); + setImeVisibilityOnFocusedWindowClient(false, userData, + null /* TODO(b329229469) initialize statsToken here? */); } else { hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */, diff --git a/services/core/java/com/android/server/location/LocationManagerService.java b/services/core/java/com/android/server/location/LocationManagerService.java index 6780866d4038..6dff2d8d0a98 100644 --- a/services/core/java/com/android/server/location/LocationManagerService.java +++ b/services/core/java/com/android/server/location/LocationManagerService.java @@ -475,7 +475,8 @@ public class LocationManagerService extends ILocationManager.Stub implements FUSED_PROVIDER, ACTION_FUSED_PROVIDER, com.android.internal.R.bool.config_enableFusedLocationOverlay, - com.android.internal.R.string.config_fusedLocationProviderPackageName); + com.android.internal.R.string.config_fusedLocationProviderPackageName, + com.android.internal.R.bool.config_fusedLocationOverlayUnstableFallback); if (fusedProvider != null) { LocationProviderManager fusedManager = new LocationProviderManager(mContext, mInjector, FUSED_PROVIDER, mPassiveManager); @@ -498,14 +499,13 @@ public class LocationManagerService extends ILocationManager.Stub implements com.android.internal.R.bool.config_useGnssHardwareProvider); AbstractLocationProvider gnssProvider = null; if (!useGnssHardwareProvider) { - // TODO: Create a separate config_enableGnssLocationOverlay config resource - // if we want to selectively enable a GNSS overlay but disable a fused overlay. gnssProvider = ProxyLocationProvider.create( mContext, GPS_PROVIDER, ACTION_GNSS_PROVIDER, - com.android.internal.R.bool.config_enableFusedLocationOverlay, - com.android.internal.R.string.config_gnssLocationProviderPackageName); + com.android.internal.R.bool.config_enableGnssLocationOverlay, + com.android.internal.R.string.config_gnssLocationProviderPackageName, + com.android.internal.R.bool.config_gnssLocationOverlayUnstableFallback); } if (gnssProvider == null) { gnssProvider = mGnssManagerService.getGnssLocationProvider(); diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java index bbf7732c9596..7059c83d60b2 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java @@ -44,6 +44,7 @@ import android.util.Log; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import java.util.Collection; import java.util.HashSet; @@ -53,6 +54,9 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; /** @@ -71,6 +75,9 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub /** The duration of wakelocks acquired during HAL callbacks */ private static final long WAKELOCK_TIMEOUT_MILLIS = 5 * 1000; + /** The timeout of open session request */ + @VisibleForTesting static final long OPEN_SESSION_REQUEST_TIMEOUT_SECONDS = 10; + /* * Internal interface used to invoke client callbacks. */ @@ -81,6 +88,9 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub /** The context of the service. */ private final Context mContext; + /** The shared executor service for handling session operation timeout. */ + private final ScheduledExecutorService mSessionTimeoutExecutor; + /** The proxy to talk to the Context Hub HAL for endpoint communication. */ private final IEndpointCommunication mHubInterface; @@ -119,6 +129,8 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub private SessionState mSessionState = SessionState.PENDING; + private ScheduledFuture<?> mSessionOpenTimeoutFuture; + private final boolean mRemoteInitiated; /** @@ -151,6 +163,17 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub mSessionState = state; } + public void setSessionOpenTimeoutFuture(ScheduledFuture<?> future) { + mSessionOpenTimeoutFuture = future; + } + + public void cancelSessionOpenTimeoutFuture() { + if (mSessionOpenTimeoutFuture != null) { + mSessionOpenTimeoutFuture.cancel(false); + } + mSessionOpenTimeoutFuture = null; + } + public boolean isActive() { return mSessionState == SessionState.ACTIVE; } @@ -240,7 +263,8 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub @NonNull IContextHubEndpointCallback callback, String packageName, String attributionTag, - ContextHubTransactionManager transactionManager) { + ContextHubTransactionManager transactionManager, + ScheduledExecutorService sessionTimeoutExecutor) { mContext = context; mHubInterface = hubInterface; mEndpointManager = endpointManager; @@ -250,6 +274,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub mPackageName = packageName; mAttributionTag = attributionTag; mTransactionManager = transactionManager; + mSessionTimeoutExecutor = sessionTimeoutExecutor; mPid = Binder.getCallingPid(); mUid = Binder.getCallingUid(); @@ -352,6 +377,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } try { mHubInterface.endpointSessionOpenComplete(sessionId); + info.cancelSessionOpenTimeoutFuture(); info.setSessionState(Session.SessionState.ACTIVE); } catch (RemoteException | IllegalArgumentException | UnsupportedOperationException e) { Log.e(TAG, "Exception while calling endpointSessionOpenComplete", e); @@ -636,9 +662,10 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } // Check & handle error cases for duplicated session id. - final boolean existingSession; - final boolean existingSessionActive; synchronized (mOpenSessionLock) { + final boolean existingSession; + final boolean existingSessionActive; + if (hasSessionId(sessionId)) { existingSession = true; existingSessionActive = mSessionMap.get(sessionId).isActive(); @@ -652,19 +679,23 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } else { existingSession = false; existingSessionActive = false; - mSessionMap.put(sessionId, new Session(initiator, true)); + Session pendingSession = new Session(initiator, true); + pendingSession.setSessionOpenTimeoutFuture( + mSessionTimeoutExecutor.schedule( + () -> onEndpointSessionOpenRequestTimeout(sessionId), + OPEN_SESSION_REQUEST_TIMEOUT_SECONDS, + TimeUnit.SECONDS)); + mSessionMap.put(sessionId, pendingSession); } - } - if (existingSession) { - if (existingSessionActive) { - // Existing session is already active, call onSessionOpenComplete. - openSessionRequestComplete(sessionId); + if (existingSession) { + if (existingSessionActive) { + // Existing session is already active, call onSessionOpenComplete. + openSessionRequestComplete(sessionId); + } + // Silence this request. The session open timeout future will handle clean up. return Optional.empty(); } - // Reject the session open request for now. Consider invalidating previous pending - // session open request based on timeout. - return Optional.of(Reason.OPEN_ENDPOINT_SESSION_REQUEST_REJECTED); } boolean success = @@ -679,6 +710,20 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub return success ? Optional.empty() : Optional.of(reason); } + private void onEndpointSessionOpenRequestTimeout(int sessionId) { + synchronized (mOpenSessionLock) { + Session s = mSessionMap.get(sessionId); + if (s == null || s.isActive()) { + return; + } + Log.w( + TAG, + "onEndpointSessionOpenRequestTimeout: " + "clean up session, id: " + sessionId); + cleanupSessionResources(sessionId); + mEndpointManager.halCloseEndpointSessionNoThrow(sessionId, Reason.TIMEOUT); + } + } + private byte onMessageReceivedInternal(int sessionId, HubMessage message) { synchronized (mOpenSessionLock) { if (!isSessionActive(sessionId)) { diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java index e1561599517d..0dc1b832f5a4 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java @@ -46,6 +46,8 @@ import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.function.Consumer; /** @@ -112,6 +114,9 @@ import java.util.function.Consumer; /** The interface for endpoint communication (retrieved from HAL in init()) */ private IEndpointCommunication mHubInterface = null; + /** Thread pool executor for handling timeout */ + private final ScheduledExecutorService mSessionTimeoutExecutor; + /* * The list of previous registration records. */ @@ -154,15 +159,31 @@ import java.util.function.Consumer; } } - /* package */ ContextHubEndpointManager( + @VisibleForTesting + ContextHubEndpointManager( Context context, IContextHubWrapper contextHubProxy, HubInfoRegistry hubInfoRegistry, - ContextHubTransactionManager transactionManager) { + ContextHubTransactionManager transactionManager, + ScheduledExecutorService scheduledExecutorService) { mContext = context; mContextHubProxy = contextHubProxy; mHubInfoRegistry = hubInfoRegistry; mTransactionManager = transactionManager; + mSessionTimeoutExecutor = scheduledExecutorService; + } + + /* package */ ContextHubEndpointManager( + Context context, + IContextHubWrapper contextHubProxy, + HubInfoRegistry hubInfoRegistry, + ContextHubTransactionManager transactionManager) { + this( + context, + contextHubProxy, + hubInfoRegistry, + transactionManager, + new ScheduledThreadPoolExecutor(1)); } /** @@ -264,7 +285,8 @@ import java.util.function.Consumer; callback, packageName, attributionTag, - mTransactionManager); + mTransactionManager, + mSessionTimeoutExecutor); broker.register(); mEndpointMap.put(endpointId, broker); diff --git a/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java b/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java index 3f75b11befc2..ea4b3d426346 100644 --- a/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java +++ b/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java @@ -23,6 +23,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; +import android.location.flags.Flags; import android.os.SystemClock; import android.telephony.TelephonyCallback; import android.telephony.TelephonyManager; @@ -71,13 +72,25 @@ public class SystemEmergencyHelper extends EmergencyHelper { return; } - synchronized (SystemEmergencyHelper.this) { + if (Flags.fixIsInEmergencyAnr()) { try { - mIsInEmergencyCall = mTelephonyManager.isEmergencyNumber( + boolean isInEmergency = mTelephonyManager.isEmergencyNumber( intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER)); + synchronized (SystemEmergencyHelper.this) { + mIsInEmergencyCall = isInEmergency; + } } catch (IllegalStateException | UnsupportedOperationException e) { Log.w(TAG, "Failed to call TelephonyManager.isEmergencyNumber().", e); } + } else { + synchronized (SystemEmergencyHelper.this) { + try { + mIsInEmergencyCall = mTelephonyManager.isEmergencyNumber( + intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER)); + } catch (IllegalStateException | UnsupportedOperationException e) { + Log.w(TAG, "Failed to call TelephonyManager.isEmergencyNumber().", e); + } + } } dispatchEmergencyStateChanged(); @@ -98,27 +111,55 @@ public class SystemEmergencyHelper extends EmergencyHelper { } @Override - public synchronized boolean isInEmergency(long extensionTimeMs) { - if (mTelephonyManager == null) { - return false; - } + public boolean isInEmergency(long extensionTimeMs) { + if (Flags.fixIsInEmergencyAnr()) { + if (mTelephonyManager == null) { + return false; + } + boolean emergencyCallbackMode = false; + boolean emergencySmsMode = false; + PackageManager pm = mContext.getPackageManager(); + if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) { + emergencyCallbackMode = mTelephonyManager.getEmergencyCallbackMode(); + } + if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)) { + emergencySmsMode = mTelephonyManager.isInEmergencySmsMode(); + } + boolean isInExtensionTime; + synchronized (this) { + isInExtensionTime = mEmergencyCallEndRealtimeMs != Long.MIN_VALUE + && (SystemClock.elapsedRealtime() - mEmergencyCallEndRealtimeMs) + < extensionTimeMs; + return mIsInEmergencyCall + || isInExtensionTime + || emergencyCallbackMode + || emergencySmsMode; + } + } else { + synchronized (this) { + if (mTelephonyManager == null) { + return false; + } - boolean isInExtensionTime = mEmergencyCallEndRealtimeMs != Long.MIN_VALUE - && (SystemClock.elapsedRealtime() - mEmergencyCallEndRealtimeMs) < extensionTimeMs; + boolean isInExtensionTime = mEmergencyCallEndRealtimeMs != Long.MIN_VALUE + && (SystemClock.elapsedRealtime() - mEmergencyCallEndRealtimeMs) + < extensionTimeMs; - boolean emergencyCallbackMode = false; - boolean emergencySmsMode = false; - PackageManager pm = mContext.getPackageManager(); - if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) { - emergencyCallbackMode = mTelephonyManager.getEmergencyCallbackMode(); - } - if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)) { - emergencySmsMode = mTelephonyManager.isInEmergencySmsMode(); + boolean emergencyCallbackMode = false; + boolean emergencySmsMode = false; + PackageManager pm = mContext.getPackageManager(); + if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) { + emergencyCallbackMode = mTelephonyManager.getEmergencyCallbackMode(); + } + if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)) { + emergencySmsMode = mTelephonyManager.isInEmergencySmsMode(); + } + return mIsInEmergencyCall + || isInExtensionTime + || emergencyCallbackMode + || emergencySmsMode; + } } - return mIsInEmergencyCall - || isInExtensionTime - || emergencyCallbackMode - || emergencySmsMode; } private class EmergencyCallTelephonyCallback extends TelephonyCallback implements diff --git a/services/core/java/com/android/server/location/provider/proxy/ProxyLocationProvider.java b/services/core/java/com/android/server/location/provider/proxy/ProxyLocationProvider.java index 8fdc22b81769..a52b948dc53f 100644 --- a/services/core/java/com/android/server/location/provider/proxy/ProxyLocationProvider.java +++ b/services/core/java/com/android/server/location/provider/proxy/ProxyLocationProvider.java @@ -77,6 +77,22 @@ public class ProxyLocationProvider extends AbstractLocationProvider implements } } + /** + * Creates and registers this proxy. If no suitable service is available for the proxy, returns + * null. + */ + @Nullable + public static ProxyLocationProvider create(Context context, String provider, String action, + int enableOverlayResId, int nonOverlayPackageResId, int unstableOverlayFallbackResId) { + ProxyLocationProvider proxy = new ProxyLocationProvider(context, provider, action, + enableOverlayResId, nonOverlayPackageResId, unstableOverlayFallbackResId); + if (proxy.checkServiceResolves()) { + return proxy; + } else { + return null; + } + } + final Object mLock = new Object(); final Context mContext; @@ -111,6 +127,24 @@ public class ProxyLocationProvider extends AbstractLocationProvider implements mRequest = ProviderRequest.EMPTY_REQUEST; } + private ProxyLocationProvider(Context context, String provider, String action, + int enableOverlayResId, int nonOverlayPackageResId, int unstableOverlayFallbackResId) { + // safe to use direct executor since our locks are not acquired in a code path invoked by + // our owning provider + super(DIRECT_EXECUTOR, null, null, Collections.emptySet()); + + mContext = context; + boolean unstableFallbackEnabled = + context.getResources().getBoolean(unstableOverlayFallbackResId); + mServiceWatcher = ServiceWatcher.create(context, provider, unstableFallbackEnabled, + CurrentUserServiceSupplier.createFromConfig(context, action, enableOverlayResId, + nonOverlayPackageResId), this); + mName = provider; + + mProxy = null; + mRequest = ProviderRequest.EMPTY_REQUEST; + } + private boolean checkServiceResolves() { return mServiceWatcher.checkServiceResolves(); } diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java index 85dc811a7811..80cb5480fec1 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java @@ -168,6 +168,11 @@ public class KeySyncTask implements Runnable { } private void syncKeys() throws RemoteException { + if (mCredential != null && mCredential.length >= 80) { + // The value is likely a randomly generated profile password + // It doesn't match string typed by the user. + Log.e(TAG, "Unexpected credential length for user " + mUserId); + } if (mCredentialUpdated && mRecoverableKeyStoreDb.getBadRemoteGuessCounter(mUserId) != 0) { mRecoverableKeyStoreDb.setBadRemoteGuessCounter(mUserId, 0); } diff --git a/services/core/java/com/android/server/media/LegacyDeviceRouteController.java b/services/core/java/com/android/server/media/LegacyDeviceRouteController.java index 65b0ad0d61a0..1e8ebca7f336 100644 --- a/services/core/java/com/android/server/media/LegacyDeviceRouteController.java +++ b/services/core/java/com/android/server/media/LegacyDeviceRouteController.java @@ -39,6 +39,7 @@ import android.os.UserHandle; import android.util.Slog; import com.android.internal.R; +import com.android.media.flags.Flags; import java.util.Collections; import java.util.List; @@ -123,7 +124,9 @@ import java.util.Objects; @Override public synchronized List<MediaRoute2Info> getAvailableRoutes() { - return Collections.emptyList(); + return Flags.enableFixForEmptySystemRoutesCrash() + ? List.of(mDeviceRoute) + : Collections.emptyList(); } @Override diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index 988924d9f498..e4e81ac0e720 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -25,7 +25,6 @@ import static android.media.MediaRouter2.SCANNING_STATE_SCANNING_FULL; import static android.media.MediaRouter2.SCANNING_STATE_WHILE_INTERACTIVE; import static android.media.MediaRouter2Utils.getOriginalId; import static android.media.MediaRouter2Utils.getProviderId; - import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_CREATE_SESSION; import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_DESELECT_ROUTE; @@ -78,14 +77,12 @@ import android.util.ArrayMap; import android.util.Log; import android.util.Slog; import android.util.SparseArray; - import com.android.internal.annotations.GuardedBy; import com.android.internal.util.function.pooled.PooledLambda; import com.android.media.flags.Flags; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; import com.android.server.statusbar.StatusBarManagerInternal; - import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -3857,6 +3854,16 @@ class MediaRouter2ServiceImpl { && activelyScanningPackages.equals(mUserRecord.mActivelyScanningPackages)) { return false; } + + var oldShouldPerformActiveScan = + mUserRecord.mCompositeDiscoveryPreference.shouldPerformActiveScan(); + var newShouldPerformActiveScan = newPreference.shouldPerformActiveScan(); + if (oldShouldPerformActiveScan != newShouldPerformActiveScan) { + // State access is synchronized with service.mLock. + // Linter still fails due to b/323906305#comment3 + mMediaRouterMetricLogger.updateScanningState(newShouldPerformActiveScan); + } + mUserRecord.mCompositeDiscoveryPreference = newPreference; mUserRecord.mActivelyScanningPackages = activelyScanningPackages; } diff --git a/services/core/java/com/android/server/media/MediaRouterMetricLogger.java b/services/core/java/com/android/server/media/MediaRouterMetricLogger.java index 56d2a1b22254..dc94ff2a063b 100644 --- a/services/core/java/com/android/server/media/MediaRouterMetricLogger.java +++ b/services/core/java/com/android/server/media/MediaRouterMetricLogger.java @@ -16,6 +16,8 @@ package com.android.server.media; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_SCANNING_STARTED; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_SCANNING_STOPPED; import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_FAILED_TO_REROUTE_SYSTEM_MEDIA; import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_INVALID_COMMAND; import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_NETWORK_ERROR; @@ -114,6 +116,33 @@ final class MediaRouterMetricLogger { } /** + * Logs the overall scanning state. + * + * @param isScanning The scanning state for the user. + */ + public void updateScanningState(boolean isScanning) { + if (!isScanning) { + logScanningStopped(); + } else { + logScanningStarted(); + } + } + + /** Logs the scanning started event. */ + private void logScanningStarted() { + logMediaRouterEvent( + MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_SCANNING_STARTED, + MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_UNSPECIFIED); + } + + /** Logs the scanning stopped event. */ + private void logScanningStopped() { + logMediaRouterEvent( + MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_SCANNING_STOPPED, + MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_UNSPECIFIED); + } + + /** * Converts a reason code from {@link MediaRoute2ProviderService} to a result code for logging. * * @param reason The reason code from {@link MediaRoute2ProviderService}. diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index c174451e8f5b..5d571de2ce54 100644 --- a/services/core/java/com/android/server/media/MediaSessionService.java +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -3269,13 +3269,21 @@ public class MediaSessionService extends SystemService implements Monitor { if (!postedNotification.isMediaNotification()) { return; } + if ((postedNotification.flags & Notification.FLAG_FOREGROUND_SERVICE) == 0) { + // Ignore notifications posted without a foreground service. + return; + } synchronized (mLock) { Map<String, StatusBarNotification> notifications = mMediaNotifications.get(uid); if (notifications == null) { notifications = new HashMap<>(); mMediaNotifications.put(uid, notifications); } - notifications.put(sbn.getKey(), sbn); + StatusBarNotification previousSbn = notifications.put(sbn.getKey(), sbn); + if (previousSbn != null) { + // Only act on the first notification update. + return; + } MediaSessionRecordImpl userEngagedRecord = getUserEngagedMediaSessionRecordForNotification(uid, postedNotification); if (userEngagedRecord != null) { diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java index 9a09807d52d7..a3d9c66c2668 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityService.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java @@ -40,10 +40,13 @@ import android.hardware.tv.mediaquality.PictureParameter; import android.hardware.tv.mediaquality.PictureParameters; import android.hardware.tv.mediaquality.SoundParameter; import android.hardware.tv.mediaquality.SoundParameters; +import android.hardware.tv.mediaquality.StreamStatus; import android.hardware.tv.mediaquality.VendorParamCapability; +import android.media.quality.ActiveProcessingPicture; import android.media.quality.AmbientBacklightEvent; import android.media.quality.AmbientBacklightMetadata; import android.media.quality.AmbientBacklightSettings; +import android.media.quality.IActiveProcessingPictureListener; import android.media.quality.IAmbientBacklightCallback; import android.media.quality.IMediaQualityManager; import android.media.quality.IPictureProfileCallback; @@ -57,7 +60,9 @@ import android.media.quality.SoundProfileHandle; import android.os.Binder; import android.os.Bundle; import android.os.Environment; +import android.os.Handler; import android.os.IBinder; +import android.os.Looper; import android.os.Parcel; import android.os.PersistableBundle; import android.os.RemoteCallbackList; @@ -69,6 +74,8 @@ import android.util.Log; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; +import android.view.SurfaceControlActivePicture; +import android.view.SurfaceControlActivePictureListener; import com.android.internal.annotations.GuardedBy; import com.android.server.SystemService; @@ -102,6 +109,7 @@ public class MediaQualityService extends SystemService { private final MediaQualityDbHelper mMediaQualityDbHelper; private final BiMap<Long, String> mPictureProfileTempIdMap; private final BiMap<Long, String> mSoundProfileTempIdMap; + private final Map<String, Long> mPackageDefaultPictureProfileHandleMap = new HashMap<>(); private IMediaQuality mMediaQuality; private PictureProfileAdjustmentListenerImpl mPictureProfileAdjListener; private SoundProfileAdjustmentListenerImpl mSoundProfileAdjListener; @@ -116,6 +124,8 @@ public class MediaQualityService extends SystemService { private HalNotifier mHalNotifier; private MqManagerNotifier mMqManagerNotifier; private MqDatabaseUtils mMqDatabaseUtils; + private Handler mHandler; + private SurfaceControlActivePictureListener mSurfaceControlActivePictureListener; // A global lock for picture profile objects. private final Object mPictureProfileLock = new Object(); @@ -126,6 +136,9 @@ public class MediaQualityService extends SystemService { // A global lock for ambient backlight objects. private final Object mAmbientBacklightLock = new Object(); + private final Map<Long, PictureProfile> mHandleToPictureProfile = new HashMap<>(); + private final BiMap<Long, Long> mCurrentPictureHandleToOriginal = new BiMap<>(); + public MediaQualityService(Context context) { super(context); mContext = context; @@ -141,6 +154,7 @@ public class MediaQualityService extends SystemService { mHalNotifier = new HalNotifier(); mPictureProfileAdjListener = new PictureProfileAdjustmentListenerImpl(); mSoundProfileAdjListener = new SoundProfileAdjustmentListenerImpl(); + mHandler = new Handler(Looper.getMainLooper()); // The package info in the context isn't initialized in the way it is for normal apps, // so the standard, name-based context.getSharedPreferences doesn't work. Instead, we @@ -156,6 +170,7 @@ public class MediaQualityService extends SystemService { soundProfilePrefs, Context.MODE_PRIVATE); } + @GuardedBy("mPictureProfileLock") @Override public void onStart() { IBinder binder = ServiceManager.getService(IMediaQuality.DESCRIPTOR + "/default"); @@ -165,6 +180,14 @@ public class MediaQualityService extends SystemService { } Slogf.d(TAG, "Binder is not null"); + mSurfaceControlActivePictureListener = new SurfaceControlActivePictureListener() { + @Override + public void onActivePicturesChanged(SurfaceControlActivePicture[] activePictures) { + handleOnActivePicturesChanged(activePictures); + } + }; + mSurfaceControlActivePictureListener.startListening(); // TODO: stop listening + mMediaQuality = IMediaQuality.Stub.asInterface(binder); if (mMediaQuality != null) { try { @@ -176,6 +199,22 @@ public class MediaQualityService extends SystemService { mMediaQuality.setPictureProfileAdjustmentListener(mPictureProfileAdjListener); mMediaQuality.setSoundProfileAdjustmentListener(mSoundProfileAdjListener); + synchronized (mPictureProfileLock) { + String selection = BaseParameters.PARAMETER_TYPE + " = ? AND " + + BaseParameters.PARAMETER_NAME + " = ?"; + String[] selectionArguments = { + Integer.toString(PictureProfile.TYPE_SYSTEM), + PictureProfile.NAME_DEFAULT + }; + List<PictureProfile> packageDefaultPictureProfiles = + mMqDatabaseUtils.getPictureProfilesBasedOnConditions(MediaQualityUtils + .getMediaProfileColumns(false), selection, selectionArguments); + mPackageDefaultPictureProfileHandleMap.clear(); + for (PictureProfile profile : packageDefaultPictureProfiles) { + mPackageDefaultPictureProfileHandleMap.put( + profile.getPackageName(), profile.getHandle().getId()); + } + } } catch (RemoteException e) { Slog.e(TAG, "Failed to set ambient backlight detector callback", e); } @@ -184,60 +223,122 @@ public class MediaQualityService extends SystemService { publishBinderService(Context.MEDIA_QUALITY_SERVICE, new BinderService()); } - private final class BinderService extends IMediaQualityManager.Stub { + private void handleOnActivePicturesChanged(SurfaceControlActivePicture[] scActivePictures) { + if (DEBUG) { + Slog.d(TAG, "handleOnActivePicturesChanged"); + } + synchronized (mPictureProfileLock) { + // TODO handle other users + UserState userState = getOrCreateUserState(UserHandle.USER_SYSTEM); + int n = userState.mActiveProcessingPictureCallbackList.beginBroadcast(); + for (int i = 0; i < n; ++i) { + try { + IActiveProcessingPictureListener l = userState + .mActiveProcessingPictureCallbackList + .getBroadcastItem(i); + ActiveProcessingPictureListenerInfo info = + userState.mActiveProcessingPictureListenerMap.get(l); + if (info == null) { + continue; + } + int uid = info.mUid; + boolean hasGlobalPermission = mContext.checkPermission( + android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE, + info.mPid, uid) + == PackageManager.PERMISSION_GRANTED; + List<ActiveProcessingPicture> aps = new ArrayList<>(); + for (SurfaceControlActivePicture scap : scActivePictures) { + if (!hasGlobalPermission && scap.getOwnerUid() != uid) { + // should not receive the event + continue; + } + String profileId = mPictureProfileTempIdMap.getValue( + scap.getPictureProfileHandle().getId()); + if (profileId == null) { + continue; + } + aps.add(new ActiveProcessingPicture( + scap.getLayerId(), profileId, scap.getOwnerUid() != uid)); - @GuardedBy("mPictureProfileLock") - @Override - public PictureProfile createPictureProfile(PictureProfile pp, int userId) { - if ((pp.getPackageName() != null && !pp.getPackageName().isEmpty() - && !incomingPackageEqualsCallingUidPackage(pp.getPackageName())) - && !hasGlobalPictureQualityServicePermission()) { - mMqManagerNotifier.notifyOnPictureProfileError(null, - PictureProfile.ERROR_NO_PERMISSION, - Binder.getCallingUid(), Binder.getCallingPid()); + } + + l.onActiveProcessingPicturesChanged(aps); + } catch (RemoteException e) { + Slog.e(TAG, "failed to report added AD service to callback", e); + } } + userState.mActiveProcessingPictureCallbackList.finishBroadcast(); + } + } - synchronized (mPictureProfileLock) { - SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); + private final class BinderService extends IMediaQualityManager.Stub { - ContentValues values = MediaQualityUtils.getContentValues(null, - pp.getProfileType(), - pp.getName(), - pp.getPackageName() == null || pp.getPackageName().isEmpty() - ? getPackageOfCallingUid() : pp.getPackageName(), - pp.getInputId(), - pp.getParameters()); + @GuardedBy("mPictureProfileLock") + @Override + public void createPictureProfile(PictureProfile pp, int userId) { + mHandler.post( + () -> { + if ((pp.getPackageName() != null && !pp.getPackageName().isEmpty() + && !incomingPackageEqualsCallingUidPackage(pp.getPackageName())) + && !hasGlobalPictureQualityServicePermission()) { + mMqManagerNotifier.notifyOnPictureProfileError(null, + PictureProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } - // id is auto-generated by SQLite upon successful insertion of row - Long id = db.insert(mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, - null, values); - MediaQualityUtils.populateTempIdMap(mPictureProfileTempIdMap, id); - String value = mPictureProfileTempIdMap.getValue(id); - pp.setProfileId(value); - mMqManagerNotifier.notifyOnPictureProfileAdded(value, pp, Binder.getCallingUid(), - Binder.getCallingPid()); - return pp; - } + synchronized (mPictureProfileLock) { + SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); + + ContentValues values = MediaQualityUtils.getContentValues(null, + pp.getProfileType(), + pp.getName(), + pp.getPackageName() == null || pp.getPackageName().isEmpty() + ? getPackageOfCallingUid() : pp.getPackageName(), + pp.getInputId(), + pp.getParameters()); + + // id is auto-generated by SQLite upon successful insertion of row + Long id = db.insert(mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, + null, values); + MediaQualityUtils.populateTempIdMap(mPictureProfileTempIdMap, id); + String value = mPictureProfileTempIdMap.getValue(id); + pp.setProfileId(value); + mMqManagerNotifier.notifyOnPictureProfileAdded(value, pp, + Binder.getCallingUid(), Binder.getCallingPid()); + if (isPackageDefaultPictureProfile(pp)) { + mPackageDefaultPictureProfileHandleMap.put( + pp.getPackageName(), pp.getHandle().getId()); + } + } + } + ); } @GuardedBy("mPictureProfileLock") @Override public void updatePictureProfile(String id, PictureProfile pp, int userId) { - Long dbId = mPictureProfileTempIdMap.getKey(id); - if (!hasPermissionToUpdatePictureProfile(dbId, pp)) { - mMqManagerNotifier.notifyOnPictureProfileError(id, - PictureProfile.ERROR_NO_PERMISSION, - Binder.getCallingUid(), Binder.getCallingPid()); - } - synchronized (mPictureProfileLock) { - ContentValues values = MediaQualityUtils.getContentValues(dbId, - pp.getProfileType(), - pp.getName(), - pp.getPackageName(), - pp.getInputId(), - pp.getParameters()); - updateDatabaseOnPictureProfileAndNotifyManagerAndHal(values, pp.getParameters()); - } + mHandler.post(() -> { + Long dbId = mPictureProfileTempIdMap.getKey(id); + if (!hasPermissionToUpdatePictureProfile(dbId, pp)) { + mMqManagerNotifier.notifyOnPictureProfileError(id, + PictureProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } + synchronized (mPictureProfileLock) { + ContentValues values = MediaQualityUtils.getContentValues(dbId, + pp.getProfileType(), + pp.getName(), + pp.getPackageName(), + pp.getInputId(), + pp.getParameters()); + updateDatabaseOnPictureProfileAndNotifyManagerAndHal(values, + pp.getParameters()); + if (isPackageDefaultPictureProfile(pp)) { + mPackageDefaultPictureProfileHandleMap.put( + pp.getPackageName(), pp.getHandle().getId()); + } + } + }); } private boolean hasPermissionToUpdatePictureProfile(Long dbId, PictureProfile toUpdate) { @@ -251,35 +352,42 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override public void removePictureProfile(String id, int userId) { - synchronized (mPictureProfileLock) { - Long dbId = mPictureProfileTempIdMap.getKey(id); + mHandler.post(() -> { + synchronized (mPictureProfileLock) { + Long dbId = mPictureProfileTempIdMap.getKey(id); - PictureProfile toDelete = mMqDatabaseUtils.getPictureProfile(dbId); - if (!hasPermissionToRemovePictureProfile(toDelete)) { - mMqManagerNotifier.notifyOnPictureProfileError(id, - PictureProfile.ERROR_NO_PERMISSION, - Binder.getCallingUid(), Binder.getCallingPid()); - } - - if (dbId != null) { - SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); - String selection = BaseParameters.PARAMETER_ID + " = ?"; - String[] selectionArgs = {Long.toString(dbId)}; - int result = db.delete(mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, - selection, selectionArgs); - if (result == 0) { + PictureProfile toDelete = mMqDatabaseUtils.getPictureProfile(dbId); + if (!hasPermissionToRemovePictureProfile(toDelete)) { mMqManagerNotifier.notifyOnPictureProfileError(id, - PictureProfile.ERROR_INVALID_ARGUMENT, + PictureProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); - } else { - mMqManagerNotifier.notifyOnPictureProfileRemoved( - mPictureProfileTempIdMap.getValue(dbId), toDelete, - Binder.getCallingUid(), Binder.getCallingPid()); - mPictureProfileTempIdMap.remove(dbId); - mHalNotifier.notifyHalOnPictureProfileChange(dbId, null); + } + + if (dbId != null) { + SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); + String selection = BaseParameters.PARAMETER_ID + " = ?"; + String[] selectionArgs = {Long.toString(dbId)}; + int result = db.delete(mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, + selection, selectionArgs); + if (result == 0) { + mMqManagerNotifier.notifyOnPictureProfileError(id, + PictureProfile.ERROR_INVALID_ARGUMENT, + Binder.getCallingUid(), Binder.getCallingPid()); + } else { + mMqManagerNotifier.notifyOnPictureProfileRemoved( + mPictureProfileTempIdMap.getValue(dbId), toDelete, + Binder.getCallingUid(), Binder.getCallingPid()); + mPictureProfileTempIdMap.remove(dbId); + mHalNotifier.notifyHalOnPictureProfileChange(dbId, null); + } + } + + if (isPackageDefaultPictureProfile(toDelete)) { + mPackageDefaultPictureProfileHandleMap.remove( + toDelete.getPackageName()); } } - } + }); } private boolean hasPermissionToRemovePictureProfile(PictureProfile toDelete) { @@ -361,13 +469,19 @@ public class MediaQualityService extends SystemService { Binder.getCallingUid(), Binder.getCallingPid()); } - PictureProfile pictureProfile = mMqDatabaseUtils.getPictureProfile( - mPictureProfileTempIdMap.getKey(profileId)); + Long longId = mPictureProfileTempIdMap.getKey(profileId); + if (longId == null) { + return false; + } + PictureProfile pictureProfile = mMqDatabaseUtils.getPictureProfile(longId); PersistableBundle params = pictureProfile.getParameters(); try { if (mMediaQuality != null) { PictureParameters pp = new PictureParameters(); + // put ID in params for profile update in HAL + // TODO: update HAL API for this case + params.putLong(BaseParameters.PARAMETER_ID, longId); PictureParameter[] pictureParameters = MediaQualityUtils .convertPersistableBundleToPictureParameterList(params); @@ -422,6 +536,72 @@ public class MediaQualityService extends SystemService { return toReturn; } + @GuardedBy("mPictureProfileLock") + @Override + public long getPictureProfileHandleValue(String id, int userId) { + synchronized (mPictureProfileLock) { + Long value = mPictureProfileTempIdMap.getKey(id); + return value != null ? value : -1; + } + } + + @GuardedBy("mPictureProfileLock") + @Override + public long getDefaultPictureProfileHandleValue(int userId) { + synchronized (mPictureProfileLock) { + String packageName = getPackageOfCallingUid(); + Long value = null; + if (packageName != null) { + value = mPackageDefaultPictureProfileHandleMap.get(packageName); + } + return value != null ? value : -1; + } + } + + @GuardedBy("mPictureProfileLock") + @Override + public void notifyPictureProfileHandleSelection(long handle, int userId) { + PictureProfile profile = mMqDatabaseUtils.getPictureProfile(handle); + if (profile != null) { + mHalNotifier.notifyHalOnPictureProfileChange(handle, profile.getParameters()); + } + } + + public long getPictureProfileForTvInput(String inputId, int userId) { + // TODO: cache profiles + if (!hasGlobalPictureQualityServicePermission()) { + mMqManagerNotifier.notifyOnPictureProfileError(null, + PictureProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } + String[] columns = {BaseParameters.PARAMETER_ID}; + String selection = BaseParameters.PARAMETER_TYPE + " = ? AND " + + BaseParameters.PARAMETER_NAME + " = ? AND " + + BaseParameters.PARAMETER_INPUT_ID + " = ?"; + String[] selectionArguments = { + Integer.toString(PictureProfile.TYPE_SYSTEM), + PictureProfile.NAME_DEFAULT, + inputId + }; + synchronized (mPictureProfileLock) { + try (Cursor cursor = mMqDatabaseUtils.getCursorAfterQuerying( + mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, + columns, selection, selectionArguments)) { + int count = cursor.getCount(); + if (count == 0) { + return -1; + } + long handle = -1; + cursor.moveToFirst(); + int colIndex = cursor.getColumnIndex(BaseParameters.PARAMETER_ID); + if (colIndex != -1) { + handle = cursor.getLong(colIndex); + } + return handle; + } + } + } + @GuardedBy("mSoundProfileLock") @Override public List<SoundProfileHandle> getSoundProfileHandle(String[] ids, int userId) { @@ -441,56 +621,60 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public SoundProfile createSoundProfile(SoundProfile sp, int userId) { - if ((sp.getPackageName() != null && !sp.getPackageName().isEmpty() - && !incomingPackageEqualsCallingUidPackage(sp.getPackageName())) - && !hasGlobalPictureQualityServicePermission()) { - mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, - Binder.getCallingUid(), Binder.getCallingPid()); - } - - synchronized (mSoundProfileLock) { - SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); + public void createSoundProfile(SoundProfile sp, int userId) { + mHandler.post(() -> { + if ((sp.getPackageName() != null && !sp.getPackageName().isEmpty() + && !incomingPackageEqualsCallingUidPackage(sp.getPackageName())) + && !hasGlobalSoundQualityServicePermission()) { + mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } - ContentValues values = MediaQualityUtils.getContentValues(null, - sp.getProfileType(), - sp.getName(), - sp.getPackageName() == null || sp.getPackageName().isEmpty() - ? getPackageOfCallingUid() : sp.getPackageName(), - sp.getInputId(), - sp.getParameters()); + synchronized (mSoundProfileLock) { + SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); - // id is auto-generated by SQLite upon successful insertion of row - Long id = db.insert(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, - null, values); - MediaQualityUtils.populateTempIdMap(mSoundProfileTempIdMap, id); - String value = mSoundProfileTempIdMap.getValue(id); - sp.setProfileId(value); - mMqManagerNotifier.notifyOnSoundProfileAdded(value, sp, Binder.getCallingUid(), - Binder.getCallingPid()); - return sp; - } + ContentValues values = MediaQualityUtils.getContentValues(null, + sp.getProfileType(), + sp.getName(), + sp.getPackageName() == null || sp.getPackageName().isEmpty() + ? getPackageOfCallingUid() : sp.getPackageName(), + sp.getInputId(), + sp.getParameters()); + + // id is auto-generated by SQLite upon successful insertion of row + Long id = db.insert(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, + null, values); + MediaQualityUtils.populateTempIdMap(mSoundProfileTempIdMap, id); + String value = mSoundProfileTempIdMap.getValue(id); + sp.setProfileId(value); + mMqManagerNotifier.notifyOnSoundProfileAdded(value, sp, Binder.getCallingUid(), + Binder.getCallingPid()); + } + }); } @GuardedBy("mSoundProfileLock") @Override public void updateSoundProfile(String id, SoundProfile sp, int userId) { - Long dbId = mSoundProfileTempIdMap.getKey(id); - if (!hasPermissionToUpdateSoundProfile(dbId, sp)) { - mMqManagerNotifier.notifyOnSoundProfileError(id, SoundProfile.ERROR_NO_PERMISSION, - Binder.getCallingUid(), Binder.getCallingPid()); - } + mHandler.post(() -> { + Long dbId = mSoundProfileTempIdMap.getKey(id); + if (!hasPermissionToUpdateSoundProfile(dbId, sp)) { + mMqManagerNotifier.notifyOnSoundProfileError(id, + SoundProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } - synchronized (mSoundProfileLock) { - ContentValues values = MediaQualityUtils.getContentValues(dbId, - sp.getProfileType(), - sp.getName(), - sp.getPackageName(), - sp.getInputId(), - sp.getParameters()); + synchronized (mSoundProfileLock) { + ContentValues values = MediaQualityUtils.getContentValues(dbId, + sp.getProfileType(), + sp.getName(), + sp.getPackageName(), + sp.getInputId(), + sp.getParameters()); - updateDatabaseOnSoundProfileAndNotifyManagerAndHal(values, sp.getParameters()); - } + updateDatabaseOnSoundProfileAndNotifyManagerAndHal(values, sp.getParameters()); + } + }); } private boolean hasPermissionToUpdateSoundProfile(Long dbId, SoundProfile sp) { @@ -504,34 +688,36 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override public void removeSoundProfile(String id, int userId) { - synchronized (mSoundProfileLock) { - Long dbId = mSoundProfileTempIdMap.getKey(id); - SoundProfile toDelete = mMqDatabaseUtils.getSoundProfile(dbId); - if (!hasPermissionToRemoveSoundProfile(toDelete)) { - mMqManagerNotifier.notifyOnSoundProfileError(id, - SoundProfile.ERROR_NO_PERMISSION, - Binder.getCallingUid(), Binder.getCallingPid()); - } - if (dbId != null) { - SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); - String selection = BaseParameters.PARAMETER_ID + " = ?"; - String[] selectionArgs = {Long.toString(dbId)}; - int result = db.delete(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, - selection, - selectionArgs); - if (result == 0) { + mHandler.post(() -> { + synchronized (mSoundProfileLock) { + Long dbId = mSoundProfileTempIdMap.getKey(id); + SoundProfile toDelete = mMqDatabaseUtils.getSoundProfile(dbId); + if (!hasPermissionToRemoveSoundProfile(toDelete)) { mMqManagerNotifier.notifyOnSoundProfileError(id, - SoundProfile.ERROR_INVALID_ARGUMENT, + SoundProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); - } else { - mMqManagerNotifier.notifyOnSoundProfileRemoved( - mSoundProfileTempIdMap.getValue(dbId), toDelete, - Binder.getCallingUid(), Binder.getCallingPid()); - mSoundProfileTempIdMap.remove(dbId); - mHalNotifier.notifyHalOnSoundProfileChange(dbId, null); + } + if (dbId != null) { + SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); + String selection = BaseParameters.PARAMETER_ID + " = ?"; + String[] selectionArgs = {Long.toString(dbId)}; + int result = db.delete(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, + selection, + selectionArgs); + if (result == 0) { + mMqManagerNotifier.notifyOnSoundProfileError(id, + SoundProfile.ERROR_INVALID_ARGUMENT, + Binder.getCallingUid(), Binder.getCallingPid()); + } else { + mMqManagerNotifier.notifyOnSoundProfileRemoved( + mSoundProfileTempIdMap.getValue(dbId), toDelete, + Binder.getCallingUid(), Binder.getCallingPid()); + mSoundProfileTempIdMap.remove(dbId); + mHalNotifier.notifyHalOnSoundProfileChange(dbId, null); + } } } - } + }); } private boolean hasPermissionToRemoveSoundProfile(SoundProfile toDelete) { @@ -612,16 +798,25 @@ public class MediaQualityService extends SystemService { Binder.getCallingUid(), Binder.getCallingPid()); } - SoundProfile soundProfile = - mMqDatabaseUtils.getSoundProfile(mSoundProfileTempIdMap.getKey(profileId)); + Long longId = mSoundProfileTempIdMap.getKey(profileId); + if (longId == null) { + return false; + } + + SoundProfile soundProfile = mMqDatabaseUtils.getSoundProfile(longId); PersistableBundle params = soundProfile.getParameters(); try { if (mMediaQuality != null) { + SoundParameters sp = new SoundParameters(); + // put ID in params for profile update in HAL + // TODO: update HAL API for this case + params.putLong(BaseParameters.PARAMETER_ID, longId); SoundParameter[] soundParameters = MediaQualityUtils.convertPersistableBundleToSoundParameterList(params); - SoundParameters sp = new SoundParameters(); + Parcel parcel = Parcel.obtain(); + setVendorSoundParameters(sp, parcel, params); sp.soundParameters = soundParameters; mMediaQuality.sendDefaultSoundParameters(sp); @@ -667,21 +862,21 @@ public class MediaQualityService extends SystemService { } private boolean hasGlobalPictureQualityServicePermission() { - return mPackageManager.checkPermission(android.Manifest.permission - .MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE, - mContext.getPackageName()) == mPackageManager.PERMISSION_GRANTED; + return mContext.checkCallingPermission( + android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE) + == PackageManager.PERMISSION_GRANTED; } private boolean hasGlobalSoundQualityServicePermission() { - return mPackageManager.checkPermission(android.Manifest.permission - .MANAGE_GLOBAL_SOUND_QUALITY_SERVICE, - mContext.getPackageName()) == mPackageManager.PERMISSION_GRANTED; + return mContext.checkCallingPermission( + android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE) + == PackageManager.PERMISSION_GRANTED; } private boolean hasReadColorZonesPermission() { - return mPackageManager.checkPermission(android.Manifest.permission - .READ_COLOR_ZONES, - mContext.getPackageName()) == mPackageManager.PERMISSION_GRANTED; + return mContext.checkCallingPermission( + android.Manifest.permission.READ_COLOR_ZONES) + == PackageManager.PERMISSION_GRANTED; } @Override @@ -705,6 +900,18 @@ public class MediaQualityService extends SystemService { } @Override + public void registerActiveProcessingPictureListener( + final IActiveProcessingPictureListener l) { + int callingPid = Binder.getCallingPid(); + int callingUid = Binder.getCallingUid(); + + UserState userState = getOrCreateUserState(Binder.getCallingUid()); + String packageName = getPackageOfCallingUid(); + userState.mActiveProcessingPictureListenerMap.put(l, + new ActiveProcessingPictureListenerInfo(callingUid, callingPid, packageName)); + } + + @Override public void registerAmbientBacklightCallback(IAmbientBacklightCallback callback) { if (DEBUG) { Slogf.d(TAG, "registerAmbientBacklightCallback"); @@ -731,6 +938,26 @@ public class MediaQualityService extends SystemService { } } + public void unregisterAmbientBacklightCallback(IAmbientBacklightCallback callback) { + if (DEBUG) { + Slogf.d(TAG, "unregisterAmbientBacklightCallback"); + } + + if (!hasReadColorZonesPermission()) { + //TODO: error handling + } + + synchronized (mCallbackRecords) { + for (AmbientBacklightCallbackRecord record : mCallbackRecords.values()) { + if (record.mCallback.asBinder().equals(callback.asBinder())) { + record.release(); + mCallbackRecords.remove(record.mPackageName); + return; + } + } + } + } + @GuardedBy("mAmbientBacklightLock") @Override public void setAmbientBacklightSettings( @@ -842,14 +1069,16 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override public void setPictureProfileAllowList(List<String> packages, int userId) { - if (!hasGlobalPictureQualityServicePermission()) { - mMqManagerNotifier.notifyOnPictureProfileError(null, - PictureProfile.ERROR_NO_PERMISSION, - Binder.getCallingUid(), Binder.getCallingPid()); - } - SharedPreferences.Editor editor = mPictureProfileSharedPreference.edit(); - editor.putString(ALLOWLIST, String.join(COMMA_DELIMITER, packages)); - editor.commit(); + mHandler.post(() -> { + if (!hasGlobalPictureQualityServicePermission()) { + mMqManagerNotifier.notifyOnPictureProfileError(null, + PictureProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } + SharedPreferences.Editor editor = mPictureProfileSharedPreference.edit(); + editor.putString(ALLOWLIST, String.join(COMMA_DELIMITER, packages)); + editor.commit(); + }); } @GuardedBy("mSoundProfileLock") @@ -870,13 +1099,16 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override public void setSoundProfileAllowList(List<String> packages, int userId) { - if (!hasGlobalSoundQualityServicePermission()) { - mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, - Binder.getCallingUid(), Binder.getCallingPid()); - } - SharedPreferences.Editor editor = mSoundProfileSharedPreference.edit(); - editor.putString(ALLOWLIST, String.join(COMMA_DELIMITER, packages)); - editor.commit(); + mHandler.post(() -> { + if (!hasGlobalSoundQualityServicePermission()) { + mMqManagerNotifier.notifyOnSoundProfileError(null, + SoundProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } + SharedPreferences.Editor editor = mSoundProfileSharedPreference.edit(); + editor.putString(ALLOWLIST, String.join(COMMA_DELIMITER, packages)); + editor.commit(); + }); } @Override @@ -887,22 +1119,24 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override public void setAutoPictureQualityEnabled(boolean enabled, int userId) { - if (!hasGlobalPictureQualityServicePermission()) { - mMqManagerNotifier.notifyOnPictureProfileError(null, - PictureProfile.ERROR_NO_PERMISSION, - Binder.getCallingUid(), Binder.getCallingPid()); - } - synchronized (mPictureProfileLock) { - try { - if (mMediaQuality != null) { - if (mMediaQuality.isAutoPqSupported()) { - mMediaQuality.setAutoPqEnabled(enabled); + mHandler.post(() -> { + if (!hasGlobalPictureQualityServicePermission()) { + mMqManagerNotifier.notifyOnPictureProfileError(null, + PictureProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } + synchronized (mPictureProfileLock) { + try { + if (mMediaQuality != null) { + if (mMediaQuality.isAutoPqSupported()) { + mMediaQuality.setAutoPqEnabled(enabled); + } } + } catch (RemoteException e) { + Slog.e(TAG, "Failed to set auto picture quality", e); } - } catch (RemoteException e) { - Slog.e(TAG, "Failed to set auto picture quality", e); } - } + }); } @GuardedBy("mPictureProfileLock") @@ -925,22 +1159,24 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override public void setSuperResolutionEnabled(boolean enabled, int userId) { - if (!hasGlobalPictureQualityServicePermission()) { - mMqManagerNotifier.notifyOnPictureProfileError(null, - PictureProfile.ERROR_NO_PERMISSION, - Binder.getCallingUid(), Binder.getCallingPid()); - } - synchronized (mPictureProfileLock) { - try { - if (mMediaQuality != null) { - if (mMediaQuality.isAutoSrSupported()) { - mMediaQuality.setAutoSrEnabled(enabled); + mHandler.post(() -> { + if (!hasGlobalPictureQualityServicePermission()) { + mMqManagerNotifier.notifyOnPictureProfileError(null, + PictureProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } + synchronized (mPictureProfileLock) { + try { + if (mMediaQuality != null) { + if (mMediaQuality.isAutoSrSupported()) { + mMediaQuality.setAutoSrEnabled(enabled); + } } + } catch (RemoteException e) { + Slog.e(TAG, "Failed to set super resolution", e); } - } catch (RemoteException e) { - Slog.e(TAG, "Failed to set super resolution", e); } - } + }); } @GuardedBy("mPictureProfileLock") @@ -963,22 +1199,25 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override public void setAutoSoundQualityEnabled(boolean enabled, int userId) { - if (!hasGlobalSoundQualityServicePermission()) { - mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, - Binder.getCallingUid(), Binder.getCallingPid()); - } + mHandler.post(() -> { + if (!hasGlobalSoundQualityServicePermission()) { + mMqManagerNotifier.notifyOnSoundProfileError(null, + SoundProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } - synchronized (mSoundProfileLock) { - try { - if (mMediaQuality != null) { - if (mMediaQuality.isAutoAqSupported()) { - mMediaQuality.setAutoAqEnabled(enabled); + synchronized (mSoundProfileLock) { + try { + if (mMediaQuality != null) { + if (mMediaQuality.isAutoAqSupported()) { + mMediaQuality.setAutoAqEnabled(enabled); + } } + } catch (RemoteException e) { + Slog.e(TAG, "Failed to set auto sound quality", e); } - } catch (RemoteException e) { - Slog.e(TAG, "Failed to set auto sound quality", e); } - } + }); } @GuardedBy("mSoundProfileLock") @@ -1079,6 +1318,20 @@ public class MediaQualityService extends SystemService { } } + private class ActiveProcessingPictureCallbackList extends + RemoteCallbackList<IActiveProcessingPictureListener> { + @Override + public void onCallbackDied(IActiveProcessingPictureListener l) { + synchronized (mPictureProfileLock) { + for (int i = 0; i < mUserStates.size(); i++) { + int userId = mUserStates.keyAt(i); + UserState userState = getOrCreateUserState(userId); + userState.mActiveProcessingPictureListenerMap.remove(l); + } + } + } + } + private final class UserState { // A list of callbacks. private final MediaQualityManagerPictureProfileCallbackList mPictureProfileCallbacks = @@ -1087,18 +1340,35 @@ public class MediaQualityService extends SystemService { private final MediaQualityManagerSoundProfileCallbackList mSoundProfileCallbacks = new MediaQualityManagerSoundProfileCallbackList(); + private final ActiveProcessingPictureCallbackList mActiveProcessingPictureCallbackList = + new ActiveProcessingPictureCallbackList(); + private final Map<IPictureProfileCallback, Pair<Integer, Integer>> mPictureProfileCallbackPidUidMap = new HashMap<>(); private final Map<ISoundProfileCallback, Pair<Integer, Integer>> mSoundProfileCallbackPidUidMap = new HashMap<>(); + private final Map<IActiveProcessingPictureListener, ActiveProcessingPictureListenerInfo> + mActiveProcessingPictureListenerMap = new HashMap<>(); + private UserState(Context context, int userId) { } } - @GuardedBy("mUserStateLock") + private final class ActiveProcessingPictureListenerInfo { + private int mUid; + private int mPid; + private String mPackageName; + + ActiveProcessingPictureListenerInfo(int uid, int pid, String packageName) { + mUid = uid; + mPid = pid; + mPackageName = packageName; + } + } + private UserState getOrCreateUserState(int userId) { UserState userState = getUserState(userId); if (userState == null) { @@ -1120,15 +1390,17 @@ public class MediaQualityService extends SystemService { private final class MqDatabaseUtils { private PictureProfile getPictureProfile(Long dbId) { + return getPictureProfile(dbId, false); + } + + private PictureProfile getPictureProfile(Long dbId, boolean includeParams) { String selection = BaseParameters.PARAMETER_ID + " = ?"; String[] selectionArguments = {Long.toString(dbId)}; - try ( - Cursor cursor = getCursorAfterQuerying( - mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, - MediaQualityUtils.getMediaProfileColumns(false), selection, - selectionArguments) - ) { + try (Cursor cursor = getCursorAfterQuerying( + mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, + MediaQualityUtils.getMediaProfileColumns(includeParams), selection, + selectionArguments)) { int count = cursor.getCount(); if (count == 0) { return null; @@ -1147,11 +1419,9 @@ public class MediaQualityService extends SystemService { private List<PictureProfile> getPictureProfilesBasedOnConditions(String[] columns, String selection, String[] selectionArguments) { - try ( - Cursor cursor = getCursorAfterQuerying( - mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, columns, selection, - selectionArguments) - ) { + try (Cursor cursor = getCursorAfterQuerying( + mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, columns, selection, + selectionArguments)) { List<PictureProfile> pictureProfiles = new ArrayList<>(); while (cursor.moveToNext()) { pictureProfiles.add(MediaQualityUtils.convertCursorToPictureProfileWithTempId( @@ -1165,12 +1435,10 @@ public class MediaQualityService extends SystemService { String selection = BaseParameters.PARAMETER_ID + " = ?"; String[] selectionArguments = {Long.toString(dbId)}; - try ( - Cursor cursor = mMqDatabaseUtils.getCursorAfterQuerying( - mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, - MediaQualityUtils.getMediaProfileColumns(false), selection, - selectionArguments) - ) { + try (Cursor cursor = mMqDatabaseUtils.getCursorAfterQuerying( + mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, + MediaQualityUtils.getMediaProfileColumns(false), selection, + selectionArguments)) { int count = cursor.getCount(); if (count == 0) { return null; @@ -1189,11 +1457,9 @@ public class MediaQualityService extends SystemService { private List<SoundProfile> getSoundProfilesBasedOnConditions(String[] columns, String selection, String[] selectionArguments) { - try ( - Cursor cursor = mMqDatabaseUtils.getCursorAfterQuerying( - mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, columns, selection, - selectionArguments) - ) { + try (Cursor cursor = mMqDatabaseUtils.getCursorAfterQuerying( + mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, columns, selection, + selectionArguments)) { List<SoundProfile> soundProfiles = new ArrayList<>(); while (cursor.moveToNext()) { soundProfiles.add(MediaQualityUtils.convertCursorToSoundProfileWithTempId( @@ -1409,8 +1675,19 @@ public class MediaQualityService extends SystemService { private void notifyHalOnPictureProfileChange(Long dbId, PersistableBundle params) { // TODO: only notify HAL when the profile is active / being used if (mPpChangedListener != null) { + Long currentHandle = mCurrentPictureHandleToOriginal.getKey(dbId); + if (currentHandle != null) { + // this handle maps to another current profile, skip + return; + } try { - mPpChangedListener.onPictureProfileChanged(convertToHalPictureProfile(dbId, + Long idForHal = dbId; + Long originalHandle = mCurrentPictureHandleToOriginal.getValue(dbId); + if (originalHandle != null) { + // the original id is used in HAL because of status change + idForHal = originalHandle; + } + mPpChangedListener.onPictureProfileChanged(convertToHalPictureProfile(idForHal, params)); } catch (RemoteException e) { Slog.e(TAG, "Failed to notify HAL on picture profile change.", e); @@ -1457,6 +1734,11 @@ public class MediaQualityService extends SystemService { soundParameters.soundParameters = MediaQualityUtils.convertPersistableBundleToSoundParameterList(params); + Parcel parcel = Parcel.obtain(); + if (params != null) { + setVendorSoundParameters(soundParameters, parcel, params); + } + android.hardware.tv.mediaquality.SoundProfile toReturn = new android.hardware.tv.mediaquality.SoundProfile(); toReturn.soundProfileId = id; @@ -1539,9 +1821,116 @@ public class MediaQualityService extends SystemService { } @Override - public void onStreamStatusChanged(long pictureProfileId, byte status) + public void onStreamStatusChanged(long profileHandle, byte status) throws RemoteException { - // TODO + mHandler.post(() -> { + synchronized (mPictureProfileLock) { + // get from map if exists + PictureProfile previous = mHandleToPictureProfile.get(profileHandle); + if (previous == null) { + // get from DB if not exists + previous = mMqDatabaseUtils.getPictureProfile(profileHandle); + if (previous == null) { + return; + } + } + String[] arr = splitNameAndStatus(previous.getName()); + String profileName = arr[0]; + String profileStatus = arr[1]; + if (status == StreamStatus.HDR10) { + if (isHdr(profileStatus)) { + // already HDR + return; + } + if (isSdr(profileStatus)) { + // SDR to HDR + String selection = BaseParameters.PARAMETER_TYPE + " = ? AND " + + BaseParameters.PARAMETER_PACKAGE + " = ? AND " + + BaseParameters.PARAMETER_NAME + " = ?"; + String[] selectionArguments = { + Integer.toString(previous.getProfileType()), + previous.getPackageName(), + profileName + "/" + PictureProfile.STATUS_HDR + }; + List<PictureProfile> list = + mMqDatabaseUtils.getPictureProfilesBasedOnConditions( + MediaQualityUtils.getMediaProfileColumns(true), + selection, + selectionArguments); + if (list.isEmpty()) { + // HDR profile not found + return; + } + PictureProfile current = list.get(0); + mHandleToPictureProfile.put(profileHandle, current); + mCurrentPictureHandleToOriginal.put( + current.getHandle().getId(), profileHandle); + + mHalNotifier.notifyHalOnPictureProfileChange(profileHandle, + current.getParameters()); + + } + } else if (status == StreamStatus.SDR) { + if (isSdr(profileStatus)) { + // already SDR + return; + } + if (isHdr(profileStatus)) { + // HDR to SDR + String selection = BaseParameters.PARAMETER_TYPE + " = ? AND " + + BaseParameters.PARAMETER_PACKAGE + " = ? AND (" + + BaseParameters.PARAMETER_NAME + " = ? OR " + + BaseParameters.PARAMETER_NAME + " = ?)"; + String[] selectionArguments = { + Integer.toString(previous.getProfileType()), + previous.getPackageName(), + profileName, + profileName + "/" + PictureProfile.STATUS_SDR + }; + List<PictureProfile> list = + mMqDatabaseUtils.getPictureProfilesBasedOnConditions( + MediaQualityUtils.getMediaProfileColumns(true), + selection, + selectionArguments); + if (list.isEmpty()) { + // SDR profile not found + return; + } + PictureProfile current = list.get(0); + mHandleToPictureProfile.put(profileHandle, current); + mCurrentPictureHandleToOriginal.put( + current.getHandle().getId(), profileHandle); + + mHalNotifier.notifyHalOnPictureProfileChange(profileHandle, + current.getParameters()); + } + } + } + }); + + } + + @NonNull + private String[] splitNameAndStatus(@NonNull String nameAndStatus) { + int index = nameAndStatus.lastIndexOf('/'); + if (index == -1 || index == nameAndStatus.length() - 1) { + // no status in the original name + return new String[] {nameAndStatus, ""}; + } + return new String[] { + nameAndStatus.substring(0, index), + nameAndStatus.substring(index + 1) + }; + + } + + private boolean isSdr(@NonNull String profileStatus) { + return profileStatus.equals(PictureProfile.STATUS_SDR) + || profileStatus.isEmpty(); + } + + private boolean isHdr(@NonNull String profileStatus) { + return profileStatus.equals(PictureProfile.STATUS_HDR); } @Override @@ -1753,4 +2142,21 @@ public class MediaQualityService extends SystemService { vendorBundleToByteArray, vendorBundleToByteArray.length); pictureParameters.vendorPictureParameters.setParcelable(defaultExtension); } + + private void setVendorSoundParameters( + SoundParameters soundParameters, + Parcel parcel, + PersistableBundle vendorSoundParameters) { + vendorSoundParameters.writeToParcel(parcel, 0); + byte[] vendorBundleToByteArray = parcel.marshall(); + DefaultExtension defaultExtension = new DefaultExtension(); + defaultExtension.bytes = Arrays.copyOf( + vendorBundleToByteArray, vendorBundleToByteArray.length); + soundParameters.vendorSoundParameters.setParcelable(defaultExtension); + } + + private boolean isPackageDefaultPictureProfile(PictureProfile pp) { + return pp != null && pp.getProfileType() == PictureProfile.TYPE_SYSTEM && + pp.getName().equals(PictureProfile.NAME_DEFAULT); + } } diff --git a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java index 05aac5587c2c..f58bc982373b 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java @@ -1073,80 +1073,127 @@ public final class MediaQualityUtils { if (params.containsKey(SoundQuality.PARAMETER_BALANCE)) { soundParams.add(SoundParameter.balance(params.getInt( SoundQuality.PARAMETER_BALANCE))); + params.remove(SoundQuality.PARAMETER_BALANCE); } if (params.containsKey(SoundQuality.PARAMETER_BASS)) { soundParams.add(SoundParameter.bass(params.getInt(SoundQuality.PARAMETER_BASS))); + params.remove(SoundQuality.PARAMETER_BASS); } if (params.containsKey(SoundQuality.PARAMETER_TREBLE)) { soundParams.add(SoundParameter.treble(params.getInt( SoundQuality.PARAMETER_TREBLE))); + params.remove(SoundQuality.PARAMETER_TREBLE); } if (params.containsKey(SoundQuality.PARAMETER_SURROUND_SOUND)) { soundParams.add(SoundParameter.surroundSoundEnabled(params.getBoolean( SoundQuality.PARAMETER_SURROUND_SOUND))); + params.remove(SoundQuality.PARAMETER_SURROUND_SOUND); } if (params.containsKey(SoundQuality.PARAMETER_SPEAKERS)) { soundParams.add(SoundParameter.speakersEnabled(params.getBoolean( SoundQuality.PARAMETER_SPEAKERS))); + params.remove(SoundQuality.PARAMETER_SPEAKERS); } if (params.containsKey(SoundQuality.PARAMETER_SPEAKERS_DELAY_MILLIS)) { soundParams.add(SoundParameter.speakersDelayMs(params.getInt( SoundQuality.PARAMETER_SPEAKERS_DELAY_MILLIS))); + params.remove(SoundQuality.PARAMETER_SPEAKERS_DELAY_MILLIS); } if (params.containsKey(SoundQuality.PARAMETER_AUTO_VOLUME_CONTROL)) { soundParams.add(SoundParameter.autoVolumeControl(params.getBoolean( SoundQuality.PARAMETER_AUTO_VOLUME_CONTROL))); + params.remove(SoundQuality.PARAMETER_AUTO_VOLUME_CONTROL); } if (params.containsKey(SoundQuality.PARAMETER_DTS_DRC)) { soundParams.add(SoundParameter.dtsDrc(params.getBoolean( SoundQuality.PARAMETER_DTS_DRC))); + params.remove(SoundQuality.PARAMETER_DTS_DRC); } if (params.containsKey(SoundQuality.PARAMETER_DIGITAL_OUTPUT_DELAY_MILLIS)) { soundParams.add(SoundParameter.surroundSoundEnabled(params.getBoolean( SoundQuality.PARAMETER_DIGITAL_OUTPUT_DELAY_MILLIS))); + params.remove(SoundQuality.PARAMETER_DIGITAL_OUTPUT_DELAY_MILLIS); } if (params.containsKey(SoundQuality.PARAMETER_EARC)) { soundParams.add(SoundParameter.enhancedAudioReturnChannelEnabled(params.getBoolean( SoundQuality.PARAMETER_EARC))); + params.remove(SoundQuality.PARAMETER_EARC); } if (params.containsKey(SoundQuality.PARAMETER_DOWN_MIX_MODE)) { soundParams.add(SoundParameter.downmixMode((byte) params.getInt( SoundQuality.PARAMETER_DOWN_MIX_MODE))); + params.remove(SoundQuality.PARAMETER_DOWN_MIX_MODE); } if (params.containsKey(SoundQuality.PARAMETER_SOUND_STYLE)) { soundParams.add(SoundParameter.soundStyle((byte) params.getInt( SoundQuality.PARAMETER_SOUND_STYLE))); + params.remove(SoundQuality.PARAMETER_SOUND_STYLE); } if (params.containsKey(SoundQuality.PARAMETER_DIGITAL_OUTPUT_MODE)) { soundParams.add(SoundParameter.digitalOutput((byte) params.getInt( SoundQuality.PARAMETER_DIGITAL_OUTPUT_MODE))); + params.remove(SoundQuality.PARAMETER_DIGITAL_OUTPUT_MODE); } if (params.containsKey(SoundQuality.PARAMETER_DIALOGUE_ENHANCER)) { soundParams.add(SoundParameter.dolbyDialogueEnhancer((byte) params.getInt( SoundQuality.PARAMETER_DIALOGUE_ENHANCER))); + params.remove(SoundQuality.PARAMETER_DIALOGUE_ENHANCER); } DolbyAudioProcessing dab = new DolbyAudioProcessing(); - dab.soundMode = - (byte) params.getInt(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SOUND_MODE); - dab.volumeLeveler = - params.getBoolean(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_VOLUME_LEVELER); - dab.surroundVirtualizer = params.getBoolean( - SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SURROUND_VIRTUALIZER); - dab.dolbyAtmos = - params.getBoolean(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_DOLBY_ATMOS); + if (params.containsKey(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SOUND_MODE)) { + dab.soundMode = + (byte) params.getInt(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SOUND_MODE); + params.remove(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SOUND_MODE); + } + if (params.containsKey(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_VOLUME_LEVELER)) { + dab.volumeLeveler = + params.getBoolean(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_VOLUME_LEVELER); + params.remove(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_VOLUME_LEVELER); + } + if (params.containsKey( + SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SURROUND_VIRTUALIZER)) { + dab.surroundVirtualizer = params.getBoolean( + SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SURROUND_VIRTUALIZER); + params.remove(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SURROUND_VIRTUALIZER); + } + if (params.containsKey(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_DOLBY_ATMOS)) { + dab.dolbyAtmos = + params.getBoolean(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_DOLBY_ATMOS); + params.remove(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_DOLBY_ATMOS); + } soundParams.add(SoundParameter.dolbyAudioProcessing(dab)); DtsVirtualX dts = new DtsVirtualX(); - dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TBHDX); - dts.limiter = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_LIMITER); - dts.truSurroundX = params.getBoolean( - SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_SURROUND_X); - dts.truVolumeHd = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_VOLUME_HD); - dts.dialogClarity = params.getBoolean( - SoundQuality.PARAMETER_DTS_VIRTUAL_X_DIALOG_CLARITY); - dts.definition = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DEFINITION); - dts.height = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_HEIGHT); + if (params.containsKey(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TBHDX)) { + dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TBHDX); + params.remove(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TBHDX); + } + + if (params.containsKey(SoundQuality.PARAMETER_DTS_VIRTUAL_X_LIMITER)) { + dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_LIMITER); + params.remove(SoundQuality.PARAMETER_DTS_VIRTUAL_X_LIMITER); + } + if (params.containsKey(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_SURROUND_X)) { + dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_SURROUND_X); + params.remove(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_SURROUND_X); + } + if (params.containsKey(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_VOLUME_HD)) { + dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_VOLUME_HD); + params.remove(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_VOLUME_HD); + } + if (params.containsKey(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DIALOG_CLARITY)) { + dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DIALOG_CLARITY); + params.remove(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DIALOG_CLARITY); + } + if (params.containsKey(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DEFINITION)) { + dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DEFINITION); + params.remove(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DEFINITION); + } + if (params.containsKey(SoundQuality.PARAMETER_DTS_VIRTUAL_X_HEIGHT)) { + dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_HEIGHT); + params.remove(SoundQuality.PARAMETER_DTS_VIRTUAL_X_HEIGHT); + } soundParams.add(SoundParameter.dtsVirtualX(dts)); return soundParams.toArray(new SoundParameter[0]); @@ -1273,14 +1320,18 @@ public final class MediaQualityUtils { */ public static PictureProfile convertCursorToPictureProfileWithTempId(Cursor cursor, BiMap<Long, String> map) { + String tmpId = getTempId(map, cursor); + Long dbId = map.getKey(tmpId); + PictureProfileHandle handle = dbId == null + ? PictureProfileHandle.NONE : new PictureProfileHandle(dbId); return new PictureProfile( - getTempId(map, cursor), + tmpId, getType(cursor), getName(cursor), getInputId(cursor), getPackageName(cursor), jsonToPersistableBundle(getSettingsString(cursor)), - PictureProfileHandle.NONE + handle ); } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 6ce1746ed3f6..de20b82f505c 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -1877,73 +1877,35 @@ public class NotificationManagerService extends SystemService { } }; - private void unclassifyNotificationsForUser(final int userId) { - if (DBG) { - Slog.v(TAG, "unclassifyForUser: " + userId); - } - unclassifyNotificationsFiltered((r) -> r.getUserId() == userId); + private void applyNotificationUpdateForUser(final int userId, + NotificationUpdate notificationUpdate) { + applyUpdateForNotificationsFiltered((r) -> r.getUserId() == userId, + notificationUpdate); } - private void unclassifyNotificationsForUid(final int userId, @NonNull final String pkg) { - if (DBG) { - Slog.v(TAG, "unclassifyForUid userId: " + userId + " pkg: " + pkg); - } - unclassifyNotificationsFiltered((r) -> + private void applyNotificationUpdateForUid(final int userId, @NonNull final String pkg, + NotificationUpdate notificationUpdate) { + applyUpdateForNotificationsFiltered((r) -> r.getUserId() == userId - && Objects.equals(r.getSbn().getPackageName(), pkg)); + && Objects.equals(r.getSbn().getPackageName(), pkg), + notificationUpdate); } - private void unclassifyNotificationsForUserAndType(final int userId, - final @Types int bundleType) { - if (DBG) { - Slog.v(TAG, - "unclassifyForUserAndType userId: " + userId + " bundleType: " + bundleType); - } + private void applyNotificationUpdateForUserAndChannelType(final int userId, + final @Types int bundleType, NotificationUpdate notificationUpdate) { final String bundleChannelId = NotificationChannel.getChannelIdForBundleType(bundleType); - unclassifyNotificationsFiltered((r) -> + applyUpdateForNotificationsFiltered((r) -> r.getUserId() == userId && r.getChannel() != null - && Objects.equals(bundleChannelId, r.getChannel().getId())); + && Objects.equals(bundleChannelId, r.getChannel().getId()), + notificationUpdate); } - private void unclassifyNotificationsFiltered(Predicate<NotificationRecord> filter) { - if (!(notificationClassificationUi() && notificationRegroupOnClassification())) { - return; - } - synchronized (mNotificationLock) { - for (int i = 0; i < mEnqueuedNotifications.size(); i++) { - final NotificationRecord r = mEnqueuedNotifications.get(i); - if (filter.test(r)) { - unclassifyNotificationLocked(r); - } - } - - for (int i = 0; i < mNotificationList.size(); i++) { - final NotificationRecord r = mNotificationList.get(i); - if (filter.test(r)) { - unclassifyNotificationLocked(r); - } - } - } - } - - @GuardedBy("mNotificationLock") - private void unclassifyNotificationLocked(@NonNull final NotificationRecord r) { - if (DBG) { - Slog.v(TAG, "unclassifyNotification: " + r); - } - // Only NotificationRecord's mChannel is updated when bundled, the Notification - // mChannelId will always be the original channel. - String origChannelId = r.getNotification().getChannelId(); - NotificationChannel originalChannel = mPreferencesHelper.getNotificationChannel( - r.getSbn().getPackageName(), r.getUid(), origChannelId, false); - String currChannelId = r.getChannel().getId(); - boolean isClassified = NotificationChannel.SYSTEM_RESERVED_IDS.contains(currChannelId); - if (originalChannel != null && !origChannelId.equals(currChannelId) && isClassified) { - r.updateNotificationChannel(originalChannel); - mGroupHelper.onNotificationUnbundled(r, - GroupHelper.isOriginalGroupSummaryPresent(r, mSummaryByGroupKey)); - } + private void applyNotificationUpdateForUserAndType(final int userId, + final @Types int bundleType, NotificationUpdate notificationUpdate) { + applyUpdateForNotificationsFiltered( + (r) -> r.getUserId() == userId && r.getBundleType() == bundleType, + notificationUpdate); } @VisibleForTesting @@ -1956,7 +1918,7 @@ public class NotificationManagerService extends SystemService { if (r == null) { return; } - unclassifyNotificationLocked(r); + unclassifyNotificationLocked(r, true); } } @@ -1974,50 +1936,36 @@ public class NotificationManagerService extends SystemService { } } - private void reclassifyNotificationsFiltered(Predicate<NotificationRecord> filter) { - if (!(notificationClassificationUi() && notificationRegroupOnClassification())) { - return; - } - synchronized (mNotificationLock) { - for (int i = 0; i < mEnqueuedNotifications.size(); i++) { - final NotificationRecord r = mEnqueuedNotifications.get(i); - if (filter.test(r)) { - reclassifyNotificationLocked(r, false); - } - } - - for (int i = 0; i < mNotificationList.size(); i++) { - final NotificationRecord r = mNotificationList.get(i); - if (filter.test(r)) { - reclassifyNotificationLocked(r, true); - } - } - } - } - - private void reclassifyNotificationsForUserAndType(final int userId, - final @Types int bundleType) { + @GuardedBy("mNotificationLock") + private void unclassifyNotificationLocked(@NonNull final NotificationRecord r, + boolean isPosted) { if (DBG) { - Slog.v(TAG, "reclassifyNotificationsForUserAndType userId: " + userId + " bundleType: " - + bundleType); + Slog.v(TAG, "unclassifyNotification: " + r); } - reclassifyNotificationsFiltered( - (r) -> r.getUserId() == userId && r.getBundleType() == bundleType); - } - - private void reclassifyNotificationsForUid(final int userId, final String pkg) { - if (DBG) { - Slog.v(TAG, "reclassifyNotificationsForUid userId: " + userId + " pkg: " + pkg); + // Only NotificationRecord's mChannel is updated when bundled, the Notification + // mChannelId will always be the original channel. + String origChannelId = r.getNotification().getChannelId(); + NotificationChannel originalChannel = mPreferencesHelper.getNotificationChannel( + r.getSbn().getPackageName(), r.getUid(), origChannelId, false); + String currChannelId = r.getChannel().getId(); + boolean isClassified = NotificationChannel.SYSTEM_RESERVED_IDS.contains(currChannelId); + if (originalChannel != null && !origChannelId.equals(currChannelId) && isClassified) { + r.updateNotificationChannel(originalChannel); + mGroupHelper.onNotificationUnbundled(r, + GroupHelper.isOriginalGroupSummaryPresent(r, mSummaryByGroupKey)); } - reclassifyNotificationsFiltered((r) -> - r.getUserId() == userId && Objects.equals(r.getSbn().getPackageName(), pkg)); } - private void reclassifyNotificationsForUser(final int userId) { - if (DBG) { - Slog.v(TAG, "reclassifyAllNotificationsForUser: " + userId); - } - reclassifyNotificationsFiltered((r) -> r.getUserId() == userId); + @GuardedBy("mNotificationLock") + private void unsummarizeNotificationLocked(@NonNull final NotificationRecord r, + boolean isPosted) { + Bundle signals = new Bundle(); + signals.putString(KEY_SUMMARIZATION, null); + Adjustment adjustment = new Adjustment(r.getSbn().getPackageName(), r.getKey(), signals, "", + r.getSbn().getUserId()); + r.addAdjustment(adjustment); + mRankingHandler.requestSort(); + } @GuardedBy("mNotificationLock") @@ -2043,6 +1991,33 @@ public class NotificationManagerService extends SystemService { } } + /** + * Given a filter and a function to update a notification record, runs that function on all + * enqueued and posted notifications that match the filter + */ + private void applyUpdateForNotificationsFiltered(Predicate<NotificationRecord> filter, + NotificationUpdate notificationUpdate) { + synchronized (mNotificationLock) { + for (int i = 0; i < mEnqueuedNotifications.size(); i++) { + final NotificationRecord r = mEnqueuedNotifications.get(i); + if (filter.test(r)) { + notificationUpdate.apply(r, false); + } + } + + for (int i = 0; i < mNotificationList.size(); i++) { + final NotificationRecord r = mNotificationList.get(i); + if (filter.test(r)) { + notificationUpdate.apply(r, true); + } + } + } + } + + private interface NotificationUpdate { + void apply(NotificationRecord r, boolean isPosted); + } + NotificationManagerPrivate mNotificationManagerPrivate = new NotificationManagerPrivate() { @Nullable @Override @@ -4475,9 +4450,11 @@ public class NotificationManagerService extends SystemService { public void allowAssistantAdjustment(String adjustmentType) { checkCallerIsSystemOrSystemUiOrShell(); mAssistants.allowAdjustmentType(adjustmentType); + int userId = UserHandle.getUserId(Binder.getCallingUid()); if ((notificationClassificationUi() && notificationRegroupOnClassification())) { if (KEY_TYPE.equals(adjustmentType)) { - reclassifyNotificationsForUser(UserHandle.getUserId(Binder.getCallingUid())); + applyNotificationUpdateForUser(userId, + NotificationManagerService.this::reclassifyNotificationLocked); } } handleSavePolicyFile(); @@ -4488,9 +4465,17 @@ public class NotificationManagerService extends SystemService { public void disallowAssistantAdjustment(String adjustmentType) { checkCallerIsSystemOrSystemUiOrShell(); mAssistants.disallowAdjustmentType(adjustmentType); + int userId = UserHandle.getUserId(Binder.getCallingUid()); if ((notificationClassificationUi() && notificationRegroupOnClassification())) { if (KEY_TYPE.equals(adjustmentType)) { - unclassifyNotificationsForUser(UserHandle.getUserId(Binder.getCallingUid())); + applyNotificationUpdateForUser(userId, + NotificationManagerService.this::unclassifyNotificationLocked); + } + } + if (nmSummarizationUi() || nmSummarization()) { + if (KEY_SUMMARIZATION.equals(adjustmentType)) { + applyNotificationUpdateForUser(userId, + NotificationManagerService.this::unsummarizeNotificationLocked); } } handleSavePolicyFile(); @@ -4539,11 +4524,13 @@ public class NotificationManagerService extends SystemService { mAssistants.setAssistantAdjustmentKeyTypeState(type, enabled); if ((notificationClassificationUi() && notificationRegroupOnClassification())) { if (enabled) { - reclassifyNotificationsForUserAndType( - UserHandle.getUserId(Binder.getCallingUid()), type); + applyNotificationUpdateForUserAndType( + UserHandle.getUserId(Binder.getCallingUid()), type, + NotificationManagerService.this::reclassifyNotificationLocked); } else { - unclassifyNotificationsForUserAndType( - UserHandle.getUserId(Binder.getCallingUid()), type); + applyNotificationUpdateForUserAndChannelType( + UserHandle.getUserId(Binder.getCallingUid()), type, + NotificationManagerService.this::unclassifyNotificationLocked); } } handleSavePolicyFile(); @@ -4569,11 +4556,17 @@ public class NotificationManagerService extends SystemService { if (notificationClassificationUi() && notificationRegroupOnClassification() && key.equals(KEY_TYPE)) { if (enabled) { - reclassifyNotificationsForUid(UserHandle.getUserId(Binder.getCallingUid()), - pkg); + applyNotificationUpdateForUid(UserHandle.getUserId(Binder.getCallingUid()), + pkg, NotificationManagerService.this::reclassifyNotificationLocked); } else { - unclassifyNotificationsForUid(UserHandle.getUserId(Binder.getCallingUid()), - pkg); + applyNotificationUpdateForUid(UserHandle.getUserId(Binder.getCallingUid()), + pkg, NotificationManagerService.this::unclassifyNotificationLocked); + } + } + if (nmSummarization() || nmSummarizationUi()) { + if (KEY_SUMMARIZATION.equals(key) && !enabled) { + applyNotificationUpdateForUid(UserHandle.getUserId(Binder.getCallingUid()), + pkg, NotificationManagerService.this::unsummarizeNotificationLocked); } } handleSavePolicyFile(); @@ -7417,6 +7410,10 @@ public class NotificationManagerService extends SystemService { adjustments); if (newChannel == null || newChannel.getId().equals(r.getChannel().getId())) { adjustments.remove(KEY_TYPE); + } else if (android.app.Flags.apiRichOngoing() && hasFlag(r.getNotification().flags, + FLAG_PROMOTED_ONGOING)) { + // Don't bundle any promoted ongoing notifications + adjustments.remove(KEY_TYPE); } else { // Save the app-provided type for logging. int classification = adjustments.getInt(KEY_TYPE); diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index cec5a93a2a15..700f6fafe2d7 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -999,7 +999,7 @@ public final class NotificationRecord { return null; } - public String getSummarization() { + public @Nullable String getSummarization() { if ((android.app.Flags.nmSummarizationUi() || android.app.Flags.nmSummarization())) { return mSummarization; } diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index e02ec6a9e3b4..d98f3617f587 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -4519,9 +4519,6 @@ final class InstallPackageHelper { * at boot. */ private boolean needSignatureMatchToSystem(String packageName) { - if (!android.security.Flags.extendVbChainToUpdatedApk()) { - return false; - } return mPm.mInjector.getSystemConfig().getPreinstallPackagesWithStrictSignatureCheck() .contains(packageName); } diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index 2dd679818ada..5160319c8cf6 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -379,9 +379,10 @@ public class LauncherAppsService extends SystemService { public List<UserHandle> getUserProfiles() { int[] userIds; if (!canAccessHiddenProfile(getCallingUid(), getCallingPid())) { - userIds = mUm.getProfileIdsExcludingHidden(getCallingUserId(), /* enabled= */ true); + userIds = mUserManagerInternal.getProfileIdsExcludingHidden(getCallingUserId(), + /* enabled= */ true); } else { - userIds = mUm.getEnabledProfileIds(getCallingUserId()); + userIds = mUserManagerInternal.getProfileIds(getCallingUserId(), true); } final List<UserHandle> result = new ArrayList<>(userIds.length); for (int userId : userIds) { @@ -398,9 +399,10 @@ public class LauncherAppsService extends SystemService { int[] userIds; if (!canAccessHiddenProfile(callingUid, Binder.getCallingPid())) { - userIds = mUm.getProfileIdsExcludingHidden(getCallingUserId(), /* enabled= */ true); + userIds = mUserManagerInternal.getProfileIdsExcludingHidden(getCallingUserId(), + /* enabled= */ true); } else { - userIds = mUm.getEnabledProfileIds(getCallingUserId()); + userIds = mUserManagerInternal.getProfileIds(getCallingUserId(), true); } final long token = Binder.clearCallingIdentity(); @@ -503,16 +505,11 @@ public class LauncherAppsService extends SystemService { return true; } - long ident = injectClearCallingIdentity(); - try { - final UserInfo callingUserInfo = mUm.getUserInfo(callingUserId); - if (callingUserInfo != null && callingUserInfo.isProfile()) { - Slog.w(TAG, message + " for another profile " - + targetUserId + " from " + callingUserId + " not allowed"); - return false; - } - } finally { - injectRestoreCallingIdentity(ident); + final UserInfo callingUserInfo = mUserManagerInternal.getUserInfo(callingUserId); + if (callingUserInfo != null && callingUserInfo.isProfile()) { + Slog.w(TAG, message + " for another profile " + + targetUserId + " from " + callingUserId + " not allowed"); + return false; } if (isHiddenProfile(UserHandle.of(targetUserId)) @@ -529,9 +526,9 @@ public class LauncherAppsService extends SystemService { return false; } - long identity = injectClearCallingIdentity(); try { - UserProperties properties = mUm.getUserProperties(targetUser); + UserProperties properties = mUserManagerInternal + .getUserProperties(targetUser.getIdentifier()); if (properties == null) { return false; } @@ -540,8 +537,6 @@ public class LauncherAppsService extends SystemService { == UserProperties.PROFILE_API_VISIBILITY_HIDDEN; } catch (IllegalArgumentException e) { return false; - } finally { - injectRestoreCallingIdentity(identity); } } @@ -686,7 +681,7 @@ public class LauncherAppsService extends SystemService { final int callingUid = injectBinderCallingUid(); final long ident = injectClearCallingIdentity(); try { - if (mUm.getUserInfo(user.getIdentifier()).isManagedProfile()) { + if (mUserManagerInternal.getUserInfo(user.getIdentifier()).isManagedProfile()) { // Managed profile should not show hidden apps return launcherActivities; } @@ -1713,7 +1708,7 @@ public class LauncherAppsService extends SystemService { } final long identity = Binder.clearCallingIdentity(); try { - String userType = mUm.getUserInfo(user.getIdentifier()).userType; + String userType = mUserManagerInternal.getUserInfo(user.getIdentifier()).userType; Set<String> preInstalledPackages = mUm.getPreInstallableSystemPackages(userType); if (preInstalledPackages == null) { return new ArrayList<>(); diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index af788ea6ccdb..f694dc924c02 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -3107,8 +3107,17 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { mInternalProgress = 0.5f; computeProgressLocked(true); } + final File libDir = new File(stageDir, NativeLibraryHelper.LIB_DIR_NAME); + if (!mayInheritNativeLibs()) { + // Start from a clean slate + NativeLibraryHelper.removeNativeBinariesFromDirLI(libDir, true); + } + // Skip native libraries processing for archival installation. + if (isArchivedInstallation()) { + return; + } extractNativeLibraries( - mPackageLite, stageDir, params.abiOverride, mayInheritNativeLibs()); + mPackageLite, libDir, params.abiOverride); } } } @@ -3601,10 +3610,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { // Needs to happen before the first v4 signature verification, which happens in // getAddedApkLitesLocked. - if (android.security.Flags.extendVbChainToUpdatedApk()) { - if (!isIncrementalInstallation()) { - enableFsVerityToAddedApksWithIdsig(); - } + if (!isIncrementalInstallation()) { + enableFsVerityToAddedApksWithIdsig(); } final List<ApkLite> addedFiles = getAddedApkLitesLocked(); @@ -4115,8 +4122,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { stageFileLocked(origFile, targetFile); // Stage APK's v4 signature if present, and fs-verity is supported. - if (android.security.Flags.extendVbChainToUpdatedApk() - && VerityUtils.isFsVeritySupported()) { + if (VerityUtils.isFsVeritySupported()) { maybeStageV4SignatureLocked(origFile, targetFile); } // Stage ART managed install files (e.g., dex metadata (.dm)) and corresponding fs-verity @@ -4143,9 +4149,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private void inheritFileLocked(File origFile, List<String> artManagedFilePaths) { mResolvedInheritedFiles.add(origFile); - if (android.security.Flags.extendVbChainToUpdatedApk()) { - maybeInheritV4SignatureLocked(origFile); - } + maybeInheritV4SignatureLocked(origFile); // Inherit ART managed install files (e.g., dex metadata (.dm)) if present. if (com.android.art.flags.Flags.artServiceV3()) { @@ -4505,21 +4509,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { Slog.d(TAG, "Copied " + fromFiles.size() + " files into " + toDir); } - private void extractNativeLibraries(PackageLite packageLite, File packageDir, - String abiOverride, boolean inherit) + private void extractNativeLibraries(PackageLite packageLite, File libDir, + String abiOverride) throws PackageManagerException { Objects.requireNonNull(packageLite); - final File libDir = new File(packageDir, NativeLibraryHelper.LIB_DIR_NAME); - if (!inherit) { - // Start from a clean slate - NativeLibraryHelper.removeNativeBinariesFromDirLI(libDir, true); - } - - // Skip native libraries processing for archival installation. - if (isArchivedInstallation()) { - return; - } - NativeLibraryHelper.Handle handle = null; try { handle = NativeLibraryHelper.Handle.create(packageLite); diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java index 3e376b6958ec..2ecdb0b1a02a 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java @@ -545,8 +545,7 @@ public class PackageManagerServiceUtils { // Also make sure the parsed signatures are consistent with the disabled package // setting, if any. The additional UNKNOWN check is because disabled package settings // may not have SigningDetails currently, and we don't want to cause an uninstall. - if (android.security.Flags.extendVbChainToUpdatedApk() - && match && disabledPkgSetting != null + if (match && disabledPkgSetting != null && disabledPkgSetting.getSigningDetails() != SigningDetails.UNKNOWN) { match = matchSignatureInSystem(packageName, parsedSignatures, disabledPkgSetting); } diff --git a/services/core/java/com/android/server/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java index e98176b0e82b..41ce4fa81668 100644 --- a/services/core/java/com/android/server/pm/UserManagerInternal.java +++ b/services/core/java/com/android/server/pm/UserManagerInternal.java @@ -368,6 +368,21 @@ public abstract class UserManagerInternal { public abstract @NonNull int[] getProfileIds(@UserIdInt int userId, boolean enabledOnly); /** + * Returns a list of the users that are associated with the specified user, including the user + * itself. This includes the user, its profiles, its parent, and its parent's other profiles, + * as applicable. + * + * <p>Note that this includes only profile types that are not hidden. + * + * @param userId id of the user to return profiles for + * @param enabledOnly whether return only {@link UserInfo#isEnabled() enabled} profiles + * @return A non-empty array of ids of profiles associated with the specified user if the user + * exists. Otherwise, an empty array. + */ + public abstract @NonNull int[] getProfileIdsExcludingHidden(@UserIdInt int userId, + boolean enabledOnly); + + /** * Checks if the {@code callingUserId} and {@code targetUserId} are same or in same group * and that the {@code callingUserId} is not a profile and {@code targetUserId} is enabled. * diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 053b4aae90dd..e1e8fc231dda 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1712,15 +1712,19 @@ public class UserManagerService extends IUserManager.Stub { @Override public int getCredentialOwnerProfile(@UserIdInt int userId) { checkManageUsersPermission("get the credential owner"); - if (!mLockPatternUtils.isSeparateProfileChallengeEnabled(userId)) { - synchronized (mUsersLock) { - UserInfo profileParent = getProfileParentLU(userId); - if (profileParent != null) { - return profileParent.id; + final long identity = Binder.clearCallingIdentity(); + try { + if (!mLockPatternUtils.isSeparateProfileChallengeEnabled(userId)) { + synchronized (mUsersLock) { + UserInfo profileParent = getProfileParentLU(userId); + if (profileParent != null) { + return profileParent.id; + } } } + } finally { + Binder.restoreCallingIdentity(identity); } - return userId; } @@ -8049,6 +8053,14 @@ public class UserManagerService extends IUserManager.Stub { } @Override + public int[] getProfileIdsExcludingHidden(@UserIdInt int userId, boolean enabledOnly) { + synchronized (mUsersLock) { + return getProfileIdsLU(userId, null /* userType */, enabledOnly, /* excludeHidden */ + true).toArray(); + } + } + + @Override public @Nullable LauncherUserInfo getLauncherUserInfo(@UserIdInt int userId) { UserInfo userInfo; synchronized (mUsersLock) { 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 ac19ea12c6a4..fbf81b9accad 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -1541,8 +1541,19 @@ public class PermissionManagerService extends IPermissionManager.Stub { } final AttributionSource resolvedAttributionSource = accessorSource.withPackageName(resolvedAccessorPackageName); - final int opMode = appOpsManager.unsafeCheckOpRawNoThrow(op, - resolvedAttributionSource); + // Avoid checking the first attr in the chain in some cases for consistency with + // checks for data delivery. + // In particular, for chains of 2 or more, when skipProxyOperation is true, the + // for data delivery implementation does not actually check the first link in the + // chain. If the attribution is just a singleReceiverFromDatasource, this + // exemption does not apply, since it does not go through proxyOp flow, and the top + // of the chain is actually removed above. + // Skipping the check avoids situations where preflight checks fail since the data + // source itself does not have the op (e.g. audioserver). + final int opMode = (skipProxyOperation && !singleReceiverFromDatasource) ? + AppOpsManager.MODE_ALLOWED : + appOpsManager.unsafeCheckOpRawNoThrow(op, resolvedAttributionSource); + final AttributionSource next = accessorSource.getNext(); if (!selfAccess && opMode == AppOpsManager.MODE_ALLOWED && next != null) { final String resolvedNextPackageName = resolvePackageName(context, next); diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 8cf0481b1dc3..d3aa0469435c 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -520,32 +520,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { private WindowWakeUpPolicy mWindowWakeUpPolicy; - /** - * The three variables below are used for custom power key gesture detection in - * PhoneWindowManager. They are used to detect when the power button has been double pressed - * and, when it does happen, makes the behavior overrideable by the app. - * - * We cannot use the {@link PowerKeyRule} for this because multi-press power gesture detection - * and behaviors are handled by {@link com.android.server.GestureLauncherService}, and the - * {@link PowerKeyRule} only handles single and long-presses of the power button. As a result, - * overriding the double tap behavior requires custom gesture detection here that mimics the - * logic in {@link com.android.server.GestureLauncherService}. - * - * Long-term, it would be beneficial to move all power gesture detection to - * {@link PowerKeyRule} so that this custom logic isn't required. - */ - // Time of last power down event. - private long mLastPowerDown; - - // Number of power button events consecutively triggered (within a specific timeout threshold). - private int mPowerButtonConsecutiveTaps = 0; - - // Whether a double tap of the power button has been detected. - volatile boolean mDoubleTapPowerDetected; - - // Runnable that is queued on a delay when the first power keyDown event is sent to the app. - private Runnable mPowerKeyDelayedRunnable = null; - boolean mSafeMode; // Whether to allow dock apps with METADATA_DOCK_HOME to temporarily take over the Home key. @@ -1135,10 +1109,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { || handledByPowerManager || isKeyGestureTriggered || mKeyCombinationManager.isPowerKeyIntercepted(); - if (overridePowerKeyBehaviorInFocusedWindow()) { - mPowerKeyHandled |= mDoubleTapPowerDetected; - } - if (!mPowerKeyHandled) { if (!interactive) { wakeUpFromWakeKey(event); @@ -2785,18 +2755,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (mShouldEarlyShortPressOnPower) { return; } - // TODO(b/380433365): Remove deferring single power press action when refactoring. - if (overridePowerKeyBehaviorInFocusedWindow()) { - mDeferredKeyActionExecutor.cancelQueuedAction(KEYCODE_POWER); - mDeferredKeyActionExecutor.queueKeyAction( - KEYCODE_POWER, - downTime, - () -> { - powerPress(downTime, 1 /*count*/, displayId); - }); - } else { - powerPress(downTime, 1 /*count*/, displayId); - } + powerPress(downTime, 1 /*count*/, displayId); } @Override @@ -2827,17 +2786,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { @Override void onMultiPress(long downTime, int count, int displayId) { - if (overridePowerKeyBehaviorInFocusedWindow()) { - mDeferredKeyActionExecutor.cancelQueuedAction(KEYCODE_POWER); - mDeferredKeyActionExecutor.queueKeyAction( - KEYCODE_POWER, - downTime, - () -> { - powerPress(downTime, count, displayId); - }); - } else { - powerPress(downTime, count, displayId); - } + powerPress(downTime, count, displayId); } @Override @@ -3614,12 +3563,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } - if (overridePowerKeyBehaviorInFocusedWindow() && event.getKeyCode() == KEYCODE_POWER - && event.getAction() == KeyEvent.ACTION_UP - && mDoubleTapPowerDetected) { - mDoubleTapPowerDetected = false; - } - return needToConsumeKey ? keyConsumed : keyNotConsumed; } @@ -4117,8 +4060,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { sendSystemKeyToStatusBarAsync(event); return true; } - case KeyEvent.KEYCODE_POWER: - return interceptPowerKeyBeforeDispatching(focusedToken, event); case KeyEvent.KEYCODE_SCREENSHOT: if (firstDown) { interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/); @@ -4174,8 +4115,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { sendSystemKeyToStatusBarAsync(event); return true; } - case KeyEvent.KEYCODE_POWER: - return interceptPowerKeyBeforeDispatching(focusedToken, event); } if (isValidGlobalKey(keyCode) && mGlobalKeyManager.handleGlobalKey(mContext, keyCode, event)) { @@ -4193,90 +4132,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { return (metaState & KeyEvent.META_META_ON) != 0; } - /** - * Called by interceptKeyBeforeDispatching to handle interception logic for KEYCODE_POWER - * KeyEvents. - * - * @return true if intercepting the key, false if sending to app. - */ - private boolean interceptPowerKeyBeforeDispatching(IBinder focusedToken, KeyEvent event) { - if (!overridePowerKeyBehaviorInFocusedWindow()) { - //Flag disabled: intercept the power key and do not send to app. - return true; - } - if (event.getKeyCode() != KEYCODE_POWER) { - Log.wtf(TAG, "interceptPowerKeyBeforeDispatching received a non-power KeyEvent " - + "with key code: " + event.getKeyCode()); - return false; - } - - // Intercept keys (don't send to app) for 3x, 4x, 5x gestures) - if (mPowerButtonConsecutiveTaps > DOUBLE_POWER_TAP_COUNT_THRESHOLD) { - setDeferredKeyActionsExecutableAsync(KEYCODE_POWER, event.getDownTime()); - return true; - } - - // UP key; just reuse the original decision. - if (event.getAction() == KeyEvent.ACTION_UP) { - final Set<Integer> consumedKeys = mConsumedKeysForDevice.get(event.getDeviceId()); - return consumedKeys != null - && consumedKeys.contains(event.getKeyCode()); - } - - KeyInterceptionInfo info = - mWindowManagerInternal.getKeyInterceptionInfoFromToken(focusedToken); - - if (info == null || !mButtonOverridePermissionChecker.canWindowOverridePowerKey(mContext, - info.windowOwnerUid, info.inputFeaturesFlags)) { - // The focused window does not have the permission to override power key behavior. - if (DEBUG_INPUT) { - String interceptReason = ""; - if (info == null) { - interceptReason = "Window is null"; - } else if (!mButtonOverridePermissionChecker.canAppOverrideSystemKey(mContext, - info.windowOwnerUid)) { - interceptReason = "Application does not have " - + "OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW permission"; - } else { - interceptReason = "Window does not have inputFeatureFlag set"; - } - - Log.d(TAG, TextUtils.formatSimple("Intercepting KEYCODE_POWER event. action=%d, " - + "eventTime=%d to window=%s. interceptReason=%s. " - + "mDoubleTapPowerDetected=%b", - event.getAction(), event.getEventTime(), (info != null) - ? info.windowTitle : "null", interceptReason, - mDoubleTapPowerDetected)); - } - // Intercept the key (i.e. do not send to app) - setDeferredKeyActionsExecutableAsync(KEYCODE_POWER, event.getDownTime()); - return true; - } - - if (DEBUG_INPUT) { - Log.d(TAG, TextUtils.formatSimple("Sending KEYCODE_POWER to app. action=%d, " - + "eventTime=%d to window=%s. mDoubleTapPowerDetected=%b", - event.getAction(), event.getEventTime(), info.windowTitle, - mDoubleTapPowerDetected)); - } - - if (!mDoubleTapPowerDetected) { - //Single press: post a delayed runnable for the single press power action that will be - // called if it's not cancelled by a double press. - final var downTime = event.getDownTime(); - mPowerKeyDelayedRunnable = () -> - setDeferredKeyActionsExecutableAsync(KEYCODE_POWER, downTime); - mHandler.postDelayed(mPowerKeyDelayedRunnable, POWER_MULTI_PRESS_TIMEOUT_MILLIS); - } else if (mPowerKeyDelayedRunnable != null) { - //Double press detected: cancel the single press runnable. - mHandler.removeCallbacks(mPowerKeyDelayedRunnable); - mPowerKeyDelayedRunnable = null; - } - - // Focused window has permission. Send to app. - return false; - } - @SuppressLint("MissingPermission") private void initKeyGestures() { if (!useKeyGestureEventHandler()) { @@ -4303,7 +4158,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN, KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER, KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS, - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS, KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH, KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH, KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT, @@ -4456,7 +4310,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { } break; case KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS: - case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS: if (complete && isKeyEventForCurrentUser(event.getDisplayId(), event.getKeycodes()[0], "launchAllAppsViaA11y")) { launchAllAppsAction(); @@ -4764,11 +4617,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { return true; } - if (overridePowerKeyBehaviorInFocusedWindow() && keyCode == KEYCODE_POWER) { - handleUnhandledSystemKey(event); - return true; - } - if (useKeyGestureEventHandler()) { return false; } @@ -5595,12 +5443,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { KeyEvent.actionToString(event.getAction()), mPowerKeyHandled ? 1 : 0, mSingleKeyGestureDetector.getKeyPressCounter(KeyEvent.KEYCODE_POWER)); - if (overridePowerKeyBehaviorInFocusedWindow()) { - result |= ACTION_PASS_TO_USER; - } else { - // Any activity on the power button stops the accessibility shortcut - result &= ~ACTION_PASS_TO_USER; - } + // Any activity on the power button stops the accessibility shortcut + result &= ~ACTION_PASS_TO_USER; isWakeKey = false; // wake-up will be handled separately if (down) { interceptPowerKeyDown(event, interactiveAndAwake, isKeyGestureTriggered); @@ -5862,35 +5706,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { } if (event.getKeyCode() == KEYCODE_POWER && event.getAction() == KeyEvent.ACTION_DOWN) { - if (overridePowerKeyBehaviorInFocusedWindow()) { - if (event.getRepeatCount() > 0 && !mHasFeatureWatch) { - return; - } - if (mGestureLauncherService != null) { - mGestureLauncherService.processPowerKeyDown(event); - } - - if (detectDoubleTapPower(event)) { - mDoubleTapPowerDetected = true; - - // Copy of the event for handler in case the original event gets recycled. - KeyEvent eventCopy = KeyEvent.obtain(event); - mDeferredKeyActionExecutor.queueKeyAction( - KeyEvent.KEYCODE_POWER, - eventCopy.getEventTime(), - () -> { - if (!handleCameraGesture(eventCopy, interactive)) { - mSingleKeyGestureDetector.interceptKey( - eventCopy, interactive, defaultDisplayOn); - } else { - mSingleKeyGestureDetector.reset(); - } - eventCopy.recycle(); - }); - return; - } - } - mPowerKeyHandled = handleCameraGesture(event, interactive); if (mPowerKeyHandled) { // handled by camera gesture. @@ -5902,26 +5717,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { mSingleKeyGestureDetector.interceptKey(event, interactive, defaultDisplayOn); } - private boolean detectDoubleTapPower(KeyEvent event) { - //Watches use the SingleKeyGestureDetector for detecting multi-press gestures. - if (mHasFeatureWatch || event.getKeyCode() != KEYCODE_POWER - || event.getAction() != KeyEvent.ACTION_DOWN || event.getRepeatCount() != 0) { - return false; - } - - final long powerTapInterval = event.getEventTime() - mLastPowerDown; - mLastPowerDown = event.getEventTime(); - if (powerTapInterval >= POWER_MULTI_PRESS_TIMEOUT_MILLIS) { - // Tap too slow for double press - mPowerButtonConsecutiveTaps = 1; - } else { - mPowerButtonConsecutiveTaps++; - } - - return powerTapInterval < POWER_MULTI_PRESS_TIMEOUT_MILLIS - && mPowerButtonConsecutiveTaps == DOUBLE_POWER_TAP_COUNT_THRESHOLD; - } - // The camera gesture will be detected by GestureLauncherService. private boolean handleCameraGesture(KeyEvent event, boolean interactive) { // camera gesture. @@ -7779,12 +7574,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { null) == PERMISSION_GRANTED; } - - boolean canWindowOverridePowerKey(Context context, int uid, int inputFeaturesFlags) { - return canAppOverrideSystemKey(context, uid) - && (inputFeaturesFlags & WindowManager.LayoutParams - .INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS) != 0; - } } private int getTargetDisplayIdForKeyEvent(KeyEvent event) { diff --git a/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java b/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java index f90da644c0ce..a9896e96a08f 100644 --- a/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java +++ b/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java @@ -272,15 +272,6 @@ public class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStat mHandler.removeMessages(SYNC_WAKELOCK_CHANGE); } - @Override - public void scheduleSyncDueToBatteryLevelChange(long delayMillis) { - synchronized (BatteryExternalStatsWorker.this) { - scheduleDelayedSyncLocked(SYNC_BATTERY_LEVEL_CHANGE, - () -> scheduleSync("battery-level", UPDATE_ALL), - delayMillis); - } - } - @GuardedBy("this") private void cancelSyncDueToBatteryLevelChangeLocked() { mHandler.removeMessages(SYNC_BATTERY_LEVEL_CHANGE); diff --git a/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java b/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java index 7cd9bdbc662c..b4ca7845ffee 100644 --- a/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java +++ b/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java @@ -505,7 +505,9 @@ public class BatteryHistoryDirectory implements BatteryStatsHistory.BatteryHisto for (int i = 0; i < mHistoryFiles.size(); i++) { size += (int) mHistoryFiles.get(i).atomicFile.getBaseFile().length(); } - while (size > mMaxHistorySize) { + // Trim until the directory size is within the limit or there is just one most + // recent file left in the directory + while (size > mMaxHistorySize && mHistoryFiles.size() > 1) { BatteryHistoryFile oldest = mHistoryFiles.get(0); int length = (int) oldest.atomicFile.getBaseFile().length(); oldest.atomicFile.delete(); diff --git a/services/core/java/com/android/server/power/stats/BatteryHistoryStepDetailsProvider.java b/services/core/java/com/android/server/power/stats/BatteryHistoryStepDetailsProvider.java new file mode 100644 index 000000000000..cd7612523b3e --- /dev/null +++ b/services/core/java/com/android/server/power/stats/BatteryHistoryStepDetailsProvider.java @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.power.stats; + +import android.hardware.power.stats.PowerEntity; +import android.hardware.power.stats.State; +import android.hardware.power.stats.StateResidency; +import android.hardware.power.stats.StateResidencyResult; +import android.os.BatteryStats; +import android.power.PowerStatsInternal; +import android.util.Slog; +import android.util.SparseArray; + +import com.android.server.LocalServices; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +class BatteryHistoryStepDetailsProvider { + public static final String TAG = "BatteryHistoryStepDetails"; + private static final boolean DEBUG = false; + + private static final int POWER_STATS_QUERY_TIMEOUT_MILLIS = 2000; + private static final int MAX_LOW_POWER_STATS_SIZE = 32768; + + private final BatteryStatsImpl mBatteryStats; + + private final BatteryStats.HistoryStepDetails mDetails = new BatteryStats.HistoryStepDetails(); + + private boolean mHasHistoryStepDetails; + + /** + * Total time (in milliseconds) spent executing in user code. + */ + private long mLastStepCpuUserTimeMs; + private long mCurStepCpuUserTimeMs; + /** + * Total time (in milliseconds) spent executing in kernel code. + */ + private long mLastStepCpuSystemTimeMs; + private long mCurStepCpuSystemTimeMs; + /** + * Times from /proc/stat (but measured in milliseconds). + */ + private long mLastStepStatUserTimeMs; + private long mLastStepStatSystemTimeMs; + private long mLastStepStatIOWaitTimeMs; + private long mLastStepStatIrqTimeMs; + private long mLastStepStatSoftIrqTimeMs; + private long mLastStepStatIdleTimeMs; + private long mCurStepStatUserTimeMs; + private long mCurStepStatSystemTimeMs; + private long mCurStepStatIOWaitTimeMs; + private long mCurStepStatIrqTimeMs; + private long mCurStepStatSoftIrqTimeMs; + private long mCurStepStatIdleTimeMs; + + private PowerStatsInternal mPowerStatsInternal; + private final Map<Integer, String> mEntityNames = new HashMap<>(); + private final Map<Integer, Map<Integer, String>> mStateNames = new HashMap<>(); + + BatteryHistoryStepDetailsProvider(BatteryStatsImpl batteryStats) { + mBatteryStats = batteryStats; + } + + void onSystemReady() { + mPowerStatsInternal = LocalServices.getService(PowerStatsInternal.class); + if (mPowerStatsInternal != null) { + populatePowerEntityMaps(); + } + } + + void requestUpdate() { + mBatteryStats.mHandler.post(this::update); + } + + void update() { + mHasHistoryStepDetails = false; + mBatteryStats.updateCpuDetails(); + calculateHistoryStepDetails(); + updateStateResidency(); + mBatteryStats.getHistory().recordHistoryStepDetails(mDetails, + mBatteryStats.mClock.elapsedRealtime(), + mBatteryStats.mClock.uptimeMillis()); + } + + private void calculateHistoryStepDetails() { + if (!mHasHistoryStepDetails) { + return; + } + + if (DEBUG) { + Slog.d(TAG, "Step stats last: user=" + mLastStepCpuUserTimeMs + " sys=" + + mLastStepStatSystemTimeMs + " io=" + mLastStepStatIOWaitTimeMs + + " irq=" + mLastStepStatIrqTimeMs + " sirq=" + + mLastStepStatSoftIrqTimeMs + " idle=" + mLastStepStatIdleTimeMs); + Slog.d(TAG, "Step stats cur: user=" + mCurStepCpuUserTimeMs + " sys=" + + mCurStepStatSystemTimeMs + " io=" + mCurStepStatIOWaitTimeMs + + " irq=" + mCurStepStatIrqTimeMs + " sirq=" + + mCurStepStatSoftIrqTimeMs + " idle=" + mCurStepStatIdleTimeMs); + } + mDetails.userTime = (int) (mCurStepCpuUserTimeMs - mLastStepCpuUserTimeMs); + mDetails.systemTime = (int) (mCurStepCpuSystemTimeMs - mLastStepCpuSystemTimeMs); + mDetails.statUserTime = (int) (mCurStepStatUserTimeMs - mLastStepStatUserTimeMs); + mDetails.statSystemTime = + (int) (mCurStepStatSystemTimeMs - mLastStepStatSystemTimeMs); + mDetails.statIOWaitTime = + (int) (mCurStepStatIOWaitTimeMs - mLastStepStatIOWaitTimeMs); + mDetails.statIrqTime = (int) (mCurStepStatIrqTimeMs - mLastStepStatIrqTimeMs); + mDetails.statSoftIrqTime = + (int) (mCurStepStatSoftIrqTimeMs - mLastStepStatSoftIrqTimeMs); + mDetails.statIdlTime = (int) (mCurStepStatIdleTimeMs - mLastStepStatIdleTimeMs); + mDetails.appCpuUid1 = mDetails.appCpuUid2 = mDetails.appCpuUid3 = -1; + mDetails.appCpuUTime1 = mDetails.appCpuUTime2 = mDetails.appCpuUTime3 = 0; + mDetails.appCpuSTime1 = mDetails.appCpuSTime2 = mDetails.appCpuSTime3 = 0; + SparseArray<? extends BatteryStats.Uid> uidStats = mBatteryStats.getUidStats(); + final int uidCount = uidStats.size(); + for (int i = 0; i < uidCount; i++) { + final BatteryStatsImpl.Uid uid = (BatteryStatsImpl.Uid) uidStats.valueAt(i); + final int totalUTimeMs = + (int) (uid.mCurStepUserTimeMs - uid.mLastStepUserTimeMs); + final int totalSTimeMs = + (int) (uid.mCurStepSystemTimeMs - uid.mLastStepSystemTimeMs); + final int totalTimeMs = totalUTimeMs + totalSTimeMs; + uid.mLastStepUserTimeMs = uid.mCurStepUserTimeMs; + uid.mLastStepSystemTimeMs = uid.mCurStepSystemTimeMs; + if (totalTimeMs <= (mDetails.appCpuUTime3 + mDetails.appCpuSTime3)) { + continue; + } + if (totalTimeMs <= (mDetails.appCpuUTime2 + mDetails.appCpuSTime2)) { + mDetails.appCpuUid3 = uid.mUid; + mDetails.appCpuUTime3 = totalUTimeMs; + mDetails.appCpuSTime3 = totalSTimeMs; + } else { + mDetails.appCpuUid3 = mDetails.appCpuUid2; + mDetails.appCpuUTime3 = mDetails.appCpuUTime2; + mDetails.appCpuSTime3 = mDetails.appCpuSTime2; + if (totalTimeMs <= (mDetails.appCpuUTime1 + mDetails.appCpuSTime1)) { + mDetails.appCpuUid2 = uid.mUid; + mDetails.appCpuUTime2 = totalUTimeMs; + mDetails.appCpuSTime2 = totalSTimeMs; + } else { + mDetails.appCpuUid2 = mDetails.appCpuUid1; + mDetails.appCpuUTime2 = mDetails.appCpuUTime1; + mDetails.appCpuSTime2 = mDetails.appCpuSTime1; + mDetails.appCpuUid1 = uid.mUid; + mDetails.appCpuUTime1 = totalUTimeMs; + mDetails.appCpuSTime1 = totalSTimeMs; + } + } + } + mLastStepCpuUserTimeMs = mCurStepCpuUserTimeMs; + mLastStepCpuSystemTimeMs = mCurStepCpuSystemTimeMs; + mLastStepStatUserTimeMs = mCurStepStatUserTimeMs; + mLastStepStatSystemTimeMs = mCurStepStatSystemTimeMs; + mLastStepStatIOWaitTimeMs = mCurStepStatIOWaitTimeMs; + mLastStepStatIrqTimeMs = mCurStepStatIrqTimeMs; + mLastStepStatSoftIrqTimeMs = mCurStepStatSoftIrqTimeMs; + mLastStepStatIdleTimeMs = mCurStepStatIdleTimeMs; + } + + public void addCpuStats(int totalUTimeMs, int totalSTimeMs, int statUserTimeMs, + int statSystemTimeMs, int statIOWaitTimeMs, int statIrqTimeMs, + int statSoftIrqTimeMs, int statIdleTimeMs) { + if (DEBUG) { + Slog.d(TAG, "Adding cpu: tuser=" + totalUTimeMs + " tsys=" + totalSTimeMs + + " user=" + statUserTimeMs + " sys=" + statSystemTimeMs + + " io=" + statIOWaitTimeMs + " irq=" + statIrqTimeMs + + " sirq=" + statSoftIrqTimeMs + " idle=" + statIdleTimeMs); + } + mCurStepCpuUserTimeMs += totalUTimeMs; + mCurStepCpuSystemTimeMs += totalSTimeMs; + mCurStepStatUserTimeMs += statUserTimeMs; + mCurStepStatSystemTimeMs += statSystemTimeMs; + mCurStepStatIOWaitTimeMs += statIOWaitTimeMs; + mCurStepStatIrqTimeMs += statIrqTimeMs; + mCurStepStatSoftIrqTimeMs += statSoftIrqTimeMs; + mCurStepStatIdleTimeMs += statIdleTimeMs; + } + + public void finishAddingCpuLocked() { + mHasHistoryStepDetails = true; + } + + public void reset() { + mHasHistoryStepDetails = false; + mLastStepCpuUserTimeMs = mCurStepCpuUserTimeMs = 0; + mLastStepCpuSystemTimeMs = mCurStepCpuSystemTimeMs = 0; + mLastStepStatUserTimeMs = mCurStepStatUserTimeMs = 0; + mLastStepStatSystemTimeMs = mCurStepStatSystemTimeMs = 0; + mLastStepStatIOWaitTimeMs = mCurStepStatIOWaitTimeMs = 0; + mLastStepStatIrqTimeMs = mCurStepStatIrqTimeMs = 0; + mLastStepStatSoftIrqTimeMs = mCurStepStatSoftIrqTimeMs = 0; + mLastStepStatIdleTimeMs = mCurStepStatIdleTimeMs = 0; + } + + private void updateStateResidency() { + mDetails.statSubsystemPowerState = null; + + if (mPowerStatsInternal == null || mEntityNames.isEmpty() || mStateNames.isEmpty()) { + return; + } + + final StateResidencyResult[] results; + try { + results = mPowerStatsInternal.getStateResidencyAsync(new int[0]) + .get(POWER_STATS_QUERY_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + } catch (Exception e) { + Slog.e(TAG, "Failed to getStateResidencyAsync", e); + return; + } + + if (results == null || results.length == 0) { + return; + } + + StringBuilder builder = new StringBuilder("SubsystemPowerState"); + for (int i = 0; i < results.length; i++) { + final StateResidencyResult result = results[i]; + int length = builder.length(); + builder.append(" subsystem_").append(i); + builder.append(" name=").append(mEntityNames.get(result.id)); + + for (int j = 0; j < result.stateResidencyData.length; j++) { + final StateResidency stateResidency = result.stateResidencyData[j]; + builder.append(" state_").append(j); + builder.append(" name=").append(mStateNames.get(result.id).get( + stateResidency.id)); + builder.append(" time=").append(stateResidency.totalTimeInStateMs); + builder.append(" count=").append(stateResidency.totalStateEntryCount); + builder.append(" last entry=").append(stateResidency.lastEntryTimestampMs); + } + + if (builder.length() > MAX_LOW_POWER_STATS_SIZE) { + Slog.e(TAG, "updateStateResidency: buffer not enough"); + builder.setLength(length); + break; + } + } + + mDetails.statSubsystemPowerState = builder.toString(); + } + + private void populatePowerEntityMaps() { + PowerEntity[] entities = mPowerStatsInternal.getPowerEntityInfo(); + if (entities == null) { + return; + } + + for (final PowerEntity entity : entities) { + Map<Integer, String> states = new HashMap<>(); + for (int j = 0; j < entity.states.length; j++) { + final State state = entity.states[j]; + states.put(state.id, state.name); + } + + mEntityNames.put(entity.id, entity.name); + mStateNames.put(entity.id, states); + } + } +} diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index 2cf6b7efcb48..0af50805d756 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -117,7 +117,6 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.BackgroundThread; import com.android.internal.os.BatteryStatsHistory; -import com.android.internal.os.BatteryStatsHistory.HistoryStepDetailsCalculator; import com.android.internal.os.BatteryStatsHistoryIterator; import com.android.internal.os.BinderCallsStats; import com.android.internal.os.BinderTransactionNameResolver; @@ -653,7 +652,6 @@ public class BatteryStatsImpl extends BatteryStats { public interface PlatformIdleStateCallback { public void fillLowPowerStats(RpmStats rpmStats); - public String getSubsystemLowPowerStats(); } /** interface to update rail information for power monitor */ @@ -1065,10 +1063,6 @@ public class BatteryStatsImpl extends BatteryStats { */ void cancelCpuSyncDueToWakelockChange(); - /** - * Schedules a sync caused by the battery level change - */ - void scheduleSyncDueToBatteryLevelChange(long delayMillis); /** Schedule removal of UIDs corresponding to a removed user */ void scheduleCleanupDueToRemovedUser(int userId); /** Schedule a sync because of a process state change */ @@ -1131,8 +1125,8 @@ public class BatteryStatsImpl extends BatteryStats { private boolean mShuttingDown; private final HistoryEventTracker mActiveEvents = new HistoryEventTracker(); - private final HistoryStepDetailsCalculatorImpl mStepDetailsCalculator = - new HistoryStepDetailsCalculatorImpl(); + private final BatteryHistoryStepDetailsProvider mStepDetailsProvider = + new BatteryHistoryStepDetailsProvider(this); private boolean mHaveBatteryLevel = false; private boolean mBatteryPluggedIn; @@ -4553,184 +4547,6 @@ public class BatteryStatsImpl extends BatteryStats { return kmt; } - private class HistoryStepDetailsCalculatorImpl implements HistoryStepDetailsCalculator { - private final HistoryStepDetails mDetails = new HistoryStepDetails(); - - private boolean mHasHistoryStepDetails; - private boolean mUpdateRequested; - - /** - * Total time (in milliseconds) spent executing in user code. - */ - private long mLastStepCpuUserTimeMs; - private long mCurStepCpuUserTimeMs; - /** - * Total time (in milliseconds) spent executing in kernel code. - */ - private long mLastStepCpuSystemTimeMs; - private long mCurStepCpuSystemTimeMs; - /** - * Times from /proc/stat (but measured in milliseconds). - */ - private long mLastStepStatUserTimeMs; - private long mLastStepStatSystemTimeMs; - private long mLastStepStatIOWaitTimeMs; - private long mLastStepStatIrqTimeMs; - private long mLastStepStatSoftIrqTimeMs; - private long mLastStepStatIdleTimeMs; - private long mCurStepStatUserTimeMs; - private long mCurStepStatSystemTimeMs; - private long mCurStepStatIOWaitTimeMs; - private long mCurStepStatIrqTimeMs; - private long mCurStepStatSoftIrqTimeMs; - private long mCurStepStatIdleTimeMs; - - @Override - public HistoryStepDetails getHistoryStepDetails() { - if (!mUpdateRequested) { - mUpdateRequested = true; - // Perform a CPU update right after we do this collection, so we have started - // collecting good data for the next step. - requestImmediateCpuUpdate(); - - if (mPlatformIdleStateCallback != null) { - mDetails.statSubsystemPowerState = - mPlatformIdleStateCallback.getSubsystemLowPowerStats(); - if (DEBUG) { - Slog.i(TAG, - "WRITE SubsystemPowerState:" + mDetails.statSubsystemPowerState); - } - } - } - - if (!mHasHistoryStepDetails) { - // We are not generating a delta, so all we need to do is reset the stats - // we will later be doing a delta from. - final int uidCount = mUidStats.size(); - for (int i = 0; i < uidCount; i++) { - final BatteryStatsImpl.Uid uid = mUidStats.valueAt(i); - uid.mLastStepUserTimeMs = uid.mCurStepUserTimeMs; - uid.mLastStepSystemTimeMs = uid.mCurStepSystemTimeMs; - } - mLastStepCpuUserTimeMs = mCurStepCpuUserTimeMs; - mLastStepCpuSystemTimeMs = mCurStepCpuSystemTimeMs; - mLastStepStatUserTimeMs = mCurStepStatUserTimeMs; - mLastStepStatSystemTimeMs = mCurStepStatSystemTimeMs; - mLastStepStatIOWaitTimeMs = mCurStepStatIOWaitTimeMs; - mLastStepStatIrqTimeMs = mCurStepStatIrqTimeMs; - mLastStepStatSoftIrqTimeMs = mCurStepStatSoftIrqTimeMs; - mLastStepStatIdleTimeMs = mCurStepStatIdleTimeMs; - return null; - } else { - if (DEBUG) { - Slog.d(TAG, "Step stats last: user=" + mLastStepCpuUserTimeMs + " sys=" - + mLastStepStatSystemTimeMs + " io=" + mLastStepStatIOWaitTimeMs - + " irq=" + mLastStepStatIrqTimeMs + " sirq=" - + mLastStepStatSoftIrqTimeMs + " idle=" + mLastStepStatIdleTimeMs); - Slog.d(TAG, "Step stats cur: user=" + mCurStepCpuUserTimeMs + " sys=" - + mCurStepStatSystemTimeMs + " io=" + mCurStepStatIOWaitTimeMs - + " irq=" + mCurStepStatIrqTimeMs + " sirq=" - + mCurStepStatSoftIrqTimeMs + " idle=" + mCurStepStatIdleTimeMs); - } - mDetails.userTime = (int) (mCurStepCpuUserTimeMs - mLastStepCpuUserTimeMs); - mDetails.systemTime = (int) (mCurStepCpuSystemTimeMs - mLastStepCpuSystemTimeMs); - mDetails.statUserTime = (int) (mCurStepStatUserTimeMs - mLastStepStatUserTimeMs); - mDetails.statSystemTime = - (int) (mCurStepStatSystemTimeMs - mLastStepStatSystemTimeMs); - mDetails.statIOWaitTime = - (int) (mCurStepStatIOWaitTimeMs - mLastStepStatIOWaitTimeMs); - mDetails.statIrqTime = (int) (mCurStepStatIrqTimeMs - mLastStepStatIrqTimeMs); - mDetails.statSoftIrqTime = - (int) (mCurStepStatSoftIrqTimeMs - mLastStepStatSoftIrqTimeMs); - mDetails.statIdlTime = (int) (mCurStepStatIdleTimeMs - mLastStepStatIdleTimeMs); - mDetails.appCpuUid1 = mDetails.appCpuUid2 = mDetails.appCpuUid3 = -1; - mDetails.appCpuUTime1 = mDetails.appCpuUTime2 = mDetails.appCpuUTime3 = 0; - mDetails.appCpuSTime1 = mDetails.appCpuSTime2 = mDetails.appCpuSTime3 = 0; - final int uidCount = mUidStats.size(); - for (int i = 0; i < uidCount; i++) { - final BatteryStatsImpl.Uid uid = mUidStats.valueAt(i); - final int totalUTimeMs = - (int) (uid.mCurStepUserTimeMs - uid.mLastStepUserTimeMs); - final int totalSTimeMs = - (int) (uid.mCurStepSystemTimeMs - uid.mLastStepSystemTimeMs); - final int totalTimeMs = totalUTimeMs + totalSTimeMs; - uid.mLastStepUserTimeMs = uid.mCurStepUserTimeMs; - uid.mLastStepSystemTimeMs = uid.mCurStepSystemTimeMs; - if (totalTimeMs <= (mDetails.appCpuUTime3 + mDetails.appCpuSTime3)) { - continue; - } - if (totalTimeMs <= (mDetails.appCpuUTime2 + mDetails.appCpuSTime2)) { - mDetails.appCpuUid3 = uid.mUid; - mDetails.appCpuUTime3 = totalUTimeMs; - mDetails.appCpuSTime3 = totalSTimeMs; - } else { - mDetails.appCpuUid3 = mDetails.appCpuUid2; - mDetails.appCpuUTime3 = mDetails.appCpuUTime2; - mDetails.appCpuSTime3 = mDetails.appCpuSTime2; - if (totalTimeMs <= (mDetails.appCpuUTime1 + mDetails.appCpuSTime1)) { - mDetails.appCpuUid2 = uid.mUid; - mDetails.appCpuUTime2 = totalUTimeMs; - mDetails.appCpuSTime2 = totalSTimeMs; - } else { - mDetails.appCpuUid2 = mDetails.appCpuUid1; - mDetails.appCpuUTime2 = mDetails.appCpuUTime1; - mDetails.appCpuSTime2 = mDetails.appCpuSTime1; - mDetails.appCpuUid1 = uid.mUid; - mDetails.appCpuUTime1 = totalUTimeMs; - mDetails.appCpuSTime1 = totalSTimeMs; - } - } - } - mLastStepCpuUserTimeMs = mCurStepCpuUserTimeMs; - mLastStepCpuSystemTimeMs = mCurStepCpuSystemTimeMs; - mLastStepStatUserTimeMs = mCurStepStatUserTimeMs; - mLastStepStatSystemTimeMs = mCurStepStatSystemTimeMs; - mLastStepStatIOWaitTimeMs = mCurStepStatIOWaitTimeMs; - mLastStepStatIrqTimeMs = mCurStepStatIrqTimeMs; - mLastStepStatSoftIrqTimeMs = mCurStepStatSoftIrqTimeMs; - mLastStepStatIdleTimeMs = mCurStepStatIdleTimeMs; - return mDetails; - } - } - - public void addCpuStats(int totalUTimeMs, int totalSTimeMs, int statUserTimeMs, - int statSystemTimeMs, int statIOWaitTimeMs, int statIrqTimeMs, - int statSoftIrqTimeMs, int statIdleTimeMs) { - if (DEBUG) { - Slog.d(TAG, "Adding cpu: tuser=" + totalUTimeMs + " tsys=" + totalSTimeMs - + " user=" + statUserTimeMs + " sys=" + statSystemTimeMs - + " io=" + statIOWaitTimeMs + " irq=" + statIrqTimeMs - + " sirq=" + statSoftIrqTimeMs + " idle=" + statIdleTimeMs); - } - mCurStepCpuUserTimeMs += totalUTimeMs; - mCurStepCpuSystemTimeMs += totalSTimeMs; - mCurStepStatUserTimeMs += statUserTimeMs; - mCurStepStatSystemTimeMs += statSystemTimeMs; - mCurStepStatIOWaitTimeMs += statIOWaitTimeMs; - mCurStepStatIrqTimeMs += statIrqTimeMs; - mCurStepStatSoftIrqTimeMs += statSoftIrqTimeMs; - mCurStepStatIdleTimeMs += statIdleTimeMs; - } - - public void finishAddingCpuLocked() { - mHasHistoryStepDetails = true; - mUpdateRequested = false; - } - - @Override - public void clear() { - mHasHistoryStepDetails = false; - mLastStepCpuUserTimeMs = mCurStepCpuUserTimeMs = 0; - mLastStepCpuSystemTimeMs = mCurStepCpuSystemTimeMs = 0; - mLastStepStatUserTimeMs = mCurStepStatUserTimeMs = 0; - mLastStepStatSystemTimeMs = mCurStepStatSystemTimeMs = 0; - mLastStepStatIOWaitTimeMs = mCurStepStatIOWaitTimeMs = 0; - mLastStepStatIrqTimeMs = mCurStepStatIrqTimeMs = 0; - mLastStepStatSoftIrqTimeMs = mCurStepStatSoftIrqTimeMs = 0; - mLastStepStatIdleTimeMs = mCurStepStatIdleTimeMs = 0; - } - } - @GuardedBy("this") @Override public void commitCurrentHistoryBatchLocked() { @@ -5557,7 +5373,7 @@ public class BatteryStatsImpl extends BatteryStats { public void addCpuStatsLocked(int totalUTimeMs, int totalSTimeMs, int statUserTimeMs, int statSystemTimeMs, int statIOWaitTimeMs, int statIrqTimeMs, int statSoftIrqTimeMs, int statIdleTimeMs) { - mStepDetailsCalculator.addCpuStats(totalUTimeMs, totalSTimeMs, statUserTimeMs, + mStepDetailsProvider.addCpuStats(totalUTimeMs, totalSTimeMs, statUserTimeMs, statSystemTimeMs, statIOWaitTimeMs, statIrqTimeMs, statSoftIrqTimeMs, statIdleTimeMs); } @@ -5567,7 +5383,7 @@ public class BatteryStatsImpl extends BatteryStats { */ @GuardedBy("this") public void finishAddingCpuStatsLocked() { - mStepDetailsCalculator.finishAddingCpuLocked(); + mStepDetailsProvider.finishAddingCpuLocked(); } public void noteProcessDiedLocked(int uid, int pid) { @@ -11515,8 +11331,7 @@ public class BatteryStatsImpl extends BatteryStats { mBatteryHistoryDirectory = batteryHistoryDirectory; mHistory = new BatteryStatsHistory(null /* historyBuffer */, mConstants.MAX_HISTORY_BUFFER, - mBatteryHistoryDirectory, mStepDetailsCalculator, mClock, mMonotonicClock, - traceDelegate, eventLogger); + mBatteryHistoryDirectory, mClock, mMonotonicClock, traceDelegate, eventLogger); mCpuPowerStatsCollector = new CpuPowerStatsCollector(mPowerStatsCollectorInjector); mCpuPowerStatsCollector.addConsumer(this::recordPowerStats); @@ -11723,6 +11538,12 @@ public class BatteryStatsImpl extends BatteryStats { mCallback = cb; } + void updateCpuDetails() { + if (mCallback != null) { + mCallback.batteryNeedsCpuUpdate(); + } + } + public void setRadioScanningTimeoutLocked(long timeoutUs) { if (mPhoneSignalScanningTimer != null) { mPhoneSignalScanningTimer.setTimeout(timeoutUs); @@ -12343,6 +12164,8 @@ public class BatteryStatsImpl extends BatteryStats { mWakeupReasonStats.clear(); } + mStepDetailsProvider.reset(); + if (mTmpRailStats != null) { mTmpRailStats.reset(); } @@ -14948,6 +14771,8 @@ public class BatteryStatsImpl extends BatteryStats { mCpuUidFreqTimeReader.onSystemReady(); } + mStepDetailsProvider.onSystemReady(); + mPowerStatsCollectorInjector.setContext(context); mCpuPowerStatsCollector.setEnabled( @@ -15270,6 +15095,7 @@ public class BatteryStatsImpl extends BatteryStats { reportChangesToStatsLog(status, plugType, level); + boolean requestStepDetails = false; final boolean onBattery = isOnBattery(plugType, status); if (!mHaveBatteryLevel) { mHaveBatteryLevel = true; @@ -15289,6 +15115,7 @@ public class BatteryStatsImpl extends BatteryStats { mMaxChargeStepLevel = mMinDischargeStepLevel = mLastChargeStepLevel = mLastDischargeStepLevel = level; + requestStepDetails = true; } else if (mBatteryLevel != level || mOnBattery != onBattery) { recordDailyStatsIfNeededLocked(level >= 100 && onBattery, currentTimeMs); } @@ -15332,16 +15159,13 @@ public class BatteryStatsImpl extends BatteryStats { } mBatteryChargeUah = chargeUah; setOnBatteryLocked(elapsedRealtimeMs, uptimeMs, onBattery, oldStatus, level, chargeUah); + requestStepDetails = true; } else { boolean changed = false; if (mBatteryLevel != level) { mBatteryLevel = level; changed = true; - - // TODO(adamlesinski): Schedule the creation of a HistoryStepDetails record - // which will pull external stats. - mExternalSync.scheduleSyncDueToBatteryLevelChange( - mConstants.BATTERY_LEVEL_COLLECTION_DELAY_MS); + requestStepDetails = true; } if (mBatteryStatus != status) { mBatteryStatus = status; @@ -15462,6 +15286,9 @@ public class BatteryStatsImpl extends BatteryStats { if (mAccumulateBatteryUsageStats) { mBatteryUsageStatsProvider.accumulateBatteryUsageStatsAsync(this, mHandler); } + if (requestStepDetails) { + mStepDetailsProvider.requestUpdate(); + } } public static boolean isOnBattery(int plugType, int status) { diff --git a/services/core/java/com/android/server/security/AttestationVerificationManagerService.java b/services/core/java/com/android/server/security/AttestationVerificationManagerService.java index 22a359bced86..c1d1e2ba3e76 100644 --- a/services/core/java/com/android/server/security/AttestationVerificationManagerService.java +++ b/services/core/java/com/android/server/security/AttestationVerificationManagerService.java @@ -99,11 +99,6 @@ public class AttestationVerificationManagerService extends SystemService { @Override protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer, @Nullable String[] args) { - if (!android.security.Flags.dumpAttestationVerifications()) { - super.dump(fd, writer, args); - return; - } - if (!DumpUtils.checkDumpAndUsageStatsPermission(getContext(), TAG, writer)) return; final IndentingPrintWriter fout = new IndentingPrintWriter(writer, " "); diff --git a/services/core/java/com/android/server/security/advancedprotection/AdvancedProtectionService.java b/services/core/java/com/android/server/security/advancedprotection/AdvancedProtectionService.java index 3ece07c84080..0735770f9c3c 100644 --- a/services/core/java/com/android/server/security/advancedprotection/AdvancedProtectionService.java +++ b/services/core/java/com/android/server/security/advancedprotection/AdvancedProtectionService.java @@ -27,6 +27,7 @@ import android.annotation.Nullable; import android.app.StatsManager; import android.content.Context; import android.content.SharedPreferences; +import android.content.pm.UserInfo; import android.os.Binder; import android.os.Environment; import android.os.Handler; @@ -73,7 +74,7 @@ import java.util.List; import java.util.Set; /** @hide */ -public class AdvancedProtectionService extends IAdvancedProtectionService.Stub { +public class AdvancedProtectionService extends IAdvancedProtectionService.Stub { private static final String TAG = "AdvancedProtectionService"; private static final int MODE_CHANGED = 0; private static final int CALLBACK_ADDED = 1; @@ -90,6 +91,7 @@ public class AdvancedProtectionService extends IAdvancedProtectionService.Stub private final Context mContext; private final Handler mHandler; private final AdvancedProtectionStore mStore; + private final UserManagerInternal mUserManager; // Features living with the service - their code will be executed when state changes private final ArrayList<AdvancedProtectionHook> mHooks = new ArrayList<>(); @@ -106,7 +108,8 @@ public class AdvancedProtectionService extends IAdvancedProtectionService.Stub super(PermissionEnforcer.fromContext(context)); mContext = context; mHandler = new AdvancedProtectionHandler(FgThread.get().getLooper()); - mStore = new AdvancedProtectionStore(context); + mStore = new AdvancedProtectionStore(mContext); + mUserManager = LocalServices.getService(UserManagerInternal.class); } private void initFeatures(boolean enabled) { @@ -156,12 +159,18 @@ public class AdvancedProtectionService extends IAdvancedProtectionService.Stub // Only for tests @VisibleForTesting - AdvancedProtectionService(@NonNull Context context, @NonNull AdvancedProtectionStore store, - @NonNull Looper looper, @NonNull PermissionEnforcer permissionEnforcer, - @Nullable AdvancedProtectionHook hook, @Nullable AdvancedProtectionProvider provider) { + AdvancedProtectionService( + @NonNull Context context, + @NonNull AdvancedProtectionStore store, + @NonNull UserManagerInternal userManager, + @NonNull Looper looper, + @NonNull PermissionEnforcer permissionEnforcer, + @Nullable AdvancedProtectionHook hook, + @Nullable AdvancedProtectionProvider provider) { super(permissionEnforcer); mContext = context; mStore = store; + mUserManager = userManager; mHandler = new AdvancedProtectionHandler(looper); if (hook != null) { mHooks.add(hook); @@ -218,8 +227,10 @@ public class AdvancedProtectionService extends IAdvancedProtectionService.Stub @EnforcePermission(Manifest.permission.MANAGE_ADVANCED_PROTECTION_MODE) public void setAdvancedProtectionEnabled(boolean enabled) { setAdvancedProtectionEnabled_enforcePermission(); + final UserHandle user = Binder.getCallingUserHandle(); final long identity = Binder.clearCallingIdentity(); try { + enforceAdminUser(user); synchronized (mCallbacks) { if (enabled != isAdvancedProtectionEnabledInternal()) { mStore.storeAdvancedProtectionModeEnabled(enabled); @@ -364,6 +375,13 @@ public class AdvancedProtectionService extends IAdvancedProtectionService.Stub return features; } + private void enforceAdminUser(UserHandle user) { + UserInfo info = mUserManager.getUserInfo(user.getIdentifier()); + if (!info.isAdmin()) { + throw new SecurityException("Only an admin user can manage advanced protection mode"); + } + } + @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, @NonNull String[] args, ShellCallback callback, @@ -451,36 +469,34 @@ public class AdvancedProtectionService extends IAdvancedProtectionService.Stub @VisibleForTesting static class AdvancedProtectionStore { - private final Context mContext; static final int ON = 1; static final int OFF = 0; - private final UserManagerInternal mUserManager; + private final Context mContext; AdvancedProtectionStore(@NonNull Context context) { mContext = context; - mUserManager = LocalServices.getService(UserManagerInternal.class); } void storeAdvancedProtectionModeEnabled(boolean enabled) { Settings.Secure.putIntForUser(mContext.getContentResolver(), ADVANCED_PROTECTION_MODE, enabled ? ON : OFF, - mUserManager.getMainUserId()); + UserHandle.USER_SYSTEM); } boolean retrieveAdvancedProtectionModeEnabled() { return Settings.Secure.getIntForUser(mContext.getContentResolver(), - ADVANCED_PROTECTION_MODE, OFF, mUserManager.getMainUserId()) == ON; + ADVANCED_PROTECTION_MODE, OFF, UserHandle.USER_SYSTEM) == ON; } void storeInt(String key, int value) { Settings.Secure.putIntForUser(mContext.getContentResolver(), key, value, - mUserManager.getMainUserId()); + UserHandle.USER_SYSTEM); } int retrieveInt(String key, int defaultValue) { return Settings.Secure.getIntForUser(mContext.getContentResolver(), - key, defaultValue, mUserManager.getMainUserId()); + key, defaultValue, UserHandle.USER_SYSTEM); } } @@ -492,12 +508,12 @@ public class AdvancedProtectionService extends IAdvancedProtectionService.Stub @Override public void handleMessage(@NonNull Message msg) { switch (msg.what) { - //arg1 == enabled + // arg1 == enabled case MODE_CHANGED: handleAllCallbacks(msg.arg1 == 1); break; - //arg1 == enabled - //obj == callback + // arg1 == enabled + // obj == callback case CALLBACK_ADDED: handleSingleCallback(msg.arg1 == 1, (IAdvancedProtectionCallback) msg.obj); break; @@ -546,7 +562,7 @@ public class AdvancedProtectionService extends IAdvancedProtectionService.Stub private final class DeathRecipient implements IBinder.DeathRecipient { private final IBinder mBinder; - DeathRecipient(IBinder binder) { + DeathRecipient(IBinder binder) { mBinder = binder; } diff --git a/services/core/java/com/android/server/theming/ThemeSettingsManager.java b/services/core/java/com/android/server/theming/ThemeSettingsManager.java new file mode 100644 index 000000000000..94094a6f9603 --- /dev/null +++ b/services/core/java/com/android/server/theming/ThemeSettingsManager.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.theming; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.content.ContentResolver; +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsField; +import android.content.theming.ThemeSettingsUpdater; +import android.provider.Settings; +import android.telecom.Log; + +import com.android.internal.util.Preconditions; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Iterator; + +/** + * Manages the loading and saving of theme settings. This class handles the persistence of theme + * settings to and from the system settings. It utilizes a collection of {@link ThemeSettingsField} + * objects to represent individual theme setting fields. + * + * @hide + */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +class ThemeSettingsManager { + private static final String TAG = ThemeSettingsManager.class.getSimpleName(); + static final String TIMESTAMP_FIELD = "_applied_timestamp"; + private final ThemeSettingsField<?, ?>[] mFields; + private final ThemeSettings mDefaults; + + /** + * Constructs a new {@code ThemeSettingsManager} with the specified default settings. + * + * @param defaults The default theme settings to use. + */ + ThemeSettingsManager(ThemeSettings defaults) { + mDefaults = defaults; + mFields = ThemeSettingsField.getFields(defaults); + } + + /** + * Loads the theme settings for the specified user. + * + * @param userId The ID of the user. + * @param contentResolver The content resolver to use. + * @return The loaded {@link ThemeSettings}. + */ + @NonNull + ThemeSettings loadSettings(@UserIdInt int userId, ContentResolver contentResolver) { + String jsonString = Settings.Secure.getStringForUser(contentResolver, + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, userId); + + JSONObject userSettings; + + try { + userSettings = new JSONObject(jsonString == null ? "" : jsonString); + } catch (JSONException e) { + userSettings = new JSONObject(); + } + + ThemeSettingsUpdater updater = ThemeSettings.updater(); + + for (ThemeSettingsField<?, ?> field : mFields) { + field.fromJSON(userSettings, updater); + } + + return updater.toThemeSettings(mDefaults); + } + + /** + * Saves the specified theme settings for the given user. + * + * @param userId The ID of the user. + * @param contentResolver The content resolver to use. + * @param newSettings The {@link ThemeSettings} to save. + */ + void replaceSettings(@UserIdInt int userId, ContentResolver contentResolver, + ThemeSettings newSettings) throws RuntimeException { + Preconditions.checkArgument(newSettings != null, "Impossible to write empty settings"); + + JSONObject jsonSettings = new JSONObject(); + + + for (ThemeSettingsField<?, ?> field : mFields) { + field.toJSON(newSettings, jsonSettings); + } + + // user defined timestamp should be ignored. Storing new timestamp. + try { + jsonSettings.put(TIMESTAMP_FIELD, System.currentTimeMillis()); + } catch (JSONException e) { + Log.w(TAG, "Error saving timestamp: " + e.getMessage()); + } + + String jsonString = jsonSettings.toString(); + + Settings.Secure.putStringForUser(contentResolver, + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, jsonString, userId); + } + + /** + * Saves the specified theme settings for the given user, while preserving unrelated existing + * properties. + * + * @param userId The ID of the user. + * @param contentResolver The content resolver to use. + * @param newSettings The {@link ThemeSettings} to save. + */ + void updateSettings(@UserIdInt int userId, ContentResolver contentResolver, + ThemeSettings newSettings) throws JSONException, RuntimeException { + Preconditions.checkArgument(newSettings != null, "Impossible to write empty settings"); + + String existingJsonString = Settings.Secure.getStringForUser(contentResolver, + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, userId); + + JSONObject existingJson; + try { + existingJson = new JSONObject(existingJsonString == null ? "{}" : existingJsonString); + } catch (JSONException e) { + existingJson = new JSONObject(); + } + + JSONObject newJson = new JSONObject(); + for (ThemeSettingsField<?, ?> field : mFields) { + field.toJSON(newSettings, newJson); + } + + // user defined timestamp should be ignored. Storing new timestamp. + try { + newJson.put(TIMESTAMP_FIELD, System.currentTimeMillis()); + } catch (JSONException e) { + Log.w(TAG, "Error saving timestamp: " + e.getMessage()); + } + + // Merge the new settings with the existing settings + Iterator<String> keys = newJson.keys(); + while (keys.hasNext()) { + String key = keys.next(); + existingJson.put(key, newJson.get(key)); + } + + String mergedJsonString = existingJson.toString(); + + Settings.Secure.putStringForUser(contentResolver, + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, mergedJsonString, userId); + } +} diff --git a/services/core/java/com/android/server/tv/TvInputHal.java b/services/core/java/com/android/server/tv/TvInputHal.java index 87ebdbfbf4e8..8d02f2fc4f8b 100644 --- a/services/core/java/com/android/server/tv/TvInputHal.java +++ b/services/core/java/com/android/server/tv/TvInputHal.java @@ -68,6 +68,9 @@ final class TvInputHal implements Handler.Callback { private static native int nativeSetTvMessageEnabled(long ptr, int deviceId, int streamId, int type, boolean enabled); + private static native int nativeSetPictureProfile( + long ptr, int deviceId, int streamId, long profileHandle); + private final Object mLock = new Object(); private long mPtr = 0; private final Callback mCallback; @@ -122,6 +125,24 @@ final class TvInputHal implements Handler.Callback { } } + public int setPictureProfile(int deviceId, TvStreamConfig streamConfig, long profileHandle) { + synchronized (mLock) { + if (mPtr == 0) { + return ERROR_NO_INIT; + } + int generation = mStreamConfigGenerations.get(deviceId, 0); + if (generation != streamConfig.getGeneration()) { + return ERROR_STALE_CONFIG; + } + if (nativeSetPictureProfile(mPtr, deviceId, streamConfig.getStreamId(), profileHandle) + == 0) { + return SUCCESS; + } else { + return ERROR_UNKNOWN; + } + } + } + public int removeStream(int deviceId, TvStreamConfig streamConfig) { synchronized (mLock) { if (mPtr == 0) { diff --git a/services/core/java/com/android/server/tv/TvInputHardwareManager.java b/services/core/java/com/android/server/tv/TvInputHardwareManager.java index 92b57645b9a3..d3e3257fe384 100644 --- a/services/core/java/com/android/server/tv/TvInputHardwareManager.java +++ b/services/core/java/com/android/server/tv/TvInputHardwareManager.java @@ -596,6 +596,26 @@ class TvInputHardwareManager implements TvInputHal.Callback { } } + public boolean setPictureProfile(String inputId, long profileHandle) { + synchronized (mLock) { + int deviceId = findDeviceIdForInputIdLocked(inputId); + if (deviceId < 0) { + Slog.e(TAG, "Invalid inputId : " + inputId); + return false; + } + + Connection connection = mConnections.get(deviceId); + boolean success = true; + for (TvStreamConfig config : connection.getConfigsLocked()) { + success = success + && mHal.setPictureProfile(deviceId, config, profileHandle) + == TvInputHal.SUCCESS; + } + + return success; + } + } + /** * Take a snapshot of the given TV input into the provided Surface. */ @@ -844,7 +864,6 @@ class TvInputHardwareManager implements TvInputHal.Callback { return mCallback; } - @GuardedBy("mLock") public TvStreamConfig[] getConfigsLocked() { return mConfigs; } diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java index 47d6879129ee..3c3bfeb75c85 100644 --- a/services/core/java/com/android/server/tv/TvInputManagerService.java +++ b/services/core/java/com/android/server/tv/TvInputManagerService.java @@ -51,6 +51,7 @@ import android.hardware.hdmi.HdmiDeviceInfo; import android.hardware.hdmi.HdmiTvClient; import android.media.AudioPresentation; import android.media.PlaybackParams; +import android.media.quality.MediaQualityManager; import android.media.tv.AdBuffer; import android.media.tv.AdRequest; import android.media.tv.AdResponse; @@ -193,6 +194,8 @@ public final class TvInputManagerService extends SystemService { private HdmiControlManager mHdmiControlManager = null; private HdmiTvClient mHdmiTvClient = null; + private MediaQualityManager mMediaQualityManager = null; + public TvInputManagerService(Context context) { super(context); @@ -919,7 +922,7 @@ public final class TvInputManagerService extends SystemService { sendSessionTokenToClientLocked(sessionState.client, sessionState.inputId, null, null, sessionState.seq); } - if (!serviceState.isHardware) { + if (!serviceState.isHardware || serviceState.reconnecting) { updateServiceConnectionLocked(serviceState.component, userId); } else { updateHardwareServiceConnectionDelayed(userId); @@ -3702,10 +3705,24 @@ public final class TvInputManagerService extends SystemService { TvInputInfo inputInfo, ComponentName component, int userId) { ServiceState serviceState = getServiceStateLocked(component, userId); serviceState.hardwareInputMap.put(inputInfo.getId(), inputInfo); + setPictureProfileLocked(inputInfo.getId()); buildTvInputListLocked(userId, null); } @GuardedBy("mLock") + private void setPictureProfileLocked(String inputId) { + if (mMediaQualityManager == null) { + mMediaQualityManager = (MediaQualityManager) getContext() + .getSystemService(Context.MEDIA_QUALITY_SERVICE); + if (mMediaQualityManager == null) { + return; + } + } + long profileHandle = mMediaQualityManager.getPictureProfileForTvInput(inputId); + mTvInputHardwareManager.setPictureProfile(inputId, profileHandle); + } + + @GuardedBy("mLock") private void removeHardwareInputLocked(String inputId, int userId) { if (!mTvInputHardwareManager.getInputMap().containsKey(inputId)) { return; diff --git a/services/core/java/com/android/server/updates/CertPinInstallReceiver.java b/services/core/java/com/android/server/updates/CertPinInstallReceiver.java index 250e99b47b1a..c8e7a8dea5c3 100644 --- a/services/core/java/com/android/server/updates/CertPinInstallReceiver.java +++ b/services/core/java/com/android/server/updates/CertPinInstallReceiver.java @@ -19,7 +19,10 @@ package com.android.server.updates; import android.content.Context; import android.content.Intent; +import java.io.File; + public class CertPinInstallReceiver extends ConfigUpdateInstallReceiver { + private static final String KEYCHAIN_DIR = "/data/misc/keychain/"; public CertPinInstallReceiver() { super("/data/misc/keychain/", "pins", "metadata/", "version"); @@ -27,7 +30,22 @@ public class CertPinInstallReceiver extends ConfigUpdateInstallReceiver { @Override public void onReceive(final Context context, final Intent intent) { - if (!com.android.server.flags.Flags.certpininstallerRemoval()) { + if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { + if (com.android.server.flags.Flags.certpininstallerRemoval()) { + File pins = new File(KEYCHAIN_DIR + "pins"); + if (pins.exists()) { + pins.delete(); + } + File version = new File(KEYCHAIN_DIR + "metadata/version"); + if (version.exists()) { + version.delete(); + } + File metadata = new File(KEYCHAIN_DIR + "metadata"); + if (metadata.exists()) { + metadata.delete(); + } + } + } else if (!com.android.server.flags.Flags.certpininstallerRemoval()) { super.onReceive(context, intent); } } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index 274175aa71ba..a0979fa5ee7e 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -747,13 +747,22 @@ public class WallpaperManagerService extends IWallpaperManager.Stub if (connection == null) { return false; } + boolean isWallpaperDesktopExperienceEnabled = isDeviceEligibleForDesktopExperienceWallpaper( + mContext); + boolean isLiveWallpaperSupportedInDesktopExperience = + mContext.getResources().getBoolean( + R.bool.config_isLiveWallpaperSupportedInDesktopExperience); // Non image wallpaper. if (connection.mInfo != null) { + if (isWallpaperDesktopExperienceEnabled + && !isLiveWallpaperSupportedInDesktopExperience) { + return false; + } return connection.mInfo.supportsMultipleDisplays(); } // Image wallpaper - if (isDeviceEligibleForDesktopExperienceWallpaper(mContext)) { + if (isWallpaperDesktopExperienceEnabled) { return mWallpaperCropper.isWallpaperCompatibleForDisplay(displayId, connection.mWallpaper); } diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index 7da4beb95114..fd4e38e9813d 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -24,10 +24,12 @@ import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.ActivityTaskManager.INVALID_WINDOWING_MODE; import static android.app.FullscreenRequestHandler.REMOTE_CALLBACK_RESULT_KEY; import static android.app.FullscreenRequestHandler.RESULT_APPROVED; +import static android.app.FullscreenRequestHandler.RESULT_FAILED_ALREADY_FULLY_EXPANDED; import static android.app.FullscreenRequestHandler.RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY; import static android.app.FullscreenRequestHandler.RESULT_FAILED_NOT_TOP_FOCUSED; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.content.pm.PackageManager.PERMISSION_DENIED; @@ -95,6 +97,7 @@ import android.os.UserHandle; import android.service.voice.VoiceInteractionManagerInternal; import android.util.Slog; import android.view.RemoteAnimationDefinition; +import android.window.DesktopModeFlags; import android.window.SizeConfigurationBuckets; import android.window.TransitionInfo; @@ -1188,17 +1191,25 @@ class ActivityClientController extends IActivityClientController.Stub { if (requesterActivity.getWindowingMode() == WINDOWING_MODE_PINNED) { return RESULT_APPROVED; } + final int taskWindowingMode = topFocusedRootTask.getWindowingMode(); // If this is not coming from the currently top-most activity, reject the request. if (requesterActivity != topFocusedRootTask.getTopMostActivity()) { return RESULT_FAILED_NOT_TOP_FOCUSED; } if (fullscreenRequest == FULLSCREEN_MODE_REQUEST_EXIT) { - if (topFocusedRootTask.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) { + if (taskWindowingMode != WINDOWING_MODE_FULLSCREEN) { return RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY; } if (topFocusedRootTask.mMultiWindowRestoreWindowingMode == INVALID_WINDOWING_MODE) { return RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY; } + return RESULT_APPROVED; + } + + if (DesktopModeFlags.ENABLE_REQUEST_FULLSCREEN_BUGFIX.isTrue() + && (taskWindowingMode == WINDOWING_MODE_FULLSCREEN + || taskWindowingMode == WINDOWING_MODE_MULTI_WINDOW)) { + return RESULT_FAILED_ALREADY_FULLY_EXPANDED; } return RESULT_APPROVED; } @@ -1281,7 +1292,7 @@ class ActivityClientController extends IActivityClientController.Stub { } } - private static void executeMultiWindowFullscreenRequest(int fullscreenRequest, Task requester) { + private void executeMultiWindowFullscreenRequest(int fullscreenRequest, Task requester) { final int targetWindowingMode; if (fullscreenRequest == FULLSCREEN_MODE_REQUEST_ENTER) { final int restoreWindowingMode = requester.getRequestedOverrideWindowingMode(); @@ -1294,7 +1305,13 @@ class ActivityClientController extends IActivityClientController.Stub { requester.getParent().mRemoteToken.toWindowContainerToken(); } else { targetWindowingMode = requester.mMultiWindowRestoreWindowingMode; - requester.restoreWindowingMode(); + if (DesktopModeFlags.ENABLE_REQUEST_FULLSCREEN_BUGFIX.isTrue() + && targetWindowingMode == WINDOWING_MODE_PINNED) { + final ActivityRecord r = requester.topRunningActivity(); + enterPictureInPictureMode(r.token, r.pictureInPictureArgs); + } else { + requester.restoreWindowingMode(); + } } if (targetWindowingMode == WINDOWING_MODE_FULLSCREEN) { requester.setBounds(null); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index b9ab863a2805..1d7247330b7a 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -98,7 +98,6 @@ import static android.os.Build.VERSION_CODES.HONEYCOMB; import static android.os.Build.VERSION_CODES.O; import static android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS; import static android.os.Process.SYSTEM_UID; -import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15; import static android.view.WindowManager.ENABLE_ACTIVITY_EMBEDDING_FOR_ANDROID_15; @@ -109,7 +108,6 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED; import static android.view.WindowManager.PROPERTY_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING_STATE_SHARING; -import static android.view.WindowManager.TRANSIT_OLD_UNSET; import static android.view.WindowManager.TRANSIT_RELAUNCH; import static android.view.WindowManager.hasWindowExtensionsEnabled; import static android.window.TransitionInfo.FLAGS_IS_OCCLUDED_NO_ANIMATION; @@ -117,9 +115,7 @@ import static android.window.TransitionInfo.FLAG_IS_OCCLUDED; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ADD_REMOVE; -import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ANIM; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_APP_TRANSITIONS; -import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_APP_TRANSITIONS_ANIM; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_CONFIGURATION; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_CONTAINERS; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_FOCUS; @@ -135,8 +131,6 @@ import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANG import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE; import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_LETTERBOXED; import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE; -import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_ANIM; -import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER; import static com.android.server.wm.ActivityRecord.State.DESTROYED; import static com.android.server.wm.ActivityRecord.State.DESTROYING; import static com.android.server.wm.ActivityRecord.State.FINISHING; @@ -364,7 +358,6 @@ import com.android.server.uri.GrantUri; import com.android.server.uri.NeededUriGrants; import com.android.server.uri.UriPermissionOwner; import com.android.server.wm.ActivityMetricsLogger.TransitionInfoSnapshot; -import com.android.server.wm.SurfaceAnimator.AnimationType; import com.android.server.wm.WindowManagerService.H; import com.android.window.flags.Flags; @@ -1434,7 +1427,7 @@ final class ActivityRecord extends WindowToken { // precede the configuration change from the resize.) mLastReportedPictureInPictureMode = inPictureInPictureMode; mLastReportedMultiWindowMode = inPictureInPictureMode; - if (!isPip2ExperimentEnabled()) { + if (forceUpdate || !isPip2ExperimentEnabled()) { // PiP2 should handle sending out the configuration as a part of Shell Transitions. ensureActivityConfiguration(true /* ignoreVisibility */); } @@ -6367,6 +6360,15 @@ final class ActivityRecord extends WindowToken { isSuccessful = false; } if (isSuccessful) { + final int lastReportedWinMode = mLastReportedConfiguration.getMergedConfiguration() + .windowConfiguration.getWindowingMode(); + if (isPip2ExperimentEnabled() + && lastReportedWinMode == WINDOWING_MODE_PINNED && !inPinnedWindowingMode()) { + // If an activity that was previously reported as pinned has a different windowing + // mode, then send the latest activity configuration even if this activity is + // stopping. This ensures that app gets onPictureInPictureModeChanged after onStop. + updatePictureInPictureMode(null /* targetRootTaskBounds */, true /* forceUpdate */); + } mAtmService.mH.postDelayed(mStopTimeoutRunnable, STOP_TIMEOUT); } else { // Just in case, assume it to be stopped. @@ -7199,40 +7201,6 @@ final class ActivityRecord extends WindowToken { return candidate; } - @Override - public SurfaceControl getAnimationLeashParent() { - // For transitions in the root pinned task (menu activity) we just let them occur as a child - // of the root pinned task. - // All normal app transitions take place in an animation layer which is below the root - // pinned task but may be above the parent tasks of the given animating apps by default. - // When a new hierarchical animation is enabled, we just let them occur as a child of the - // parent task, i.e. the hierarchy of the surfaces is unchanged. - if (inPinnedWindowingMode()) { - return getRootTask().getSurfaceControl(); - } else { - return super.getAnimationLeashParent(); - } - } - - @VisibleForTesting - boolean shouldAnimate() { - return task == null || task.shouldAnimate(); - } - - /** - * Creates a layer to apply crop to an animation. - */ - private SurfaceControl createAnimationBoundsLayer(Transaction t) { - ProtoLog.i(WM_DEBUG_APP_TRANSITIONS_ANIM, "Creating animation bounds layer"); - final SurfaceControl.Builder builder = makeAnimationLeash() - .setParent(getAnimationLeashParent()) - .setName(getSurfaceControl() + " - animation-bounds") - .setCallsite("ActivityRecord.createAnimationBoundsLayer"); - final SurfaceControl boundsLayer = builder.build(); - t.show(boundsLayer); - return boundsLayer; - } - boolean isTransitionForward() { return (mStartingData != null && mStartingData.mIsTransitionForward) || mDisplayContent.isNextTransitionForward(); @@ -7244,25 +7212,6 @@ final class ActivityRecord extends WindowToken { } @Override - public void onLeashAnimationStarting(Transaction t, SurfaceControl leash) { - // If the animation needs to be cropped then an animation bounds layer is created as a - // child of the root pinned task or animation layer. The leash is then reparented to this - // new layer. - if (mNeedsAnimationBoundsLayer) { - mTmpRect.setEmpty(); - task.getBounds(mTmpRect); - mAnimationBoundsLayer = createAnimationBoundsLayer(t); - - // Crop to root task bounds. - t.setLayer(leash, 0); - t.setLayer(mAnimationBoundsLayer, getLastLayer()); - - // Reparent leash to animation bounds layer. - t.reparent(leash, mAnimationBoundsLayer); - } - } - - @Override boolean showSurfaceOnCreation() { return false; } @@ -7301,74 +7250,6 @@ final class ActivityRecord extends WindowToken { return mLastSurfaceShowing; } - @Override - public void onAnimationLeashLost(Transaction t) { - super.onAnimationLeashLost(t); - if (mAnimationBoundsLayer != null) { - t.remove(mAnimationBoundsLayer); - mAnimationBoundsLayer = null; - } - - mNeedsAnimationBoundsLayer = false; - } - - @Override - protected void onAnimationFinished(@AnimationType int type, AnimationAdapter anim) { - super.onAnimationFinished(type, anim); - - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "AR#onAnimationFinished"); - mTransit = TRANSIT_OLD_UNSET; - mTransitFlags = 0; - - setAppLayoutChanges(FINISH_LAYOUT_REDO_ANIM | FINISH_LAYOUT_REDO_WALLPAPER, - "ActivityRecord"); - - setClientVisible(isVisible() || mVisibleRequested); - - getDisplayContent().computeImeTargetIfNeeded(this); - - ProtoLog.v(WM_DEBUG_ANIM, "Animation done in %s" - + ": reportedVisible=%b okToDisplay=%b okToAnimate=%b startingDisplayed=%b", - this, reportedVisible, okToDisplay(), okToAnimate(), - isStartingWindowDisplayed()); - - // WindowState.onExitAnimationDone might modify the children list, so make a copy and then - // traverse the copy. - final ArrayList<WindowState> children = new ArrayList<>(mChildren); - children.forEach(WindowState::onExitAnimationDone); - // The starting window could transfer to another activity after app transition started, in - // that case the latest top activity might not receive exit animation done callback if the - // starting window didn't applied exit animation success. Notify animation finish to the - // starting window if needed. - if (task != null && startingMoved) { - final WindowState transferredStarting = task.getWindow(w -> - w.mAttrs.type == TYPE_APPLICATION_STARTING); - if (transferredStarting != null && transferredStarting.mAnimatingExit - && !transferredStarting.isSelfAnimating(0 /* flags */, - ANIMATION_TYPE_WINDOW_ANIMATION)) { - transferredStarting.onExitAnimationDone(); - } - } - - scheduleAnimation(); - - // Schedule to handle the stopping and finishing activities which the animation is done - // because the activities which were animating have not been stopped yet. - mTaskSupervisor.scheduleProcessStoppingAndFinishingActivitiesIfNeeded(); - Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); - } - - void clearAnimatingFlags() { - boolean wallpaperMightChange = false; - for (int i = mChildren.size() - 1; i >= 0; i--) { - final WindowState win = mChildren.get(i); - wallpaperMightChange |= win.clearAnimatingFlags(); - } - if (wallpaperMightChange) { - requestUpdateWallpaperIfNeeded(); - } - } - public @TransitionOldType int getTransit() { return mTransit; } diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 0e14f83c96f8..05b109c0913e 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -78,6 +78,7 @@ import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLAS import static com.android.server.wm.ActivityTaskManagerService.ANIMATE; import static com.android.server.wm.ActivityTaskManagerService.H.FIRST_SUPERVISOR_TASK_MSG; import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_NONE; +import static com.android.server.wm.ActivityTaskManagerService.isPip2ExperimentEnabled; import static com.android.server.wm.ClientLifecycleManager.shouldDispatchLaunchActivityItemIndependently; import static com.android.server.wm.LockTaskController.LOCK_TASK_AUTH_ALLOWLISTED; import static com.android.server.wm.LockTaskController.LOCK_TASK_AUTH_LAUNCHABLE; @@ -1687,9 +1688,13 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { */ rootTask.cancelAnimation(); rootTask.setForceHidden(FLAG_FORCE_HIDDEN_FOR_PINNED_TASK, true /* set */); - rootTask.ensureActivitiesVisible(null /* starting */); - activityIdleInternal(null /* idleActivity */, false /* fromTimeout */, - true /* processPausingActivities */, null /* configuration */); + if (!isPip2ExperimentEnabled()) { + // In PiP2, as the transition finishes the lifecycle updates will be sent to the app + // along with the configuration changes as a part of the transition lifecycle. + rootTask.ensureActivitiesVisible(null /* starting */); + activityIdleInternal(null /* idleActivity */, false /* fromTimeout */, + true /* processPausingActivities */, null /* configuration */); + } if (rootTask.getParent() == null) { // The activities in the task may already be finishing. Then the task could be removed diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java index 03ba1a51ad7b..61e8e09bc035 100644 --- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java +++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java @@ -21,7 +21,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; -import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE; import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE_PER_TASK; import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_TASK; @@ -242,20 +241,18 @@ class DesktopModeLaunchParamsModifier implements LaunchParamsModifier { @NonNull Task launchingTask) { if (existingTaskActivity == null || launchingActivity == null) return false; return (existingTaskActivity.packageName == launchingActivity.packageName) - && isLaunchingNewTask(launchingActivity.launchMode, - launchingTask.getBaseIntent().getFlags()) + && isLaunchingNewSingleTask(launchingActivity.launchMode) && isClosingExitingInstance(launchingTask.getBaseIntent().getFlags()); } /** - * Returns true if the launch mode or intent will result in a new task being created for the + * Returns true if the launch mode will result in a single new task being created for the * activity. */ - private boolean isLaunchingNewTask(int launchMode, int intentFlags) { + private boolean isLaunchingNewSingleTask(int launchMode) { return launchMode == LAUNCH_SINGLE_TASK || launchMode == LAUNCH_SINGLE_INSTANCE - || launchMode == LAUNCH_SINGLE_INSTANCE_PER_TASK - || (intentFlags & FLAG_ACTIVITY_NEW_TASK) != 0; + || launchMode == LAUNCH_SINGLE_INSTANCE_PER_TASK; } /** diff --git a/services/core/java/com/android/server/wm/DisplayAreaPolicy.java b/services/core/java/com/android/server/wm/DisplayAreaPolicy.java index 9d9c5ceb57d6..11fb229bb93d 100644 --- a/services/core/java/com/android/server/wm/DisplayAreaPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayAreaPolicy.java @@ -26,6 +26,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE; import static android.view.WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR; +import static android.view.WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; import static android.window.DisplayAreaOrganizer.FEATURE_APP_ZOOM_OUT; import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER; @@ -166,7 +167,7 @@ public abstract class DisplayAreaPolicy { .all() .except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL, TYPE_STATUS_BAR, TYPE_NOTIFICATION_SHADE, - TYPE_KEYGUARD_DIALOG, TYPE_WALLPAPER) + TYPE_KEYGUARD_DIALOG, TYPE_WALLPAPER, TYPE_VOLUME_OVERLAY) .build()); } if (USE_DISPLAY_AREA_FOR_FULLSCREEN_MAGNIFICATION) { diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 2f9242fbdfc9..deee44dd7f61 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -47,7 +47,6 @@ import static android.view.Display.FLAG_PRIVATE; import static android.view.Display.FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; import static android.view.Display.INVALID_DISPLAY; import static android.view.Display.STATE_UNKNOWN; -import static android.view.Display.TYPE_EXTERNAL; import static android.view.Display.isSuspendedState; import static android.view.InsetsSource.ID_IME; import static android.view.Surface.ROTATION_0; @@ -433,9 +432,9 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp /** * Ratio between overridden display density for current user and the initial display density, - * used only for external displays. + * used for updating the base density when resolution change happens to preserve display size. */ - float mExternalDisplayForcedDensityRatio = 0.0f; + float mForcedDisplayDensityRatio = 0.0f; boolean mIsDensityForced = false; /** @@ -3120,6 +3119,12 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mBaseRoundedCorners = loadRoundedCorners(baseWidth, baseHeight); } + // Update the base density if there is a forced density ratio. + if (DesktopExperienceFlags.ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS.isTrue() + && mForcedDisplayDensityRatio != 0.0f) { + mBaseDisplayDensity = getBaseDensityFromRatio(); + } + if (mMaxUiWidth > 0 && mBaseDisplayWidth > mMaxUiWidth) { final float ratio = mMaxUiWidth / (float) mBaseDisplayWidth; mBaseDisplayHeight = (int) (mBaseDisplayHeight * ratio); @@ -3137,18 +3142,22 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp + mBaseDisplayHeight + " on display:" + getDisplayId()); } } - // Update the base density if there is a forced density ratio. - if (DesktopExperienceFlags.ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS.isTrue() - && mIsDensityForced && mExternalDisplayForcedDensityRatio != 0.0f) { - mBaseDisplayDensity = (int) - (mInitialDisplayDensity * mExternalDisplayForcedDensityRatio + 0.5); - } if (mDisplayReady && !mDisplayPolicy.shouldKeepCurrentDecorInsets()) { mDisplayPolicy.mDecorInsets.invalidate(); } } /** + * Returns the forced density from forcedDensityRatio if the ratio is valid by rounding the + * density down to an even number. Returns the initial density if the ratio is 0. + */ + private int getBaseDensityFromRatio() { + return (mForcedDisplayDensityRatio != 0.0f) + ? ((int) (mInitialDisplayDensity * mForcedDisplayDensityRatio)) & ~1 + : mInitialDisplayDensity; + } + + /** * Forces this display to use the specified density. * * @param density The density in DPI to use. If the value equals to initial density, the setting @@ -3172,15 +3181,19 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp if (density == getInitialDisplayDensity()) { density = 0; } - // Save the new density ratio to settings for external displays. - if (DesktopExperienceFlags.ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS.isTrue() - && mDisplayInfo.type == TYPE_EXTERNAL) { - mExternalDisplayForcedDensityRatio = (float) - mBaseDisplayDensity / getInitialDisplayDensity(); + mWmService.mDisplayWindowSettings.setForcedDensity(getDisplayInfo(), density, userId); + } + + void setForcedDensityRatio(float ratio, int userId) { + // Save the new density ratio to settings and update forced density with the ratio. + if (DesktopExperienceFlags.ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS.isTrue()) { + mForcedDisplayDensityRatio = ratio; mWmService.mDisplayWindowSettings.setForcedDensityRatio(getDisplayInfo(), - mExternalDisplayForcedDensityRatio); + mForcedDisplayDensityRatio); + + // Set forced density from ratio. + setForcedDensity(getBaseDensityFromRatio(), userId); } - mWmService.mDisplayWindowSettings.setForcedDensity(getDisplayInfo(), density, userId); } /** @param mode {@link #FORCE_SCALING_MODE_AUTO} or {@link #FORCE_SCALING_MODE_DISABLED}. */ @@ -3403,6 +3416,10 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp void removeImmediately() { mDeferredRemoval = false; try { + if (DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue() + && mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(this)) { + mDisplayPolicy.notifyDisplayRemoveSystemDecorations(); + } mUnknownAppVisibilityController.clear(); mTransitionController.unregisterLegacyListener(mFixedRotationTransitionListener); mDeviceStateController.unregisterDeviceStateCallback(mDeviceStateConsumer); @@ -4982,22 +4999,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp return win != null; } - /** - * Callbacks when the given type of {@link WindowContainer} animation finished running in the - * hierarchy. - */ - void onWindowAnimationFinished(@NonNull WindowContainer wc, int type) { - if (mImeScreenshot != null) { - ProtoLog.i(WM_DEBUG_IME, - "onWindowAnimationFinished, wc=%s, type=%s, imeSnapshot=%s, target=%s", - wc, SurfaceAnimator.animationTypeToString(type), mImeScreenshot, - mImeScreenshot.getImeTarget()); - } - if ((type & WindowState.EXIT_ANIMATING_TYPES) != 0) { - removeImeSurfaceByTarget(wc); - } - } - // TODO: Super unexpected long method that should be broken down... void applySurfaceChangesTransaction() { final WindowSurfacePlacer surfacePlacer = mWmService.mWindowPlacerLocked; diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index f9eb0574d87c..dbae9c4b3a0f 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -1882,6 +1882,9 @@ public class DisplayPolicy { final boolean isSystemDecorationsSupported = mDisplayContent.isSystemDecorationsSupported(); final boolean isHomeSupported = mDisplayContent.isHomeSupported(); + final boolean eligibleForDesktopMode = + isSystemDecorationsSupported && (mDisplayContent.isDefaultDisplay + || mDisplayContent.allowContentModeSwitch()); mHandler.post(() -> { if (isSystemDecorationsSupported) { StatusBarManagerInternal statusBar = getStatusBarManagerInternal(); @@ -1896,6 +1899,10 @@ public class DisplayPolicy { wpMgr.onDisplayAddSystemDecorations(displayId); } } + if (eligibleForDesktopMode) { + mService.mDisplayNotificationController.dispatchDesktopModeEligibleChanged( + displayId); + } }); } else { mHandler.post(() -> { @@ -1926,6 +1933,8 @@ public class DisplayPolicy { if (wpMgr != null) { wpMgr.onDisplayRemoveSystemDecorations(displayId); } + mService.mDisplayNotificationController.dispatchDesktopModeEligibleChanged( + displayId); final NotificationManagerInternal notificationManager = LocalServices.getService(NotificationManagerInternal.class); if (notificationManager != null) { diff --git a/services/core/java/com/android/server/wm/DisplayWindowListenerController.java b/services/core/java/com/android/server/wm/DisplayWindowListenerController.java index d90fff229cd9..d705274b62e7 100644 --- a/services/core/java/com/android/server/wm/DisplayWindowListenerController.java +++ b/services/core/java/com/android/server/wm/DisplayWindowListenerController.java @@ -133,4 +133,15 @@ class DisplayWindowListenerController { } mDisplayListeners.finishBroadcast(); } + + void dispatchDesktopModeEligibleChanged(int displayId) { + int count = mDisplayListeners.beginBroadcast(); + for (int i = 0; i < count; ++i) { + try { + mDisplayListeners.getBroadcastItem(i).onDesktopModeEligibleChanged(displayId); + } catch (RemoteException e) { + } + } + mDisplayListeners.finishBroadcast(); + } } diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java index 56579206566f..2818d79a40ad 100644 --- a/services/core/java/com/android/server/wm/DisplayWindowSettings.java +++ b/services/core/java/com/android/server/wm/DisplayWindowSettings.java @@ -391,7 +391,7 @@ class DisplayWindowSettings { final int density = hasDensityOverride ? settings.mForcedDensity : dc.getInitialDisplayDensity(); if (hasDensityOverrideRatio) { - dc.mExternalDisplayForcedDensityRatio = settings.mForcedDensityRatio; + dc.mForcedDisplayDensityRatio = settings.mForcedDensityRatio; } dc.updateBaseDisplayMetrics(width, height, density, dc.mBaseDisplayPhysicalXDpi, diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java index 64ae21dc69de..1bf65d1e3536 100644 --- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java +++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java @@ -209,7 +209,8 @@ class EmbeddedWindowController { "Transfer request must originate from owner of transferFromToken"); } final boolean didTransfer = mInputManagerService.transferTouchGesture( - ew.getInputChannelToken(), transferToHostWindowState.mInputChannelToken); + ew.getInputChannelToken(), transferToHostWindowState.mInputChannelToken, + /* transferEntireGesture */ true); if (didTransfer) { ew.mGestureToEmbedded = false; } @@ -228,7 +229,7 @@ class EmbeddedWindowController { } final boolean didTransfer = mInputManagerService.transferTouchGesture( hostWindowState.mInputChannelToken, - ew.getInputChannelToken()); + ew.getInputChannelToken(), /* transferEntireGesture */ true); if (didTransfer) { ew.mGestureToEmbedded = true; mAtmService.mBackNavigationController.onEmbeddedWindowGestureTransferred( diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java index 53681f950c8e..f2bc909bd64b 100644 --- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java @@ -445,14 +445,21 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { if (controlTarget != null) { final boolean imeAnimating = Flags.reportAnimatingInsetsTypes() && (controlTarget.getAnimatingTypes() & WindowInsets.Type.ime()) != 0; - ImeTracker.forLogging().onProgress(statsToken, + final boolean imeVisible = + controlTarget.isRequestedVisible(WindowInsets.Type.ime()) || imeAnimating; + final var finalStatsToken = statsToken != null ? statsToken + : ImeTracker.forLogging().onStart( + imeVisible ? ImeTracker.TYPE_SHOW : ImeTracker.TYPE_HIDE, + ImeTracker.ORIGIN_SERVER, + SoftInputShowHideReason.IME_REQUESTED_CHANGED_LISTENER, + false /* fromUser */); + ImeTracker.forLogging().onProgress(finalStatsToken, ImeTracker.PHASE_WM_POSTING_CHANGED_IME_VISIBILITY); mDisplayContent.mWmService.mH.post(() -> { - ImeTracker.forLogging().onProgress(statsToken, + ImeTracker.forLogging().onProgress(finalStatsToken, ImeTracker.PHASE_WM_INVOKING_IME_REQUESTED_LISTENER); - imeListener.onImeRequestedChanged(controlTarget.getWindowToken(), - controlTarget.isRequestedVisible(WindowInsets.Type.ime()) - || imeAnimating, statsToken); + imeListener.onImeRequestedChanged(controlTarget.getWindowToken(), imeVisible, + finalStatsToken); }); } else { ImeTracker.forLogging().onFailed(statsToken, diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java index b9550feeab8a..24b5f618e32b 100644 --- a/services/core/java/com/android/server/wm/RecentTasks.java +++ b/services/core/java/com/android/server/wm/RecentTasks.java @@ -49,6 +49,7 @@ 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.ActivityTaskSupervisor.REMOVE_FROM_RECENTS; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityTaskManager; @@ -83,6 +84,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLog; import com.android.internal.util.function.pooled.PooledLambda; import com.android.server.am.ActivityManagerService; +import com.android.window.flags.Flags; import com.google.android.collect.Sets; @@ -1452,9 +1454,10 @@ class RecentTasks { * @return whether the given active task should be presented to the user through SystemUI. */ @VisibleForTesting - boolean isVisibleRecentTask(Task task) { + boolean isVisibleRecentTask(@NonNull Task task) { if (DEBUG_RECENTS_TRIM_TASKS) { Slog.d(TAG, "isVisibleRecentTask: task=" + task + + " isForceExcludedFromRecents=" + task.isForceExcludedFromRecents() + " minVis=" + mMinNumVisibleTasks + " maxVis=" + mMaxNumVisibleTasks + " sessionDuration=" + mActiveTasksSessionDurationMs + " inactiveDuration=" + task.getInactiveDuration() @@ -1464,6 +1467,11 @@ class RecentTasks { + " intentFlags=" + task.getBaseIntent().getFlags()); } + // Ignore the task if it is force excluded from recents. + if (Flags.excludeTaskFromRecents() && task.isForceExcludedFromRecents()) { + return false; + } + switch (task.getActivityType()) { case ACTIVITY_TYPE_HOME: case ACTIVITY_TYPE_RECENTS: diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index ec17d131958b..89634707995a 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -625,6 +625,9 @@ class Task extends TaskFragment { boolean mAlignActivityLocaleWithTask = false; + /** @see #isForceExcludedFromRecents() */ + private boolean mForceExcludedFromRecents; + private Task(ActivityTaskManagerService atmService, int _taskId, Intent _intent, Intent _affinityIntent, String _affinity, String _rootAffinity, ComponentName _realActivity, ComponentName _origActivity, boolean _rootWasReset, @@ -3842,7 +3845,8 @@ class Task extends TaskFragment { pw.print(prefix); pw.print("lastActiveTime="); pw.print(lastActiveTime); pw.println(" (inactive for " + (getInactiveDuration() / 1000) + "s)"); pw.print(prefix); pw.print("isTrimmable=" + mIsTrimmableFromRecents); - pw.print(" isForceHidden="); pw.println(isForceHidden()); + pw.print(" isForceHidden="); pw.print(isForceHidden()); + pw.print(" isForceExcludedFromRecents="); pw.println(isForceExcludedFromRecents()); if (mLaunchAdjacentDisabled) { pw.println(prefix + "mLaunchAdjacentDisabled=true"); } @@ -4555,11 +4559,45 @@ class Task extends TaskFragment { /** * @return whether this task is always on top without taking visibility into account. + * @deprecated b/388630258 replace hidden bubble tasks with reordering. + * {@link RecentTasks#isVisibleRecentTask} now checks {@link #isForceExcludedFromRecents}. */ - public boolean isAlwaysOnTopWhenVisible() { + @Deprecated + boolean isAlwaysOnTopWhenVisible() { return super.isAlwaysOnTop(); } + /** + * Returns whether this task is forcibly excluded from the Recents list. + * + * <p>This flag is used by {@link RecentTasks#isVisibleRecentTask} to determine + * if the task should be presented to the user through SystemUI. If this method + * returns {@code true}, the task will not be shown in Recents, regardless of other + * visibility criteria. + * + * @return {@code true} if the task is excluded, {@code false} otherwise. + */ + boolean isForceExcludedFromRecents() { + return mForceExcludedFromRecents; + } + + /** + * Sets whether this task should be forcibly excluded from the Recents list. + * + * <p>This method is intended to be used in conjunction with + * {@link android.window.WindowContainerTransaction#setTaskForceExcludedFromRecents} to modify the + * task's exclusion state. + * + * @param excluded {@code true} to exclude the task, {@code false} otherwise. + */ + void setForceExcludedFromRecents(boolean excluded) { + if (!Flags.excludeTaskFromRecents()) { + Slog.w(TAG, "Flag " + Flags.FLAG_EXCLUDE_TASK_FROM_RECENTS + " is not enabled"); + return; + } + mForceExcludedFromRecents = excluded; + } + boolean isForceHiddenForPinnedTask() { return (mForceHiddenFlags & FLAG_FORCE_HIDDEN_FOR_PINNED_TASK) != 0; } @@ -5764,15 +5802,16 @@ class Task extends TaskFragment { return false; } - // If we have a watcher, preflight the move before committing to it. First check - // for *other* available tasks, but if none are available, then try again allowing the - // current task to be selected. + // If we have a watcher, preflight the move before committing to it. + // Checks for other available tasks; however, if none are available, skips because this + // is the bottommost task. if (mAtmService.mController != null && isTopRootTaskInDisplayArea()) { - ActivityRecord next = topRunningActivity(null, task.mTaskId); - if (next == null) { - next = topRunningActivity(null, INVALID_TASK_ID); - } + final ActivityRecord next = getDisplayArea().getActivity( + a -> isTopRunning(a, task.mTaskId, null /* notTop */)); if (next != null) { + if (next.isState(RESUMED)) { + return true; + } // ask watcher if this is allowed boolean moveOK = true; try { diff --git a/services/core/java/com/android/server/wm/TaskChangeNotificationController.java b/services/core/java/com/android/server/wm/TaskChangeNotificationController.java index c3649fe98056..709f491a3bdc 100644 --- a/services/core/java/com/android/server/wm/TaskChangeNotificationController.java +++ b/services/core/java/com/android/server/wm/TaskChangeNotificationController.java @@ -72,7 +72,6 @@ class TaskChangeNotificationController { private final Handler mHandler; // Task stack change listeners in a remote process. - @GuardedBy("mRemoteTaskStackListeners") private final RemoteCallbackList<ITaskStackListener> mRemoteTaskStackListeners = new RemoteCallbackList<>(); @@ -311,9 +310,7 @@ class TaskChangeNotificationController { } } } else if (listener != null) { - synchronized (mRemoteTaskStackListeners) { - mRemoteTaskStackListeners.register(listener); - } + mRemoteTaskStackListeners.register(listener); } } @@ -323,24 +320,20 @@ class TaskChangeNotificationController { mLocalTaskStackListeners.remove(listener); } } else if (listener != null) { - synchronized (mRemoteTaskStackListeners) { - mRemoteTaskStackListeners.unregister(listener); - } + mRemoteTaskStackListeners.unregister(listener); } } private void forAllRemoteListeners(TaskStackConsumer callback, Message message) { - synchronized (mRemoteTaskStackListeners) { - for (int i = mRemoteTaskStackListeners.beginBroadcast() - 1; i >= 0; i--) { - try { - // Make a one-way callback to the listener - callback.accept(mRemoteTaskStackListeners.getBroadcastItem(i), message); - } catch (RemoteException e) { - // Handled by the RemoteCallbackList. - } + for (int i = mRemoteTaskStackListeners.beginBroadcast() - 1; i >= 0; i--) { + try { + // Make a one-way callback to the listener + callback.accept(mRemoteTaskStackListeners.getBroadcastItem(i), message); + } catch (RemoteException e) { + // Handled by the RemoteCallbackList. } - mRemoteTaskStackListeners.finishBroadcast(); } + mRemoteTaskStackListeners.finishBroadcast(); } private void forAllLocalListeners(TaskStackConsumer callback, Message message) { diff --git a/services/core/java/com/android/server/wm/TaskSystemBarsListenerController.java b/services/core/java/com/android/server/wm/TaskSystemBarsListenerController.java index acb6061de93f..dc6b70d839e4 100644 --- a/services/core/java/com/android/server/wm/TaskSystemBarsListenerController.java +++ b/services/core/java/com/android/server/wm/TaskSystemBarsListenerController.java @@ -48,8 +48,10 @@ final class TaskSystemBarsListenerController { int taskId, boolean visible, boolean wereRevealedFromSwipeOnSystemBar) { - HashSet<TaskSystemBarsListener> localListeners; - localListeners = new HashSet<>(mListeners); + if (mListeners.isEmpty()) { + return; + } + final HashSet<TaskSystemBarsListener> localListeners = new HashSet<>(mListeners); mBackgroundExecutor.execute(() -> { for (TaskSystemBarsListener listener : localListeners) { diff --git a/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java b/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java index e612d8ec0ce6..98c143a866d0 100644 --- a/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java +++ b/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java @@ -31,6 +31,7 @@ import android.graphics.Region; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; +import android.os.InputConfig; import android.os.RemoteException; import android.util.ArrayMap; import android.util.IntArray; @@ -260,8 +261,9 @@ public class TrustedPresentationListenerController { ArrayMap<ITrustedPresentationListener, Pair<IntArray, IntArray>> listenerUpdates = new ArrayMap<>(); for (var windowHandle : mLastWindowHandles.first) { - if (!windowHandle.canOccludePresentation) { - ProtoLog.v(WM_DEBUG_TPL, "Skipping %s", windowHandle.name); + var isInvisible = ((windowHandle.inputConfig & InputConfig.NOT_VISIBLE) + == InputConfig.NOT_VISIBLE); + if (!windowHandle.canOccludePresentation || isInvisible) { continue; } int displayId = INVALID_DISPLAY; diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 5b4870b0c0c7..247a51d9fcb3 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -55,14 +55,12 @@ import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import android.annotation.CallSuper; -import android.annotation.ColorInt; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo.ScreenOrientation; import android.content.res.Configuration; -import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; import android.os.Debug; @@ -105,7 +103,6 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedList; import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; @@ -218,14 +215,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< protected final WindowManagerService mWmService; final TransitionController mTransitionController; - /** - * Sources which triggered a surface animation on this container. An animation target can be - * promoted to higher level, for example, from a set of {@link ActivityRecord}s to - * {@link Task}. In this case, {@link ActivityRecord}s are set on this variable while - * the animation is running, and reset after finishing it. - */ - private final ArraySet<WindowContainer> mSurfaceAnimationSources = new ArraySet<>(); - private final Point mTmpPos = new Point(); protected final Point mLastSurfacePosition = new Point(); protected @Surface.Rotation int mLastDeltaRotation = Surface.ROTATION_0; @@ -279,17 +268,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< */ int mTransitFlags; - /** Layer used to constrain the animation to a container's stack bounds. */ - SurfaceControl mAnimationBoundsLayer; - - /** Whether this container needs to create mAnimationBoundsLayer for cropping animations. */ - boolean mNeedsAnimationBoundsLayer; - - /** - * This gets used during some open/close transitions as well as during a change transition - * where it represents the starting-state snapshot. - */ - final Point mTmpPoint = new Point(); protected final Rect mTmpRect = new Rect(); final Rect mTmpPrevBounds = new Rect(); @@ -2961,7 +2939,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< } void cancelAnimation() { - doAnimationFinished(mSurfaceAnimator.getAnimationType(), mSurfaceAnimator.getAnimation()); mSurfaceAnimator.cancelAnimation(); } @@ -2992,10 +2969,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< || (getParent() != null && getParent().inPinnedWindowingMode()); } - ArraySet<WindowContainer> getAnimationSources() { - return mSurfaceAnimationSources; - } - @Override public Builder makeAnimationLeash() { return makeSurface().setContainerLayer(); @@ -3094,21 +3067,10 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< return mAnimationLeash; } - private void doAnimationFinished(@AnimationType int type, AnimationAdapter anim) { - for (int i = 0; i < mSurfaceAnimationSources.size(); ++i) { - mSurfaceAnimationSources.valueAt(i).onAnimationFinished(type, anim); - } - mSurfaceAnimationSources.clear(); - if (mDisplayContent != null) { - mDisplayContent.onWindowAnimationFinished(this, type); - } - } - /** * Called when an animation has finished running. */ protected void onAnimationFinished(@AnimationType int type, AnimationAdapter anim) { - doAnimationFinished(type, anim); mWmService.onAnimationFinished(); } @@ -3241,7 +3203,9 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< mLocalInsetsSources.valueAt(i).dump(childPrefix, pw); } } - pw.println(prefix + mSafeRegionBounds + " SafeRegionBounds"); + if (mSafeRegionBounds != null) { + pw.println(prefix + "mSafeRegionBounds=" + mSafeRegionBounds); + } } final void updateSurfacePositionNonOrganized() { @@ -3821,50 +3785,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< return true; } - private class AnimationRunnerBuilder { - /** - * Runs when the surface stops animating - */ - private final List<Runnable> mOnAnimationFinished = new LinkedList<>(); - /** - * Runs when the animation is cancelled but the surface is still animating - */ - private final List<Runnable> mOnAnimationCancelled = new LinkedList<>(); - - private void setTaskBackgroundColor(@ColorInt int backgroundColor) { - TaskDisplayArea taskDisplayArea = getTaskDisplayArea(); - - if (taskDisplayArea != null && backgroundColor != Color.TRANSPARENT) { - taskDisplayArea.setBackgroundColor(backgroundColor); - - // Atomic counter to make sure the clearColor callback is only called one. - // It will be called twice in the case we cancel the animation without restart - // (in that case it will run as the cancel and finished callbacks). - final AtomicInteger callbackCounter = new AtomicInteger(0); - final Runnable clearBackgroundColorHandler = () -> { - if (callbackCounter.getAndIncrement() == 0) { - taskDisplayArea.clearBackgroundColor(); - } - }; - - // We want to make sure this is called both when the surface stops animating and - // also when an animation is cancelled (i.e. animation is replaced by another - // animation but and so the surface is still animating) - mOnAnimationFinished.add(clearBackgroundColorHandler); - mOnAnimationCancelled.add(clearBackgroundColorHandler); - } - } - - private IAnimationStarter build() { - return (Transaction t, AnimationAdapter adapter, boolean hidden, - @AnimationType int type, @Nullable AnimationAdapter snapshotAnim) -> { - startAnimation(getPendingTransaction(), adapter, !isVisible(), type, - (animType, anim) -> mOnAnimationFinished.forEach(Runnable::run), - () -> mOnAnimationCancelled.forEach(Runnable::run), snapshotAnim); - }; - } - } - private interface IAnimationStarter { void startAnimation(Transaction t, AnimationAdapter anim, boolean hidden, @AnimationType int type, @Nullable AnimationAdapter snapshotAnim); diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index 5f2a2ad7f0eb..8ed93276d646 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -371,7 +371,7 @@ public abstract class WindowManagerInternal { * @param statsToken the token tracking the current IME request. */ void onImeRequestedChanged(IBinder windowToken, boolean imeVisible, - @Nullable ImeTracker.Token statsToken); + @NonNull ImeTracker.Token statsToken); } /** diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 3ccbc868377e..aa5eb33d8069 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -67,7 +67,6 @@ import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; -import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY; import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE; @@ -99,7 +98,6 @@ import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ER import static android.view.flags.Flags.sensitiveContentAppProtection; import static android.window.WindowProviderService.isWindowProviderService; -import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ADD_REMOVE; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ANIM; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_BOOT; @@ -3163,6 +3161,11 @@ public class WindowManagerService extends IWindowManager.Stub // Reparent the window created for this window context. dc.reParentWindowToken(token); hideUntilNextDraw(token); + // Prevent a race condition where VRI temporarily reverts the context display ID + // before the onDisplayMoved callback arrives. This caused incorrect display IDs + // during configuration changes, breaking SysUI layouts dependent on it. + // Forcing a resize report ensures VRI has the correct ID before the update. + forceReportResizing(token); // This makes sure there is a traversal scheduled that will eventually report // the window resize to the client. dc.setLayoutNeeded(); @@ -3184,6 +3187,14 @@ public class WindowManagerService extends IWindowManager.Stub } } + private void forceReportResizing(@NonNull WindowContainer<?> wc) { + wc.forAllWindows(w -> { + if (!mResizingWindows.contains(w)) { + mResizingWindows.add(w); + } + }, true /* traverseTopToBottom */); + } + private void hideUntilNextDraw(@NonNull WindowToken token) { final WindowState topChild = token.getTopChild(); if (topChild != null) { @@ -6233,6 +6244,10 @@ public class WindowManagerService extends IWindowManager.Stub final long ident = Binder.clearCallingIdentity(); try { synchronized (mGlobalLock) { + // Clear forced display density ratio + setForcedDisplayDensityRatioInternal(displayId, 0.0f, userId); + + // Clear forced display density final DisplayContent displayContent = mRoot.getDisplayContent(displayId); if (displayContent != null) { displayContent.setForcedDensity(displayContent.getInitialDisplayDensity(), @@ -6257,6 +6272,37 @@ public class WindowManagerService extends IWindowManager.Stub @EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) @Override + public void setForcedDisplayDensityRatio(int displayId, float ratio, int userId) { + setForcedDisplayDensityRatio_enforcePermission(); + final long ident = Binder.clearCallingIdentity(); + try { + synchronized (mGlobalLock) { + setForcedDisplayDensityRatioInternal(displayId, ratio, userId); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private void setForcedDisplayDensityRatioInternal( + int displayId, float ratio, int userId) { + final DisplayContent displayContent = mRoot.getDisplayContent(displayId); + if (displayContent != null) { + displayContent.setForcedDensityRatio(ratio, userId); + return; + } + + final DisplayInfo info = mDisplayManagerInternal.getDisplayInfo(displayId); + if (info == null) { + ProtoLog.e(WM_ERROR, "Failed to get information about logical display %d. " + + "Skip setting forced display density.", displayId); + return; + } + mDisplayWindowSettings.setForcedDensityRatio(info, ratio); + } + + @EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + @Override public void setConfigurationChangeSettingsForUser( @NonNull List<ConfigurationChangeSetting> settings, int userId) { setConfigurationChangeSettingsForUser_enforcePermission(); @@ -8843,48 +8889,45 @@ public class WindowManagerService extends IWindowManager.Stub } void updateNonSystemOverlayWindowsVisibilityIfNeeded(WindowState win, boolean surfaceShown) { - if (!win.hideNonSystemOverlayWindowsWhenVisible() - && !mHidingNonSystemOverlayWindows.contains(win)) { + final boolean effective = (surfaceShown && win.hideNonSystemOverlayWindowsWhenVisible()); + if (effective == mHidingNonSystemOverlayWindows.contains(win)) { return; } - final boolean systemAlertWindowsHidden = !mHidingNonSystemOverlayWindows.isEmpty(); - final int numUIDsRequestHidingPreUpdate = mHidingNonSystemOverlayWindowsCountPerUid.size(); - if (surfaceShown && win.hideNonSystemOverlayWindowsWhenVisible()) { - if (!mHidingNonSystemOverlayWindows.contains(win)) { - mHidingNonSystemOverlayWindows.add(win); - int uid = win.getOwningUid(); - int count = mHidingNonSystemOverlayWindowsCountPerUid.getOrDefault(uid, 0); - mHidingNonSystemOverlayWindowsCountPerUid.put(uid, count + 1); - } + + if (effective) { + mHidingNonSystemOverlayWindows.add(win); } else { mHidingNonSystemOverlayWindows.remove(win); - int uid = win.getOwningUid(); - int count = mHidingNonSystemOverlayWindowsCountPerUid.getOrDefault(uid, 0); - if (count <= 1) { - mHidingNonSystemOverlayWindowsCountPerUid.remove(win.getOwningUid()); - } else { - mHidingNonSystemOverlayWindowsCountPerUid.put(uid, count - 1); - } } - final boolean hideSystemAlertWindows = !mHidingNonSystemOverlayWindows.isEmpty(); - final int numUIDSRequestHidingPostUpdate = mHidingNonSystemOverlayWindowsCountPerUid.size(); + + final boolean changed; if (Flags.fixHideOverlayApi()) { - if (numUIDSRequestHidingPostUpdate == numUIDsRequestHidingPreUpdate) { - return; - } - // The visibility of SAWs needs to be refreshed only when the number of uids that - // request hiding SAWs changes 0->1, 1->0, 1->2 or 2->1. - if (numUIDSRequestHidingPostUpdate != 1 && numUIDsRequestHidingPreUpdate != 1) { - return; + final int uid = win.getOwningUid(); + final int numUIDsPreUpdate = mHidingNonSystemOverlayWindowsCountPerUid.size(); + final int newCount = mHidingNonSystemOverlayWindowsCountPerUid.getOrDefault(uid, 0) + + (effective ? +1 : -1); + if (newCount <= 0) { + mHidingNonSystemOverlayWindowsCountPerUid.remove(uid); + } else { + mHidingNonSystemOverlayWindowsCountPerUid.put(uid, newCount); } + final int numUIDsPostUpdate = mHidingNonSystemOverlayWindowsCountPerUid.size(); + // The visibility of SAWs needs to be refreshed when the number of uids that + // request hiding SAWs changes between "0", "1", or "2+". + changed = (numUIDsPostUpdate != numUIDsPreUpdate) + && (numUIDsPostUpdate <= 1 || numUIDsPreUpdate <= 1); } else { - if (systemAlertWindowsHidden == hideSystemAlertWindows) { - return; - } + // The visibility of SAWs needs to be refreshed when the number of windows that + // request hiding SAWs changes between "0" or "1+". + changed = (effective && mHidingNonSystemOverlayWindows.size() == 1) + || (!effective && mHidingNonSystemOverlayWindows.isEmpty()); + } + + if (changed) { + mRoot.forAllWindows((w) -> { + w.setForceHideNonSystemOverlayWindowIfNeeded(shouldHideNonSystemOverlayWindow(w)); + }, false /* traverseTopToBottom */); } - mRoot.forAllWindows((w) -> { - w.setForceHideNonSystemOverlayWindowIfNeeded(shouldHideNonSystemOverlayWindow(w)); - }, false /* traverseTopToBottom */); } /** Called from Accessibility Controller to apply magnification spec */ @@ -9244,25 +9287,6 @@ public class WindowManagerService extends IWindowManager.Stub + "' because it isn't a trusted overlay"); return inputFeatures & ~INPUT_FEATURE_SENSITIVE_FOR_PRIVACY; } - - // You need OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW permission to be able - // to set INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS. - if (overridePowerKeyBehaviorInFocusedWindow() - && (inputFeatures - & INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS) - != 0) { - final int powerPermissionResult = - mContext.checkPermission( - permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW, - callingPid, - callingUid); - if (powerPermissionResult != PackageManager.PERMISSION_GRANTED) { - throw new IllegalArgumentException( - "Cannot use INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS from" + windowName - + " because it doesn't have the" - + " OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW permission"); - } - } return inputFeatures; } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index d43aba0d218d..93876f5eeed4 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -169,6 +169,7 @@ import static com.android.server.wm.WindowStateProto.IS_READY_FOR_DISPLAY; import static com.android.server.wm.WindowStateProto.IS_VISIBLE; import static com.android.server.wm.WindowStateProto.KEEP_CLEAR_AREAS; import static com.android.server.wm.WindowStateProto.MERGED_LOCAL_INSETS_SOURCES; +import static com.android.server.wm.WindowStateProto.PREPARE_SYNC_SEQ_ID; import static com.android.server.wm.WindowStateProto.REMOVED; import static com.android.server.wm.WindowStateProto.REMOVE_ON_EXIT; import static com.android.server.wm.WindowStateProto.REQUESTED_HEIGHT; @@ -177,6 +178,7 @@ import static com.android.server.wm.WindowStateProto.REQUESTED_WIDTH; import static com.android.server.wm.WindowStateProto.STACK_ID; import static com.android.server.wm.WindowStateProto.SURFACE_INSETS; import static com.android.server.wm.WindowStateProto.SURFACE_POSITION; +import static com.android.server.wm.WindowStateProto.SYNC_SEQ_ID; import static com.android.server.wm.WindowStateProto.UNRESTRICTED_KEEP_CLEAR_AREAS; import static com.android.server.wm.WindowStateProto.VIEW_VISIBILITY; import static com.android.server.wm.WindowStateProto.WINDOW_CONTAINER; @@ -3945,6 +3947,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP dimBounds.dumpDebug(proto, DIM_BOUNDS); } } + proto.write(SYNC_SEQ_ID, mSyncSeqId); + proto.write(PREPARE_SYNC_SEQ_ID, mPrepareSyncSeqId); proto.end(token); } @@ -4697,40 +4701,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP return super.handleCompleteDeferredRemoval(); } - boolean clearAnimatingFlags() { - boolean didSomething = false; - // We also don't clear the mAnimatingExit flag for windows which have the - // mRemoveOnExit flag. This indicates an explicit remove request has been issued - // by the client. We should let animation proceed and not clear this flag or - // they won't eventually be removed by WindowStateAnimator#finishExit. - if (!mRemoveOnExit) { - // Clear mAnimating flag together with mAnimatingExit. When animation - // changes from exiting to entering, we need to clear this flag until the - // new animation gets applied, so that isAnimationStarting() becomes true - // until then. - // Otherwise applySurfaceChangesTransaction will fail to skip surface - // placement for this window during this period, one or more frame will - // show up with wrong position or scale. - if (mAnimatingExit) { - mAnimatingExit = false; - ProtoLog.d(WM_DEBUG_ANIM, "Clear animatingExit: reason=clearAnimatingFlags win=%s", - this); - didSomething = true; - } - if (mDestroying) { - mDestroying = false; - mWmService.mDestroySurface.remove(this); - didSomething = true; - } - } - - for (int i = mChildren.size() - 1; i >= 0; --i) { - didSomething |= (mChildren.get(i)).clearAnimatingFlags(); - } - - return didSomething; - } - public boolean isRtl() { return getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; } @@ -5541,10 +5511,9 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP || mKeyInterceptionInfo.layoutParamsPrivateFlags != mAttrs.privateFlags || mKeyInterceptionInfo.layoutParamsType != mAttrs.type || mKeyInterceptionInfo.windowTitle != getWindowTag() - || mKeyInterceptionInfo.windowOwnerUid != getOwningUid() - || mKeyInterceptionInfo.inputFeaturesFlags != mAttrs.inputFeatures) { + || mKeyInterceptionInfo.windowOwnerUid != getOwningUid()) { mKeyInterceptionInfo = new KeyInterceptionInfo(mAttrs.type, mAttrs.privateFlags, - getWindowTag().toString(), getOwningUid(), mAttrs.inputFeatures); + getWindowTag().toString(), getOwningUid()); } return mKeyInterceptionInfo; } diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java index cca73c574951..7823b92e4057 100644 --- a/services/core/java/com/android/server/wm/WindowToken.java +++ b/services/core/java/com/android/server/wm/WindowToken.java @@ -134,15 +134,6 @@ class WindowToken extends WindowContainer<WindowState> { } /** - * Transforms the window container from the next rotation to the current rotation for - * showing the window in a display with different rotation. - */ - void transform(WindowContainer<?> container) { - // The default implementation assumes shell transition is enabled, so the transform - // is done by getOrCreateFixedRotationLeash(). - } - - /** * Resets the transformation of the window containers which have been rotated. This should * be called when the window has the same rotation as display. */ @@ -158,45 +149,6 @@ class WindowToken extends WindowContainer<WindowState> { } } - private static class FixedRotationTransformStateLegacy extends FixedRotationTransformState { - final SeamlessRotator mRotator; - final ArrayList<WindowContainer<?>> mRotatedContainers = new ArrayList<>(3); - - FixedRotationTransformStateLegacy(DisplayInfo rotatedDisplayInfo, - DisplayFrames rotatedDisplayFrames, Configuration rotatedConfig, - int currentRotation) { - super(rotatedDisplayInfo, rotatedDisplayFrames, rotatedConfig); - // This will use unrotate as rotate, so the new and old rotation are inverted. - mRotator = new SeamlessRotator(rotatedDisplayInfo.rotation, currentRotation, - rotatedDisplayInfo, true /* applyFixedTransformationHint */); - } - - @Override - void transform(WindowContainer<?> container) { - mRotator.unrotate(container.getPendingTransaction(), container); - if (!mRotatedContainers.contains(container)) { - mRotatedContainers.add(container); - } - } - - @Override - void resetTransform() { - for (int i = mRotatedContainers.size() - 1; i >= 0; i--) { - final WindowContainer<?> c = mRotatedContainers.get(i); - // If the window is detached (no parent), its surface may have been released. - if (c.getParent() != null) { - mRotator.finish(c.getPendingTransaction(), c); - } - } - } - - @Override - void disassociate(WindowToken token) { - super.disassociate(token); - mRotatedContainers.remove(token); - } - } - /** * Compares two child window of this token and returns -1 if the first is lesser than the * second in terms of z-order and 1 otherwise. @@ -494,10 +446,7 @@ class WindowToken extends WindowContainer<WindowState> { mFixedRotationTransformState.disassociate(this); } config = new Configuration(config); - mFixedRotationTransformState = mTransitionController.isShellTransitionsEnabled() - ? new FixedRotationTransformState(info, displayFrames, config) - : new FixedRotationTransformStateLegacy(info, displayFrames, config, - mDisplayContent.getRotation()); + mFixedRotationTransformState = new FixedRotationTransformState(info, displayFrames, config); mFixedRotationTransformState.mAssociatedTokens.add(this); mDisplayContent.getDisplayPolicy().simulateLayoutDisplay(displayFrames); onFixedRotationStatePrepared(); @@ -699,15 +648,6 @@ class WindowToken extends WindowContainer<WindowState> { return; } super.updateSurfacePosition(t); - if (!mTransitionController.isShellTransitionsEnabled() && isFixedRotationTransforming()) { - final Task rootTask = r != null ? r.getRootTask() : null; - // Don't transform the activity in PiP because the PiP task organizer will handle it. - if (rootTask == null || !rootTask.inPinnedWindowingMode()) { - // The window is laid out in a simulated rotated display but the real display hasn't - // rotated, so here transforms its surface to fit in the real display. - mFixedRotationTransformState.transform(this); - } - } } @Override diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index adfabe1e54fd..e49e60632d0e 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -191,7 +191,7 @@ cc_defaults { "android.hardware.thermal@1.0", "android.hardware.thermal-V3-ndk", "android.hardware.tv.input@1.0", - "android.hardware.tv.input-V2-ndk", + "android.hardware.tv.input-V3-ndk", "android.hardware.vibrator-V3-ndk", "android.hardware.vr@1.0", "android.hidl.token@1.0-utils", diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 017284cded8e..ee7c9368f897 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -2548,7 +2548,7 @@ static void nativeSetSystemUiLightsOut(JNIEnv* env, jobject nativeImplObj, jbool static jboolean nativeTransferTouchGesture(JNIEnv* env, jobject nativeImplObj, jobject fromChannelTokenObj, jobject toChannelTokenObj, - jboolean isDragDrop) { + jboolean isDragDrop, jboolean transferEntireGesture) { if (fromChannelTokenObj == nullptr || toChannelTokenObj == nullptr) { return JNI_FALSE; } @@ -2558,7 +2558,8 @@ static jboolean nativeTransferTouchGesture(JNIEnv* env, jobject nativeImplObj, NativeInputManager* im = getNativeInputManager(env, nativeImplObj); if (im->getInputManager()->getDispatcher().transferTouchGesture(fromChannelToken, - toChannelToken, isDragDrop)) { + toChannelToken, isDragDrop, + transferEntireGesture)) { return JNI_TRUE; } else { return JNI_FALSE; @@ -2925,6 +2926,12 @@ static void nativeReloadDeviceAliases(JNIEnv* env, jobject nativeImplObj) { InputReaderConfiguration::Change::DEVICE_ALIAS); } +static jstring nativeGetSysfsRootPath(JNIEnv* env, jobject nativeImplObj, jint deviceId) { + NativeInputManager* im = getNativeInputManager(env, nativeImplObj); + const auto path = im->getInputManager()->getReader().getSysfsRootPath(deviceId); + return path.empty() ? nullptr : env->NewStringUTF(path.c_str()); +} + static void nativeSysfsNodeChanged(JNIEnv* env, jobject nativeImplObj, jstring path) { ScopedUtfChars sysfsNodePathChars(env, path); const std::string sysfsNodePath = sysfsNodePathChars.c_str(); @@ -3344,7 +3351,7 @@ static const JNINativeMethod gInputManagerMethods[] = { {"requestPointerCapture", "(Landroid/os/IBinder;Z)V", (void*)nativeRequestPointerCapture}, {"setInputDispatchMode", "(ZZ)V", (void*)nativeSetInputDispatchMode}, {"setSystemUiLightsOut", "(Z)V", (void*)nativeSetSystemUiLightsOut}, - {"transferTouchGesture", "(Landroid/os/IBinder;Landroid/os/IBinder;Z)Z", + {"transferTouchGesture", "(Landroid/os/IBinder;Landroid/os/IBinder;ZZ)Z", (void*)nativeTransferTouchGesture}, {"transferTouch", "(Landroid/os/IBinder;I)Z", (void*)nativeTransferTouchOnDisplay}, {"getMousePointerSpeed", "()I", (void*)nativeGetMousePointerSpeed}, @@ -3387,6 +3394,7 @@ static const JNINativeMethod gInputManagerMethods[] = { {"getBatteryDevicePath", "(I)Ljava/lang/String;", (void*)nativeGetBatteryDevicePath}, {"reloadKeyboardLayouts", "()V", (void*)nativeReloadKeyboardLayouts}, {"reloadDeviceAliases", "()V", (void*)nativeReloadDeviceAliases}, + {"getSysfsRootPath", "(I)Ljava/lang/String;", (void*)nativeGetSysfsRootPath}, {"sysfsNodeChanged", "(Ljava/lang/String;)V", (void*)nativeSysfsNodeChanged}, {"dump", "()Ljava/lang/String;", (void*)nativeDump}, {"monitor", "()V", (void*)nativeMonitor}, diff --git a/services/core/jni/com_android_server_tv_TvInputHal.cpp b/services/core/jni/com_android_server_tv_TvInputHal.cpp index 1e6384031f9a..def95daea92d 100644 --- a/services/core/jni/com_android_server_tv_TvInputHal.cpp +++ b/services/core/jni/com_android_server_tv_TvInputHal.cpp @@ -91,6 +91,12 @@ static int nativeSetTvMessageEnabled(JNIEnv* env, jclass clazz, jlong ptr, jint return tvInputHal->setTvMessageEnabled(deviceId, streamId, type, enabled); } +static int nativeSetPictureProfile(JNIEnv* env, jclass clazz, jlong ptr, jint deviceId, + jint streamId, jlong profileHandle) { + JTvInputHal* tvInputHal = (JTvInputHal*)ptr; + return tvInputHal->setPictureProfileId(deviceId, streamId, profileHandle); +} + static void nativeClose(JNIEnv* env, jclass clazz, jlong ptr) { JTvInputHal* tvInputHal = (JTvInputHal*)ptr; delete tvInputHal; @@ -104,6 +110,7 @@ static const JNINativeMethod gTvInputHalMethods[] = { {"nativeGetStreamConfigs", "(JII)[Landroid/media/tv/TvStreamConfig;", (void*)nativeGetStreamConfigs}, {"nativeSetTvMessageEnabled", "(JIIIZ)I", (void*)nativeSetTvMessageEnabled}, + {"nativeSetPictureProfile", "(JIIJ)I", (void*)nativeSetPictureProfile}, {"nativeClose", "(J)V", (void*)nativeClose}, }; diff --git a/services/core/jni/gnss/GnssAssistance.cpp b/services/core/jni/gnss/GnssAssistance.cpp index fff396ea126a..e97c7c340e40 100644 --- a/services/core/jni/gnss/GnssAssistance.cpp +++ b/services/core/jni/gnss/GnssAssistance.cpp @@ -206,7 +206,7 @@ jmethodID method_beidouSatelliteClockModelGetTgd2; jmethodID method_beidouSatelliteClockModelGetTimeOfClockSeconds; jmethodID method_beidouSatelliteHealthGetSatH1; jmethodID method_beidouSatelliteHealthGetSvAccur; -jmethodID method_beidouSatelliteEphemerisTimeGetIode; +jmethodID method_beidouSatelliteEphemerisTimeGetAode; jmethodID method_beidouSatelliteEphemerisTimeGetBeidouWeekNumber; jmethodID method_beidouSatelliteEphemerisTimeGetToeSeconds; @@ -710,8 +710,8 @@ void GnssAssistance_class_init_once(JNIEnv* env, jclass clazz) { // Get the methods of BeidouSatelliteEphemerisTime jclass beidouSatelliteEphemerisTimeClass = env->FindClass( "android/location/BeidouSatelliteEphemeris$BeidouSatelliteEphemerisTime"); - method_beidouSatelliteEphemerisTimeGetIode = - env->GetMethodID(beidouSatelliteEphemerisTimeClass, "getIode", "()I"); + method_beidouSatelliteEphemerisTimeGetAode = + env->GetMethodID(beidouSatelliteEphemerisTimeClass, "getAode", "()I"); method_beidouSatelliteEphemerisTimeGetBeidouWeekNumber = env->GetMethodID(beidouSatelliteEphemerisTimeClass, "getBeidouWeekNumber", "()I"); method_beidouSatelliteEphemerisTimeGetToeSeconds = @@ -723,7 +723,7 @@ void GnssAssistance_class_init_once(JNIEnv* env, jclass clazz) { "()Landroid/location/GnssAlmanac;"); method_galileoAssistanceGetIonosphericModel = env->GetMethodID(galileoAssistanceClass, "getIonosphericModel", - "()Landroid/location/KlobucharIonosphericModel;"); + "()Landroid/location/GalileoIonosphericModel;"); method_galileoAssistanceGetUtcModel = env->GetMethodID(galileoAssistanceClass, "getUtcModel", "()Landroid/location/UtcModel;"); method_galileoAssistanceGetLeapSecondsModel = @@ -1244,8 +1244,7 @@ void GnssAssistanceUtil::setGalileoAssistance(JNIEnv* env, jobject galileoAssist env->CallObjectMethod(galileoAssistanceObj, method_galileoAssistanceGetSatelliteCorrections); setGnssAlmanac(env, galileoAlmanacObj, galileoAssistance.almanac); - setGaliloKlobucharIonosphericModel(env, ionosphericModelObj, - galileoAssistance.ionosphericModel); + setGalileoIonosphericModel(env, ionosphericModelObj, galileoAssistance.ionosphericModel); setUtcModel(env, utcModelObj, galileoAssistance.utcModel); setLeapSecondsModel(env, leapSecondsModelObj, galileoAssistance.leapSecondsModel); setTimeModels(env, timeModelsObj, galileoAssistance.timeModels); @@ -1263,9 +1262,8 @@ void GnssAssistanceUtil::setGalileoAssistance(JNIEnv* env, jobject galileoAssist env->DeleteLocalRef(satelliteCorrectionsObj); } -void GnssAssistanceUtil::setGaliloKlobucharIonosphericModel( - JNIEnv* env, jobject galileoIonosphericModelObj, - GalileoIonosphericModel& ionosphericModel) { +void GnssAssistanceUtil::setGalileoIonosphericModel(JNIEnv* env, jobject galileoIonosphericModelObj, + GalileoIonosphericModel& ionosphericModel) { if (galileoIonosphericModelObj == nullptr) return; jdouble ai0 = env->CallDoubleMethod(galileoIonosphericModelObj, method_galileoIonosphericModelGetAi0); @@ -1500,14 +1498,14 @@ void GnssAssistanceUtil::setBeidouSatelliteEphemeris( jobject satelliteEphemerisTimeObj = env->CallObjectMethod(beidouSatelliteEphemerisObj, method_beidouSatelliteEphemerisGetSatelliteEphemerisTime); - jint iode = env->CallIntMethod(satelliteEphemerisTimeObj, - method_beidouSatelliteEphemerisTimeGetIode); + jint aode = env->CallIntMethod(satelliteEphemerisTimeObj, + method_beidouSatelliteEphemerisTimeGetAode); jint beidouWeekNumber = env->CallIntMethod(satelliteEphemerisTimeObj, method_beidouSatelliteEphemerisTimeGetBeidouWeekNumber); jint toeSeconds = env->CallDoubleMethod(satelliteEphemerisTimeObj, method_beidouSatelliteEphemerisTimeGetToeSeconds); - beidouSatelliteEphemeris.satelliteEphemerisTime.aode = static_cast<int32_t>(iode); + beidouSatelliteEphemeris.satelliteEphemerisTime.aode = static_cast<int32_t>(aode); beidouSatelliteEphemeris.satelliteEphemerisTime.weekNumber = static_cast<int32_t>(beidouWeekNumber); beidouSatelliteEphemeris.satelliteEphemerisTime.toeSeconds = diff --git a/services/core/jni/gnss/GnssAssistance.h b/services/core/jni/gnss/GnssAssistance.h index ee97e19371f8..968e661fbaed 100644 --- a/services/core/jni/gnss/GnssAssistance.h +++ b/services/core/jni/gnss/GnssAssistance.h @@ -94,8 +94,8 @@ struct GnssAssistanceUtil { static void setGalileoSatelliteEphemeris( JNIEnv* env, jobject galileoSatelliteEphemerisObj, std::vector<GalileoSatelliteEphemeris>& galileoSatelliteEphemerisList); - static void setGaliloKlobucharIonosphericModel(JNIEnv* env, jobject galileoIonosphericModelObj, - GalileoIonosphericModel& ionosphericModel); + static void setGalileoIonosphericModel(JNIEnv* env, jobject galileoIonosphericModelObj, + GalileoIonosphericModel& ionosphericModel); static void setGnssAssistance(JNIEnv* env, jobject gnssAssistanceObj, GnssAssistance& gnssAssistance); static void setGpsAssistance(JNIEnv* env, jobject gpsAssistanceObj, diff --git a/services/core/jni/tvinput/JTvInputHal.cpp b/services/core/jni/tvinput/JTvInputHal.cpp index 505421e81d3d..e4821299eee9 100644 --- a/services/core/jni/tvinput/JTvInputHal.cpp +++ b/services/core/jni/tvinput/JTvInputHal.cpp @@ -156,6 +156,15 @@ int JTvInputHal::setTvMessageEnabled(int deviceId, int streamId, int type, bool return NO_ERROR; } +int JTvInputHal::setPictureProfileId(int deviceId, int streamId, long profileHandle) { + ::ndk::ScopedAStatus status = mTvInput->setPictureProfileId(deviceId, streamId, profileHandle); + if (!status.isOk()) { + ALOGE("Error in setPictureProfileId. device id:%d stream id:%d", deviceId, streamId); + return status.getStatus(); + } + return NO_ERROR; +} + const std::vector<AidlTvStreamConfig> JTvInputHal::getStreamConfigs(int deviceId) { std::vector<AidlTvStreamConfig> list; ::ndk::ScopedAStatus status = mTvInput->getStreamConfigurations(deviceId, &list); @@ -551,6 +560,16 @@ JTvInputHal::ITvInputWrapper::ITvInputWrapper(std::shared_ptr<AidlITvInput>& aid } } +::ndk::ScopedAStatus JTvInputHal::ITvInputWrapper::setPictureProfileId(int32_t deviceId, + int32_t streamId, + long profileHandle) { + if (mIsHidl) { + return ::ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION); + } else { + return mAidlTvInput->setPictureProfileId(deviceId, streamId, profileHandle); + } +} + ::ndk::ScopedAStatus JTvInputHal::ITvInputWrapper::getTvMessageQueueDesc( MQDescriptor<int8_t, SynchronizedReadWrite>* out_queue, int32_t in_deviceId, int32_t in_streamId) { diff --git a/services/core/jni/tvinput/JTvInputHal.h b/services/core/jni/tvinput/JTvInputHal.h index 2ef94ac4a3b0..4481f1d37c2b 100644 --- a/services/core/jni/tvinput/JTvInputHal.h +++ b/services/core/jni/tvinput/JTvInputHal.h @@ -85,6 +85,7 @@ public: int addOrUpdateStream(int deviceId, int streamId, const sp<Surface>& surface); int setTvMessageEnabled(int deviceId, int streamId, int type, bool enabled); + int setPictureProfileId(int deviceId, int streamId, long profileHandle); int removeStream(int deviceId, int streamId); const std::vector<AidlTvStreamConfig> getStreamConfigs(int deviceId); @@ -208,6 +209,7 @@ private: ::ndk::ScopedAStatus closeStream(int32_t in_deviceId, int32_t in_streamId); ::ndk::ScopedAStatus setTvMessageEnabled(int32_t deviceId, int32_t streamId, TvMessageEventType in_type, bool enabled); + ::ndk::ScopedAStatus setPictureProfileId(int deviceId, int streamId, long profileHandle); ::ndk::ScopedAStatus getTvMessageQueueDesc( MQDescriptor<int8_t, SynchronizedReadWrite>* out_queue, int32_t in_deviceId, int32_t in_streamId); diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java index 9781fb9a1830..e4c214fd93e6 100644 --- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java @@ -27,6 +27,7 @@ import android.credentials.CreateCredentialResponse; import android.credentials.CredentialManager; import android.credentials.CredentialProviderInfo; import android.credentials.ICreateCredentialCallback; +import android.credentials.flags.Flags; import android.credentials.selection.ProviderData; import android.credentials.selection.RequestInfo; import android.os.CancellationSignal; @@ -145,6 +146,9 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR if (response != null) { mRequestSessionMetric.collectChosenProviderStatus( ProviderStatusForMetrics.FINAL_SUCCESS.getMetricCode()); + if (Flags.fixMetricDuplicationEmits()) { + mRequestSessionMetric.collectChosenClassType(mClientRequest.getType()); + } respondToClientWithResponseAndFinish(response); } else { mRequestSessionMetric.collectChosenProviderStatus( diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java index be36b6c5690b..fd00f6dde815 100644 --- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java @@ -27,6 +27,7 @@ import android.credentials.GetCredentialException; import android.credentials.GetCredentialRequest; import android.credentials.GetCredentialResponse; import android.credentials.IGetCredentialCallback; +import android.credentials.flags.Flags; import android.credentials.selection.ProviderData; import android.credentials.selection.RequestInfo; import android.os.Binder; @@ -146,6 +147,12 @@ public class GetRequestSession extends RequestSession<GetCredentialRequest, if (response != null) { mRequestSessionMetric.collectChosenProviderStatus( ProviderStatusForMetrics.FINAL_SUCCESS.getMetricCode()); + if (Flags.fixMetricDuplicationEmits()) { + if (response.getCredential() != null) { + mRequestSessionMetric.collectChosenClassType(response.getCredential() + .getType()); + } + } respondToClientWithResponseAndFinish(response); } else { mRequestSessionMetric.collectChosenProviderStatus( diff --git a/services/credentials/java/com/android/server/credentials/MetricUtilities.java b/services/credentials/java/com/android/server/credentials/MetricUtilities.java index 11edb93dffea..9c89f4c31fc6 100644 --- a/services/credentials/java/com/android/server/credentials/MetricUtilities.java +++ b/services/credentials/java/com/android/server/credentials/MetricUtilities.java @@ -201,7 +201,8 @@ public class MetricUtilities { finalPhaseMetric.getResponseCollective().getUniqueResponseCounts(), /* framework_exception_unique_classtype */ finalPhaseMetric.getFrameworkException(), - /* primary_indicated */ finalPhaseMetric.isPrimary() + /* primary_indicated */ finalPhaseMetric.isPrimary(), + /* chosen_classtype */ finalPhaseMetric.getChosenClassType() ); } catch (Exception e) { Slog.w(TAG, "Unexpected error during final provider uid emit: " + e); @@ -587,7 +588,8 @@ public class MetricUtilities { /* primary_indicated */ finalPhaseMetric.isPrimary(), /* oem_credential_manager_ui_uid */ finalPhaseMetric.getOemUiUid(), /* fallback_credential_manager_ui_uid */ finalPhaseMetric.getFallbackUiUid(), - /* oem_ui_usage_status */ finalPhaseMetric.getOemUiUsageStatus() + /* oem_ui_usage_status */ finalPhaseMetric.getOemUiUsageStatus(), + /* chosen_classtype */ finalPhaseMetric.getChosenClassType() ); } catch (Exception e) { Slog.w(TAG, "Unexpected error during final no uid metric logging: " + e); diff --git a/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderFinalPhaseMetric.java b/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderFinalPhaseMetric.java index 9dd6db6f9d6a..c8f6ba06d020 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderFinalPhaseMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderFinalPhaseMetric.java @@ -83,12 +83,24 @@ public class ChosenProviderFinalPhaseMetric { // Indicates if this chosen provider was the primary provider, false by default private boolean mIsPrimary = false; + private String mChosenClassType = ""; + public ChosenProviderFinalPhaseMetric(int sessionIdCaller, int sessionIdProvider) { mSessionIdCaller = sessionIdCaller; mSessionIdProvider = sessionIdProvider; } + /* ------------------- Chosen Credential ------------------- */ + + public void setChosenClassType(String clickedClassType) { + mChosenClassType = clickedClassType; + } + + public String getChosenClassType() { + return mChosenClassType; + } + /* ------------------- UID ------------------- */ public int getChosenUid() { diff --git a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java index dc1747f803ea..a2d6510007bc 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java @@ -262,6 +262,21 @@ public class RequestSessionMetric { } /** + * This collects the final chosen class type. While it is possible to collect this during + * browsing, note this only collects the final tapped bit. + * + * @param createOrCredentialType the string type to collect when an entry is tapped by the user + */ + public void collectChosenClassType(String createOrCredentialType) { + String truncatedType = generateMetricKey(createOrCredentialType, DELTA_RESPONSES_CUT); + try { + mChosenProviderFinalPhaseMetric.setChosenClassType(truncatedType); + } catch (Exception e) { + Slog.i(TAG, "Unexpected error collecting chosen class type metadata: " + e); + } + } + + /** * Updates the final phase metric with the designated bit. * * @param exceptionBitFinalPhase represents if the final phase provider had an exception diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index f055febca3d5..964826d1ee73 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -9628,32 +9628,30 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } - // TODO: with a quick glance this logic seems incomplete that it doesn't properly handle - // the different behaviour between a profile with separate challenge vs a profile with - // unified challenge, which was part of getActiveAdminsForLockscreenPoliciesLocked() - // before the migration. if (Flags.setKeyguardDisabledFeaturesCoexistence()) { - Integer features = mDevicePolicyEngine.getResolvedPolicy( - PolicyDefinition.KEYGUARD_DISABLED_FEATURES, - affectedUserId); - return Binder.withCleanCallingIdentity(() -> { - int combinedFeatures = features == null ? 0 : features; - List<UserInfo> profiles = mUserManager.getProfiles(affectedUserId); - for (UserInfo profile : profiles) { - int profileId = profile.id; - if (profileId == affectedUserId) { + if (!parent && isManagedProfile(userHandle)) { + return mDevicePolicyEngine.getResolvedPolicy( + PolicyDefinition.KEYGUARD_DISABLED_FEATURES, userHandle); + } + + int targetUserId = getProfileParentUserIfRequested(userHandle, parent); + + int combinedPolicy = DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE; + for (UserInfo profile : mUserManager.getProfiles(targetUserId)) { + if (mLockPatternUtils.isSeparateProfileChallengeEnabled(profile.id)) { continue; } - Integer profileFeatures = mDevicePolicyEngine.getResolvedPolicy( - PolicyDefinition.KEYGUARD_DISABLED_FEATURES, - profileId); - if (profileFeatures != null) { - combinedFeatures |= (profileFeatures - & PROFILE_KEYGUARD_FEATURES_AFFECT_OWNER); + + Integer profilePolicy = mDevicePolicyEngine.getResolvedPolicy( + PolicyDefinition.KEYGUARD_DISABLED_FEATURES, profile.id); + profilePolicy = profilePolicy == null ? 0 : profilePolicy; + if (profile.id != userHandle) { + profilePolicy &= PROFILE_KEYGUARD_FEATURES_AFFECT_OWNER; } + combinedPolicy |= profilePolicy; } - return combinedFeatures; + return combinedPolicy; }); } @@ -23905,10 +23903,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { UserHandle.USER_ALL); synchronized (getLockObject()) { - final EnforcingAdmin admin = enforcePermissionAndGetEnforcingAdmin(null, - MANAGE_DEVICE_POLICY_MTE, callerPackageName, caller.getUserId()); - final Integer policyFromAdmin = mDevicePolicyEngine.getGlobalPolicySetByAdmin( - PolicyDefinition.MEMORY_TAGGING, admin); + final Integer policyFromAdmin = mDevicePolicyEngine.getResolvedPolicy( + PolicyDefinition.MEMORY_TAGGING, UserHandle.USER_ALL); + return (policyFromAdmin != null ? policyFromAdmin : DevicePolicyManager.MTE_NOT_CONTROLLED_BY_POLICY); } diff --git a/services/java/com/android/server/flags.aconfig b/services/java/com/android/server/flags.aconfig index 7a6bd75e5893..f864b6b8c768 100644 --- a/services/java/com/android/server/flags.aconfig +++ b/services/java/com/android/server/flags.aconfig @@ -31,6 +31,13 @@ flag { } flag { + namespace: "system_performance" + name: "enable_theme_service" + description: "Switches from SystemUi's ThemeOverlayController to Server's ThemeService." + bug: "333694176" +} + +flag { name: "allow_removing_vpn_service" namespace: "wear_frameworks" description: "Allow removing VpnManagerService" diff --git a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt index 1bb395441587..410539c8c5d0 100644 --- a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt @@ -431,7 +431,7 @@ private constructor( companion object { private val LOG_TAG = AccessPolicy::class.java.simpleName - internal const val VERSION_LATEST = 16 + internal const val VERSION_LATEST = 17 private const val TAG_ACCESS = "access" private const val TAG_DEFAULT_PERMISSION_GRANT = "default-permission-grant" diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt index 5a140d53a4d8..f4d7a8ec5484 100644 --- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt @@ -21,6 +21,7 @@ import android.content.pm.PackageManager import android.content.pm.PermissionGroupInfo import android.content.pm.PermissionInfo import android.content.pm.SigningDetails +import android.health.connect.HealthPermissions import android.os.Build import android.permission.flags.Flags import android.util.Slog @@ -100,7 +101,7 @@ class AppIdPermissionPolicy : SchemePolicy() { override fun MutateStateScope.onStorageVolumeMounted( volumeUuid: String?, packageNames: List<String>, - isSystemUpdated: Boolean + isSystemUpdated: Boolean, ) { val changedPermissionNames = MutableIndexedSet<String>() packageNames.forEachIndexed { _, packageName -> @@ -112,7 +113,6 @@ class AppIdPermissionPolicy : SchemePolicy() { addPermissions(packageState, changedPermissionNames) trimPermissions(packageState.packageName, changedPermissionNames) trimPermissionStates(packageState.appId) - revokePermissionsOnPackageUpdate(packageState.appId) } changedPermissionNames.forEachIndexed { _, permissionName -> evaluatePermissionStateForAllPackages(permissionName, null) @@ -130,6 +130,7 @@ class AppIdPermissionPolicy : SchemePolicy() { newState.externalState.userIds.forEachIndexed { _, userId -> inheritImplicitPermissionStates(packageState.appId, userId) } + revokePermissionsOnPackageUpdate(packageState.appId) } } @@ -140,7 +141,6 @@ class AppIdPermissionPolicy : SchemePolicy() { addPermissions(packageState, changedPermissionNames) trimPermissions(packageState.packageName, changedPermissionNames) trimPermissionStates(packageState.appId) - revokePermissionsOnPackageUpdate(packageState.appId) changedPermissionNames.forEachIndexed { _, permissionName -> evaluatePermissionStateForAllPackages(permissionName, null) } @@ -148,6 +148,7 @@ class AppIdPermissionPolicy : SchemePolicy() { newState.externalState.userIds.forEachIndexed { _, userId -> inheritImplicitPermissionStates(packageState.appId, userId) } + revokePermissionsOnPackageUpdate(packageState.appId) } override fun MutateStateScope.onPackageRemoved(packageName: String, appId: Int) { @@ -173,7 +174,7 @@ class AppIdPermissionPolicy : SchemePolicy() { private fun MutateStateScope.clearRestrictedPermissionImplicitExemption( packageState: PackageState, - userId: Int + userId: Int, ) { // System apps can always retain their UPGRADE_EXEMPT. if (packageState.isSystem) { @@ -198,7 +199,7 @@ class AppIdPermissionPolicy : SchemePolicy() { userId, permission, PermissionFlags.UPGRADE_EXEMPT, - 0 + 0, ) } } @@ -208,7 +209,7 @@ class AppIdPermissionPolicy : SchemePolicy() { userId: Int, permission: Permission, exemptFlagMask: Int, - exemptFlagValues: Int + exemptFlagValues: Int, ) { val permissionName = permission.name val oldFlags = getPermissionFlags(appId, userId, permissionName) @@ -236,7 +237,7 @@ class AppIdPermissionPolicy : SchemePolicy() { isSoftRestrictedPermissionExemptForPackage( it, targetSdkVersion, - permissionName + permissionName, ) } } else { @@ -257,7 +258,7 @@ class AppIdPermissionPolicy : SchemePolicy() { override fun MutateStateScope.onPackageUninstalled( packageName: String, appId: Int, - userId: Int + userId: Int, ) { resetRuntimePermissions(packageName, userId) } @@ -290,17 +291,16 @@ class AppIdPermissionPolicy : SchemePolicy() { packageState.isSystem || packageState.getUserStateOrDefault(userId).isInstalled newFlags = if ( - isSystemOrInstalled && ( - newFlags.hasBits(PermissionFlags.ROLE) || - newFlags.hasBits(PermissionFlags.PREGRANT) - ) + isSystemOrInstalled && + (newFlags.hasBits(PermissionFlags.ROLE) || + newFlags.hasBits(PermissionFlags.PREGRANT)) ) { newFlags or PermissionFlags.RUNTIME_GRANTED } else { - newFlags andInv ( - PermissionFlags.RUNTIME_GRANTED or PermissionFlags.ROLE or - PermissionFlags.PREGRANT - ) + newFlags andInv + (PermissionFlags.RUNTIME_GRANTED or + PermissionFlags.ROLE or + PermissionFlags.PREGRANT) } newFlags = newFlags andInv USER_SETTABLE_MASK if (newFlags.hasBits(PermissionFlags.LEGACY_GRANTED)) { @@ -312,7 +312,7 @@ class AppIdPermissionPolicy : SchemePolicy() { private fun MutateStateScope.adoptPermissions( packageState: PackageState, - changedPermissionNames: MutableIndexedSet<String> + changedPermissionNames: MutableIndexedSet<String>, ) { val `package` = packageState.androidPackage!! `package`.adoptPermissions.forEachIndexed { _, originalPackageName -> @@ -341,7 +341,7 @@ class AppIdPermissionPolicy : SchemePolicy() { oldPermission.copy( permissionInfo = newPermissionInfo, isReconciled = false, - appId = 0 + appId = 0, ) newState .mutateSystemState() @@ -354,7 +354,7 @@ class AppIdPermissionPolicy : SchemePolicy() { private fun MutateStateScope.canAdoptPermissions( packageName: String, - originalPackageName: String + originalPackageName: String, ): Boolean { val originalPackageState = newState.externalState.packageStates[originalPackageName] ?: return false @@ -362,7 +362,7 @@ class AppIdPermissionPolicy : SchemePolicy() { Slog.w( LOG_TAG, "Unable to adopt permissions from $originalPackageName to $packageName:" + - " original package not in system partition" + " original package not in system partition", ) return false } @@ -370,7 +370,7 @@ class AppIdPermissionPolicy : SchemePolicy() { Slog.w( LOG_TAG, "Unable to adopt permissions from $originalPackageName to $packageName:" + - " original package still exists" + " original package still exists", ) return false } @@ -386,7 +386,7 @@ class AppIdPermissionPolicy : SchemePolicy() { Slog.w( LOG_TAG, "Ignoring permission groups declared in package" + - " ${packageState.packageName}: instant apps cannot declare permission groups" + " ${packageState.packageName}: instant apps cannot declare permission groups", ) return } @@ -394,7 +394,7 @@ class AppIdPermissionPolicy : SchemePolicy() { val newPermissionGroup = PackageInfoUtils.generatePermissionGroupInfo( parsedPermissionGroup, - PackageManager.GET_META_DATA.toLong() + PackageManager.GET_META_DATA.toLong(), )!! // TODO: Clear permission state on group take-over? val permissionGroupName = newPermissionGroup.name @@ -414,7 +414,7 @@ class AppIdPermissionPolicy : SchemePolicy() { LOG_TAG, "Ignoring permission group $permissionGroupName declared in" + " package $newPackageName: already declared in another" + - " package $oldPackageName" + " package $oldPackageName", ) return@forEachIndexed } @@ -423,7 +423,7 @@ class AppIdPermissionPolicy : SchemePolicy() { LOG_TAG, "Ignoring permission group $permissionGroupName declared in" + " system package $newPackageName: already declared in another" + - " system package $oldPackageName" + " system package $oldPackageName", ) return@forEachIndexed } @@ -431,7 +431,7 @@ class AppIdPermissionPolicy : SchemePolicy() { LOG_TAG, "Overriding permission group $permissionGroupName with" + " new declaration in system package $newPackageName: originally" + - " declared in another package $oldPackageName" + " declared in another package $oldPackageName", ) } newState.mutateSystemState().mutatePermissionGroups()[permissionGroupName] = @@ -441,7 +441,7 @@ class AppIdPermissionPolicy : SchemePolicy() { private fun MutateStateScope.addPermissions( packageState: PackageState, - changedPermissionNames: MutableIndexedSet<String> + changedPermissionNames: MutableIndexedSet<String>, ) { val androidPackage = packageState.androidPackage!! // This may not be the same package as the old permission because the old permission owner @@ -454,7 +454,7 @@ class AppIdPermissionPolicy : SchemePolicy() { val newPermissionInfo = PackageInfoUtils.generatePermissionInfo( parsedPermission, - PackageManager.GET_META_DATA.toLong() + PackageManager.GET_META_DATA.toLong(), )!! val permissionName = newPermissionInfo.name val oldPermission = @@ -474,7 +474,7 @@ class AppIdPermissionPolicy : SchemePolicy() { LOG_TAG, "Ignoring permission $permissionName declared in package" + " $newPackageName: base permission tree ${permissionTree.name} is" + - " declared in another package ${permissionTree.packageName}" + " declared in another package ${permissionTree.packageName}", ) return@forEachIndexed } @@ -488,7 +488,7 @@ class AppIdPermissionPolicy : SchemePolicy() { LOG_TAG, "Ignoring permission $permissionName declared in package" + " $newPackageName: already declared in another package" + - " $oldPackageName" + " $oldPackageName", ) return@forEachIndexed } @@ -497,7 +497,7 @@ class AppIdPermissionPolicy : SchemePolicy() { LOG_TAG, "Ignoring permission $permissionName declared in system package" + " $newPackageName: already declared in another system package" + - " $oldPackageName" + " $oldPackageName", ) return@forEachIndexed } @@ -505,7 +505,7 @@ class AppIdPermissionPolicy : SchemePolicy() { LOG_TAG, "Overriding permission $permissionName with new declaration in" + " system package $newPackageName: originally declared in another" + - " package $oldPackageName" + " package $oldPackageName", ) // Remove permission state on owner change. newState.externalState.userIds.forEachIndexed { _, userId -> @@ -534,7 +534,7 @@ class AppIdPermissionPolicy : SchemePolicy() { "Revoking runtime permission $permissionName for" + " appId $appId and userId $userId as the permission" + " group changed from ${oldPermission.groupName}" + - " to ${newPermissionInfo.group}" + " to ${newPermissionInfo.group}", ) } if (isPermissionProtectionChanged) { @@ -542,7 +542,7 @@ class AppIdPermissionPolicy : SchemePolicy() { LOG_TAG, "Revoking permission $permissionName for" + " appId $appId and userId $userId as the permission" + - " protection changed." + " protection changed.", ) } setPermissionFlags(appId, userId, permissionName, 0) @@ -572,7 +572,7 @@ class AppIdPermissionPolicy : SchemePolicy() { Permission.TYPE_MANIFEST, packageState.appId, gids, - areGidsPerUser + areGidsPerUser, ) if (parsedPermission.isTree) { @@ -599,7 +599,7 @@ class AppIdPermissionPolicy : SchemePolicy() { private fun MutateStateScope.trimPermissions( packageName: String, - changedPermissionNames: MutableIndexedSet<String> + changedPermissionNames: MutableIndexedSet<String>, ) { val packageState = newState.externalState.packageStates[packageName] val androidPackage = packageState?.androidPackage @@ -675,7 +675,7 @@ class AppIdPermissionPolicy : SchemePolicy() { packageName = permissionTree.packageName }, appId = permissionTree.appId, - isReconciled = true + isReconciled = true, ) } @@ -701,6 +701,11 @@ class AppIdPermissionPolicy : SchemePolicy() { } private fun MutateStateScope.revokePermissionsOnPackageUpdate(appId: Int) { + revokeStorageAndMediaPermissionsOnPackageUpdate(appId) + revokeHeartRatePermissionsOnPackageUpdate(appId) + } + + private fun MutateStateScope.revokeStorageAndMediaPermissionsOnPackageUpdate(appId: Int) { val hasOldPackage = appId in oldState.externalState.appIdPackageNames && anyPackageInAppId(appId, oldState) { true } @@ -748,26 +753,157 @@ class AppIdPermissionPolicy : SchemePolicy() { // SYSTEM_FIXED. Otherwise the user cannot grant back the permission. if ( permissionName in STORAGE_AND_MEDIA_PERMISSIONS && - oldFlags.hasBits(PermissionFlags.RUNTIME_GRANTED) && - !oldFlags.hasAnyBit(SYSTEM_OR_POLICY_FIXED_MASK) + oldFlags.hasBits(PermissionFlags.RUNTIME_GRANTED) ) { - Slog.v( - LOG_TAG, - "Revoking storage permission: $permissionName for appId: " + - " $appId and user: $userId" + revokeRuntimePermission(appId, userId, permissionName) + } + } + } + } + } + + /** + * If the app is updated, the legacy BODY_SENSOR and READ_HEART_RATE permissions may go out of + * sync (for example, when the app eventually requests the implicit new permission). If this + * occurs, revoke both permissions to force a re-prompt. + */ + private fun MutateStateScope.revokeHeartRatePermissionsOnPackageUpdate(appId: Int) { + val targetSdkVersion = getAppIdTargetSdkVersion(appId, null) + // Apps targeting BAKLAVA and above shouldn't be using BODY_SENSORS. + if (targetSdkVersion >= Build.VERSION_CODES.BAKLAVA) { + return + } + + val isBodySensorsRequested = + anyPackageInAppId(appId, newState) { + Manifest.permission.BODY_SENSORS in it.androidPackage!!.requestedPermissions + } + val isReadHeartRateRequested = + anyPackageInAppId(appId, newState) { + HealthPermissions.READ_HEART_RATE in it.androidPackage!!.requestedPermissions + } + val isBodySensorsBackgroundRequested = + anyPackageInAppId(appId, newState) { + Manifest.permission.BODY_SENSORS_BACKGROUND in + it.androidPackage!!.requestedPermissions + } + val isReadHealthDataInBackgroundRequested = + anyPackageInAppId(appId, newState) { + HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND in + it.androidPackage!!.requestedPermissions + } + + // Walk the list of user IDs and revoke states as needed. + newState.userStates.forEachIndexed { _, userId, _ -> + // First sync BODY_SENSORS and READ_HEART_RATE, if required. + var isBodySensorsGranted = + isRuntimePermissionGranted(appId, userId, Manifest.permission.BODY_SENSORS) + if (isBodySensorsRequested && isReadHeartRateRequested) { + val isReadHeartRateGranted = + isRuntimePermissionGranted(appId, userId, HealthPermissions.READ_HEART_RATE) + if (isBodySensorsGranted != isReadHeartRateGranted) { + if (isBodySensorsGranted) { + if ( + revokeRuntimePermission(appId, userId, Manifest.permission.BODY_SENSORS) + ) { + isBodySensorsGranted = false + } + } + if (isReadHeartRateGranted) { + revokeRuntimePermission(appId, userId, HealthPermissions.READ_HEART_RATE) + } + } + } + + // Then check to ensure we haven't put the background/foreground permissions out of + // sync. + var isBodySensorsBackgroundGranted = + isRuntimePermissionGranted( + appId, + userId, + Manifest.permission.BODY_SENSORS_BACKGROUND, + ) + if (isBodySensorsBackgroundGranted && !isBodySensorsGranted) { + if ( + revokeRuntimePermission( + appId, + userId, + Manifest.permission.BODY_SENSORS_BACKGROUND, + ) + ) { + isBodySensorsBackgroundGranted = false + } + } + + // Finally sync BODY_SENSORS_BACKGROUND and READ_HEALTH_DATA_IN_BACKGROUND, if required. + if (isBodySensorsBackgroundRequested && isReadHealthDataInBackgroundRequested) { + val isReadHealthDataInBackgroundGranted = + isRuntimePermissionGranted( + appId, + userId, + HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND, + ) + if (isBodySensorsBackgroundGranted != isReadHealthDataInBackgroundGranted) { + if (isBodySensorsBackgroundGranted) { + revokeRuntimePermission( + appId, + userId, + Manifest.permission.BODY_SENSORS_BACKGROUND, + ) + } + if (isReadHealthDataInBackgroundGranted) { + revokeRuntimePermission( + appId, + userId, + HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND, ) - val newFlags = - oldFlags andInv (PermissionFlags.RUNTIME_GRANTED or USER_SETTABLE_MASK) - setPermissionFlags(appId, userId, permissionName, newFlags) } } } } } + private fun GetStateScope.isRuntimePermissionGranted( + appId: Int, + userId: Int, + permissionName: String, + ): Boolean { + val flags = getPermissionFlags(appId, userId, permissionName) + return PermissionFlags.isAppOpGranted(flags) + } + + fun MutateStateScope.revokeRuntimePermission( + appId: Int, + userId: Int, + permissionName: String, + ): Boolean { + Slog.v( + LOG_TAG, + "Revoking runtime permission for appId: $appId, " + + "permission: $permissionName, userId: $userId", + ) + var flags = getPermissionFlags(appId, userId, permissionName) + if (flags.hasAnyBit(SYSTEM_OR_POLICY_FIXED_MASK)) { + Slog.v( + LOG_TAG, + "Not allowed to revoke $permissionName for appId: $appId, userId: $userId", + ) + return false + } + + flags = + flags andInv + (PermissionFlags.RUNTIME_GRANTED or + USER_SETTABLE_MASK or + PermissionFlags.PREGRANT or + PermissionFlags.ROLE) + setPermissionFlags(appId, userId, permissionName, flags) + return true + } + private fun MutateStateScope.evaluatePermissionStateForAllPackages( permissionName: String, - installedPackageState: PackageState? + installedPackageState: PackageState?, ) { val externalState = newState.externalState externalState.userIds.forEachIndexed { _, userId -> @@ -785,13 +921,13 @@ class AppIdPermissionPolicy : SchemePolicy() { private fun MutateStateScope.evaluateAllPermissionStatesForPackage( packageState: PackageState, - installedPackageState: PackageState? + installedPackageState: PackageState?, ) { newState.externalState.userIds.forEachIndexed { _, userId -> evaluateAllPermissionStatesForPackageAndUser( packageState, userId, - installedPackageState + installedPackageState, ) } } @@ -799,14 +935,14 @@ class AppIdPermissionPolicy : SchemePolicy() { private fun MutateStateScope.evaluateAllPermissionStatesForPackageAndUser( packageState: PackageState, userId: Int, - installedPackageState: PackageState? + installedPackageState: PackageState?, ) { packageState.androidPackage?.requestedPermissions?.forEach { permissionName -> evaluatePermissionState( packageState.appId, userId, permissionName, - installedPackageState + installedPackageState, ) } } @@ -815,7 +951,7 @@ class AppIdPermissionPolicy : SchemePolicy() { appId: Int, userId: Int, permissionName: String, - installedPackageState: PackageState? + installedPackageState: PackageState?, ) { val packageNames = newState.externalState.appIdPackageNames[appId]!! // Repeatedly checking whether a permission is requested can actually be costly, so we cache @@ -989,8 +1125,7 @@ class AppIdPermissionPolicy : SchemePolicy() { "Unknown source permission $sourcePermissionName in split permissions" } !sourcePermission.isRuntime - } - ?: false + } ?: false val shouldGrantByImplicit = isLeanbackNotificationsPermission || (isImplicitPermission && isAnySourcePermissionNonRuntime) @@ -1024,7 +1159,7 @@ class AppIdPermissionPolicy : SchemePolicy() { getPermissionFlags( appId, userId, - Manifest.permission.ACCESS_BACKGROUND_LOCATION + Manifest.permission.ACCESS_BACKGROUND_LOCATION, ) shouldRetainAsNearbyDevices = PermissionFlags.isAppOpGranted(accessBackgroundLocationFlags) && @@ -1081,7 +1216,7 @@ class AppIdPermissionPolicy : SchemePolicy() { isSoftRestrictedPermissionExemptForPackage( it, targetSdkVersion, - permissionName + permissionName, ) } ) { @@ -1095,7 +1230,7 @@ class AppIdPermissionPolicy : SchemePolicy() { LOG_TAG, "Unknown protection level ${permission.protectionLevel}" + "for permission ${permission.name} while evaluating permission state" + - "for appId $appId and userId $userId" + "for appId $appId and userId $userId", ) } } @@ -1154,7 +1289,7 @@ class AppIdPermissionPolicy : SchemePolicy() { private fun isCompatibilityPermissionForPackage( androidPackage: AndroidPackage, - permissionName: String + permissionName: String, ): Boolean { for (compatibilityPermission in CompatibilityPermissionInfo.COMPAT_PERMS) { if ( @@ -1164,7 +1299,7 @@ class AppIdPermissionPolicy : SchemePolicy() { Slog.i( LOG_TAG, "Auto-granting $permissionName to old package" + - " ${androidPackage.packageName}" + " ${androidPackage.packageName}", ) return true } @@ -1174,7 +1309,7 @@ class AppIdPermissionPolicy : SchemePolicy() { private fun MutateStateScope.shouldGrantPermissionBySignature( packageState: PackageState, - permission: Permission + permission: Permission, ): Boolean { // Check if the package is allowed to use this signature permission. A package is allowed // to use a signature permission if: @@ -1197,12 +1332,12 @@ class AppIdPermissionPolicy : SchemePolicy() { val hasCommonSigner = sourceSigningDetails?.hasCommonSignerWithCapability( packageSigningDetails, - SigningDetails.CertCapabilities.PERMISSION + SigningDetails.CertCapabilities.PERMISSION, ) == true || packageSigningDetails.hasAncestorOrSelf(platformSigningDetails) || platformSigningDetails.checkCapability( packageSigningDetails, - SigningDetails.CertCapabilities.PERMISSION + SigningDetails.CertCapabilities.PERMISSION, ) if (!Flags.signaturePermissionAllowlistEnabled()) { return hasCommonSigner @@ -1237,7 +1372,7 @@ class AppIdPermissionPolicy : SchemePolicy() { LOG_TAG, "Signature permission ${permission.name} for package" + " ${packageState.packageName} (${packageState.path}) not in" + - " signature permission allowlist" + " signature permission allowlist", ) if (!Build.isDebuggable() || isSignaturePermissionAllowlistForceEnforced) { return false @@ -1249,7 +1384,7 @@ class AppIdPermissionPolicy : SchemePolicy() { private fun MutateStateScope.getSignaturePermissionAllowlistState( packageState: PackageState, - permissionName: String + permissionName: String, ): Boolean? { val permissionAllowlist = newState.externalState.permissionAllowlist val packageName = packageState.packageName @@ -1259,30 +1394,30 @@ class AppIdPermissionPolicy : SchemePolicy() { packageState.isProduct -> permissionAllowlist.getProductSignatureAppAllowlistState( packageName, - permissionName + permissionName, ) packageState.isSystemExt -> permissionAllowlist.getSystemExtSignatureAppAllowlistState( packageName, - permissionName + permissionName, ) else -> permissionAllowlist.getApexSignatureAppAllowlistState(packageName, permissionName) ?: permissionAllowlist.getProductSignatureAppAllowlistState( packageName, - permissionName + permissionName, ) ?: permissionAllowlist.getVendorSignatureAppAllowlistState( packageName, - permissionName + permissionName, ) ?: permissionAllowlist.getSystemExtSignatureAppAllowlistState( packageName, - permissionName + permissionName, ) ?: permissionAllowlist.getSignatureAppAllowlistState( packageName, - permissionName + permissionName, ) } } @@ -1292,13 +1427,13 @@ class AppIdPermissionPolicy : SchemePolicy() { * or for normal apps, we return true to indicate that we don't need to check the allowlist and * will let follow-up checks to decide whether we should grant the permission. * - * @return `true`, if the permission is allowlisted for system privileged apps, or if we - * don't need to check the allowlist (for platform or for normal apps). - * `false`, if the permission is not allowlisted for system privileged apps. + * @return `true`, if the permission is allowlisted for system privileged apps, or if we don't + * need to check the allowlist (for platform or for normal apps). `false`, if the permission + * is not allowlisted for system privileged apps. */ private fun MutateStateScope.checkPrivilegedPermissionAllowlistIfNeeded( packageState: PackageState, - permission: Permission + permission: Permission, ): Boolean { if (RoSystemProperties.CONTROL_PRIVAPP_PERMISSIONS_DISABLE) { return true @@ -1330,7 +1465,7 @@ class AppIdPermissionPolicy : SchemePolicy() { LOG_TAG, "Privileged permission ${permission.name} for package" + " ${packageState.packageName} (${packageState.path}) not in" + - " privileged permission allowlist" + " privileged permission allowlist", ) if (RoSystemProperties.CONTROL_PRIVAPP_PERMISSIONS_ENFORCE) { privilegedPermissionAllowlistViolations += @@ -1348,7 +1483,7 @@ class AppIdPermissionPolicy : SchemePolicy() { */ private fun MutateStateScope.getPrivilegedPermissionAllowlistState( packageState: PackageState, - permissionName: String + permissionName: String, ): Boolean? { val permissionAllowlist = newState.externalState.permissionAllowlist val apexModuleName = packageState.apexModuleName @@ -1357,17 +1492,17 @@ class AppIdPermissionPolicy : SchemePolicy() { packageState.isVendor || packageState.isOdm -> permissionAllowlist.getVendorPrivilegedAppAllowlistState( packageName, - permissionName + permissionName, ) packageState.isProduct -> permissionAllowlist.getProductPrivilegedAppAllowlistState( packageName, - permissionName + permissionName, ) packageState.isSystemExt -> permissionAllowlist.getSystemExtPrivilegedAppAllowlistState( packageName, - permissionName + permissionName, ) apexModuleName != null -> { val nonApexAllowlistState = @@ -1379,14 +1514,14 @@ class AppIdPermissionPolicy : SchemePolicy() { LOG_TAG, "Package $packageName is an APK in APEX but has permission" + " allowlist on the system image, please bundle the allowlist in the" + - " $apexModuleName APEX instead" + " $apexModuleName APEX instead", ) } val apexAllowlistState = permissionAllowlist.getApexPrivilegedAppAllowlistState( apexModuleName, packageName, - permissionName + permissionName, ) apexAllowlistState ?: nonApexAllowlistState } @@ -1403,7 +1538,7 @@ class AppIdPermissionPolicy : SchemePolicy() { private fun isSoftRestrictedPermissionExemptForPackage( packageState: PackageState, appIdTargetSdkVersion: Int, - permissionName: String + permissionName: String, ): Boolean = when (permissionName) { Manifest.permission.READ_EXTERNAL_STORAGE, @@ -1415,7 +1550,7 @@ class AppIdPermissionPolicy : SchemePolicy() { private fun MutateStateScope.getAppIdTargetSdkVersion( appId: Int, permissionName: String?, - state: AccessState = newState + state: AccessState = newState, ): Int = reducePackageInAppId(appId, Build.VERSION_CODES.CUR_DEVELOPMENT, state) { targetSdkVersion, @@ -1431,7 +1566,7 @@ class AppIdPermissionPolicy : SchemePolicy() { private inline fun MutateStateScope.anyPackageInAppId( appId: Int, state: AccessState = newState, - predicate: (PackageState) -> Boolean + predicate: (PackageState) -> Boolean, ): Boolean { val packageNames = state.externalState.appIdPackageNames[appId]!! return packageNames.anyIndexed { _, packageName -> @@ -1443,7 +1578,7 @@ class AppIdPermissionPolicy : SchemePolicy() { private inline fun MutateStateScope.forEachPackageInAppId( appId: Int, state: AccessState = newState, - action: (PackageState) -> Unit + action: (PackageState) -> Unit, ) { val packageNames = state.externalState.appIdPackageNames[appId]!! packageNames.forEachIndexed { _, packageName -> @@ -1459,7 +1594,7 @@ class AppIdPermissionPolicy : SchemePolicy() { appId: Int, initialValue: Int, state: AccessState = newState, - accumulator: (Int, PackageState) -> Int + accumulator: (Int, PackageState) -> Int, ): Int { val packageNames = state.externalState.appIdPackageNames[appId]!! return packageNames.reduceIndexed(initialValue) { value, _, packageName -> @@ -1474,7 +1609,7 @@ class AppIdPermissionPolicy : SchemePolicy() { private fun MutateStateScope.shouldGrantPermissionByProtectionFlags( packageState: PackageState, - permission: Permission + permission: Permission, ): Boolean { val androidPackage = packageState.androidPackage!! val knownPackages = newState.externalState.knownPackages @@ -1587,7 +1722,7 @@ class AppIdPermissionPolicy : SchemePolicy() { private fun MutateStateScope.shouldGrantPrivilegedOrOemPermission( packageState: PackageState, - permission: Permission + permission: Permission, ): Boolean { val permissionName = permission.name val packageName = packageState.packageName @@ -1605,7 +1740,7 @@ class AppIdPermissionPolicy : SchemePolicy() { LOG_TAG, "Permission $permissionName cannot be granted to privileged" + " vendor (or odm) app $packageName because it isn't a" + - " vendorPrivileged permission" + " vendorPrivileged permission", ) return false } @@ -1617,7 +1752,7 @@ class AppIdPermissionPolicy : SchemePolicy() { val allowlistState = newState.externalState.permissionAllowlist.getOemAppAllowlistState( packageName, - permissionName + permissionName, ) checkNotNull(allowlistState) { "OEM permission $permissionName requested by package" + @@ -1688,7 +1823,7 @@ class AppIdPermissionPolicy : SchemePolicy() { fun MutateStateScope.addPermission( permission: Permission, - isSynchronousWrite: Boolean = false + isSynchronousWrite: Boolean = false, ) { val writeMode = if (isSynchronousWrite) WriteMode.SYNCHRONOUS else WriteMode.ASYNCHRONOUS newState.mutateSystemState(writeMode).mutatePermissions()[permission.name] = permission @@ -1707,14 +1842,14 @@ class AppIdPermissionPolicy : SchemePolicy() { private fun MutateStateScope.getOldStatePermissionFlags( appId: Int, userId: Int, - permissionName: String + permissionName: String, ): Int = getPermissionFlags(oldState, appId, userId, permissionName) private fun getPermissionFlags( state: AccessState, appId: Int, userId: Int, - permissionName: String + permissionName: String, ): Int = state.userStates[userId]?.appIdPermissionFlags?.get(appId).getWithDefault(permissionName, 0) @@ -1725,7 +1860,7 @@ class AppIdPermissionPolicy : SchemePolicy() { appId: Int, userId: Int, permissionName: String, - flags: Int + flags: Int, ): Boolean = updatePermissionFlags(appId, userId, permissionName, PermissionFlags.MASK_ALL, flags) @@ -1734,7 +1869,7 @@ class AppIdPermissionPolicy : SchemePolicy() { userId: Int, permissionName: String, flagMask: Int, - flagValues: Int + flagValues: Int, ): Boolean { if (userId !in newState.userStates) { // Despite that we check UserManagerInternal.exists() in PermissionService, we may still @@ -1753,13 +1888,6 @@ class AppIdPermissionPolicy : SchemePolicy() { } val appIdPermissionFlags = newState.mutateUserState(userId)!!.mutateAppIdPermissionFlags() val permissionFlags = appIdPermissionFlags.mutateOrPut(appId) { MutableIndexedMap() } - // for debugging possible races TODO(b/401768134) - oldState.userStates[userId]?.appIdPermissionFlags[appId]?.map?.let { - if (permissionFlags.map === it) { - throw IllegalStateException("Unexpected sharing between old/new state") - } - } - permissionFlags.putWithDefault(permissionName, newFlags, 0) if (permissionFlags.isEmpty()) { appIdPermissionFlags -= appId @@ -1793,7 +1921,7 @@ class AppIdPermissionPolicy : SchemePolicy() { override fun MutateStateScope.upgradePackageState( packageState: PackageState, userId: Int, - version: Int + version: Int, ) { with(upgrade) { upgradePackageState(packageState, userId, version) } } @@ -1819,7 +1947,7 @@ class AppIdPermissionPolicy : SchemePolicy() { Manifest.permission.BLUETOOTH_ADVERTISE, Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN, - Manifest.permission.NEARBY_WIFI_DEVICES + Manifest.permission.NEARBY_WIFI_DEVICES, ) private val NOTIFICATIONS_PERMISSIONS = indexedSetOf(Manifest.permission.POST_NOTIFICATIONS) @@ -1832,7 +1960,7 @@ class AppIdPermissionPolicy : SchemePolicy() { Manifest.permission.READ_MEDIA_VIDEO, Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.ACCESS_MEDIA_LOCATION, - Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED + Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED, ) /** Mask for all permission flags that can be set by the user */ @@ -1866,7 +1994,7 @@ class AppIdPermissionPolicy : SchemePolicy() { userId: Int, permissionName: String, oldFlags: Int, - newFlags: Int + newFlags: Int, ) /** diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt index a4546aebef21..e3e965de4559 100644 --- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt +++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt @@ -17,8 +17,10 @@ package com.android.server.permission.access.permission import android.Manifest +import android.health.connect.HealthPermissions import android.os.Build import android.util.Slog +import com.android.server.permission.access.GetStateScope import com.android.server.permission.access.MutateStateScope import com.android.server.permission.access.immutable.* // ktlint-disable no-wildcard-imports import com.android.server.permission.access.util.andInv @@ -36,14 +38,14 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { fun MutateStateScope.upgradePackageState( packageState: PackageState, userId: Int, - version: Int + version: Int, ) { val packageName = packageState.packageName if (version <= 3) { Slog.v( LOG_TAG, "Allowlisting and upgrading background location permission for " + - "package: $packageName, version: $version, user:$userId" + "package: $packageName, version: $version, user:$userId", ) allowlistRestrictedPermissions(packageState, userId) upgradeBackgroundLocationPermission(packageState, userId) @@ -52,7 +54,7 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { Slog.v( LOG_TAG, "Upgrading access media location permission for package: $packageName" + - ", version: $version, user: $userId" + ", version: $version, user: $userId", ) upgradeAccessMediaLocationPermission(packageState, userId) } @@ -61,27 +63,37 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { Slog.v( LOG_TAG, "Upgrading scoped media and body sensor permissions for package: $packageName" + - ", version: $version, user: $userId" + ", version: $version, user: $userId", ) upgradeAuralVisualMediaPermissions(packageState, userId) - upgradeBodySensorPermissions(packageState, userId) + upgradeBodySensorBackgroundPermissions(packageState, userId) } // TODO Enable isAtLeastU check, when moving subsystem to mainline. if (version <= 14 /*&& SdkLevel.isAtLeastU()*/) { Slog.v( LOG_TAG, "Upgrading visual media permission for package: $packageName" + - ", version: $version, user: $userId" + ", version: $version, user: $userId", ) upgradeUserSelectedVisualMediaPermission(packageState, userId) } + // TODO Enable isAtLeastB check, when moving subsystem to mainline. + if (version <= 16 /*&& SdkLevel.isAtLeastB()*/) { + Slog.v( + LOG_TAG, + "Upgrading body sensor / read heart rate permissions for package: $packageName" + + ", version: $version, user: $userId", + ) + upgradeBodySensorReadHeartRatePermissions(packageState, userId) + } + // Add a new upgrade step: if (packageVersion <= LATEST_VERSION) { .... } // Also increase LATEST_VERSION } private fun MutateStateScope.allowlistRestrictedPermissions( packageState: PackageState, - userId: Int + userId: Int, ) { packageState.androidPackage!!.requestedPermissions.forEach { permissionName -> if (permissionName in LEGACY_RESTRICTED_PERMISSIONS) { @@ -91,7 +103,7 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { userId, permissionName, PermissionFlags.UPGRADE_EXEMPT, - PermissionFlags.UPGRADE_EXEMPT + PermissionFlags.UPGRADE_EXEMPT, ) } } @@ -100,7 +112,7 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { private fun MutateStateScope.upgradeBackgroundLocationPermission( packageState: PackageState, - userId: Int + userId: Int, ) { if ( Manifest.permission.ACCESS_BACKGROUND_LOCATION in @@ -122,7 +134,7 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { grantRuntimePermission( packageState, userId, - Manifest.permission.ACCESS_BACKGROUND_LOCATION + Manifest.permission.ACCESS_BACKGROUND_LOCATION, ) } } @@ -130,7 +142,7 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { private fun MutateStateScope.upgradeAccessMediaLocationPermission( packageState: PackageState, - userId: Int + userId: Int, ) { if ( Manifest.permission.ACCESS_MEDIA_LOCATION in @@ -141,14 +153,14 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { getPermissionFlags( packageState.appId, userId, - Manifest.permission.READ_EXTERNAL_STORAGE + Manifest.permission.READ_EXTERNAL_STORAGE, ) } if (PermissionFlags.isAppOpGranted(flags)) { grantRuntimePermission( packageState, userId, - Manifest.permission.ACCESS_MEDIA_LOCATION + Manifest.permission.ACCESS_MEDIA_LOCATION, ) } } @@ -157,7 +169,7 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { /** Upgrade permissions based on storage permissions grant */ private fun MutateStateScope.upgradeAuralVisualMediaPermissions( packageState: PackageState, - userId: Int + userId: Int, ) { val androidPackage = packageState.androidPackage!! if (androidPackage.targetSdkVersion < Build.VERSION_CODES.TIRAMISU) { @@ -182,9 +194,9 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { } } - private fun MutateStateScope.upgradeBodySensorPermissions( + private fun MutateStateScope.upgradeBodySensorBackgroundPermissions( packageState: PackageState, - userId: Int + userId: Int, ) { if ( Manifest.permission.BODY_SENSORS_BACKGROUND !in @@ -221,7 +233,7 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { grantRuntimePermission( packageState, userId, - Manifest.permission.BODY_SENSORS_BACKGROUND + Manifest.permission.BODY_SENSORS_BACKGROUND, ) } } @@ -229,7 +241,7 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { /** Upgrade permission based on the grant in [Manifest.permission_group.READ_MEDIA_VISUAL] */ private fun MutateStateScope.upgradeUserSelectedVisualMediaPermission( packageState: PackageState, - userId: Int + userId: Int, ) { val androidPackage = packageState.androidPackage!! if (androidPackage.targetSdkVersion < Build.VERSION_CODES.TIRAMISU) { @@ -250,21 +262,131 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { grantRuntimePermission( packageState, userId, - Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED + Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED, + ) + } + } + } + + /** + * Upgrade permissions based on the body sensors and health permissions status. + * + * Starting in BAKLAVA, the BODY_SENSORS and BODY_SENSORS_BACKGROUND permissions are being + * replaced by the READ_HEART_RATE and READ_HEALTH_DATA_IN_BACKGROUND permissions respectively. + * To ensure that older apps can continue using BODY_SENSORS without breaking we need to keep + * their permission state in sync with the new health permissions. + * + * The approach we take is to be as conservative as possible. This means if either permission is + * not granted, then we want to ensure that both end up not granted to force the user to + * re-grant with the expanded scope. + */ + private fun MutateStateScope.upgradeBodySensorReadHeartRatePermissions( + packageState: PackageState, + userId: Int, + ) { + val androidPackage = packageState.androidPackage!! + if (androidPackage.targetSdkVersion >= Build.VERSION_CODES.BAKLAVA) { + return + } + + // First sync BODY_SENSORS and READ_HEART_RATE, if required. + val isBodySensorsRequested = + Manifest.permission.BODY_SENSORS in androidPackage.requestedPermissions + val isReadHeartRateRequested = + HealthPermissions.READ_HEART_RATE in androidPackage.requestedPermissions + var isBodySensorsGranted = + isRuntimePermissionGranted(packageState, userId, Manifest.permission.BODY_SENSORS) + if (isBodySensorsRequested && isReadHeartRateRequested) { + val isReadHeartRateGranted = + isRuntimePermissionGranted(packageState, userId, HealthPermissions.READ_HEART_RATE) + if (isBodySensorsGranted != isReadHeartRateGranted) { + if (isBodySensorsGranted) { + if ( + revokeRuntimePermission( + packageState, + userId, + Manifest.permission.BODY_SENSORS, + ) + ) { + isBodySensorsGranted = false + } + } + if (isReadHeartRateGranted) { + revokeRuntimePermission(packageState, userId, HealthPermissions.READ_HEART_RATE) + } + } + } + + // Then check to ensure we haven't put the background/foreground permissions out of sync. + var isBodySensorsBackgroundGranted = + isRuntimePermissionGranted( + packageState, + userId, + Manifest.permission.BODY_SENSORS_BACKGROUND, + ) + // Background permission should not be granted without the foreground permission. + if (!isBodySensorsGranted && isBodySensorsBackgroundGranted) { + if ( + revokeRuntimePermission( + packageState, + userId, + Manifest.permission.BODY_SENSORS_BACKGROUND, + ) + ) { + isBodySensorsBackgroundGranted = false + } + } + + // Finally sync BODY_SENSORS_BACKGROUND and READ_HEALTH_DATA_IN_BACKGROUND, if required. + val isBodySensorsBackgroundRequested = + Manifest.permission.BODY_SENSORS_BACKGROUND in androidPackage.requestedPermissions + val isReadHealthDataInBackgroundRequested = + HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND in androidPackage.requestedPermissions + if (isBodySensorsBackgroundRequested && isReadHealthDataInBackgroundRequested) { + val isReadHealthDataInBackgroundGranted = + isRuntimePermissionGranted( + packageState, + userId, + HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND, ) + if (isBodySensorsBackgroundGranted != isReadHealthDataInBackgroundGranted) { + if (isBodySensorsBackgroundGranted) { + revokeRuntimePermission( + packageState, + userId, + Manifest.permission.BODY_SENSORS_BACKGROUND, + ) + } + if (isReadHealthDataInBackgroundGranted) { + revokeRuntimePermission( + packageState, + userId, + HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND, + ) + } } } } + private fun GetStateScope.isRuntimePermissionGranted( + packageState: PackageState, + userId: Int, + permissionName: String, + ): Boolean { + val permissionFlags = + with(policy) { getPermissionFlags(packageState.appId, userId, permissionName) } + return PermissionFlags.isAppOpGranted(permissionFlags) + } + private fun MutateStateScope.grantRuntimePermission( packageState: PackageState, userId: Int, - permissionName: String + permissionName: String, ) { Slog.v( LOG_TAG, "Granting runtime permission for package: ${packageState.packageName}, " + - "permission: $permissionName, userId: $userId" + "permission: $permissionName, userId: $userId", ) val permission = newState.systemState.permissions[permissionName]!! if (packageState.getUserStateOrDefault(userId).isInstantApp && !permission.isInstant) { @@ -276,7 +398,7 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { if (flags.hasAnyBit(MASK_ANY_FIXED)) { Slog.v( LOG_TAG, - "Not allowed to grant $permissionName to package ${packageState.packageName}" + "Not allowed to grant $permissionName to package ${packageState.packageName}", ) return } @@ -292,6 +414,43 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { with(policy) { setPermissionFlags(appId, userId, permissionName, flags) } } + /** + * Revoke a runtime permission for a given user from a given package. + * + * @return true if the permission was revoked, false otherwise. + */ + private fun MutateStateScope.revokeRuntimePermission( + packageState: PackageState, + userId: Int, + permissionName: String, + ): Boolean { + Slog.v( + LOG_TAG, + "Revoking runtime permission for package: ${packageState.packageName}, " + + "permission: $permissionName, userId: $userId", + ) + + val appId = packageState.appId + var flags = with(policy) { getPermissionFlags(appId, userId, permissionName) } + if (flags.hasAnyBit(MASK_SYSTEM_OR_POLICY_FIXED)) { + Slog.v( + LOG_TAG, + "Cannot revoke fixed runtime permission from package: " + + "${packageState.packageName}, permission: $permissionName, userId: $userId", + ) + return false + } + + flags = + flags andInv + (PermissionFlags.RUNTIME_GRANTED or + MASK_USER_SETTABLE or + PermissionFlags.PREGRANT or + PermissionFlags.ROLE) + with(policy) { setPermissionFlags(appId, userId, permissionName, flags) } + return true + } + companion object { private val LOG_TAG = AppIdPermissionUpgrade::class.java.simpleName @@ -302,6 +461,17 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { PermissionFlags.POLICY_FIXED or PermissionFlags.SYSTEM_FIXED + private const val MASK_SYSTEM_OR_POLICY_FIXED = + PermissionFlags.SYSTEM_FIXED or PermissionFlags.POLICY_FIXED + + private const val MASK_USER_SETTABLE = + PermissionFlags.USER_SET or + PermissionFlags.USER_FIXED or + PermissionFlags.APP_OP_REVOKED or + PermissionFlags.ONE_TIME or + PermissionFlags.HIBERNATION or + PermissionFlags.USER_SELECTED + private val LEGACY_RESTRICTED_PERMISSIONS = indexedSetOf( Manifest.permission.ACCESS_BACKGROUND_LOCATION, @@ -314,13 +484,13 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { Manifest.permission.READ_CELL_BROADCASTS, Manifest.permission.READ_CALL_LOG, Manifest.permission.WRITE_CALL_LOG, - Manifest.permission.PROCESS_OUTGOING_CALLS + Manifest.permission.PROCESS_OUTGOING_CALLS, ) private val STORAGE_PERMISSIONS = indexedSetOf( Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE + Manifest.permission.WRITE_EXTERNAL_STORAGE, ) private val AURAL_VISUAL_MEDIA_PERMISSIONS = indexedSetOf( @@ -328,14 +498,14 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO, Manifest.permission.ACCESS_MEDIA_LOCATION, - Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED + Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED, ) // Visual media permissions in T private val VISUAL_MEDIA_PERMISSIONS = indexedSetOf( Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO, - Manifest.permission.ACCESS_MEDIA_LOCATION + Manifest.permission.ACCESS_MEDIA_LOCATION, ) } } diff --git a/services/proguard.flags b/services/proguard.flags index dd3757c9e360..00e0a6ec5711 100644 --- a/services/proguard.flags +++ b/services/proguard.flags @@ -60,6 +60,7 @@ # Referenced in wear-service -keep public class com.android.server.wm.WindowManagerInternal { *; } -keep public class com.android.server.wm.WindowManagerInternal$WindowFocusChangeListener { *; } +-keep public class com.android.server.wm.ActivityAssistInfo { *; } # JNI keep rules # The global keep rule for native methods allows stripping of such methods if they're unreferenced diff --git a/services/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java index 0b5a95b0e888..c419fd2ecbd7 100644 --- a/services/supervision/java/com/android/server/supervision/SupervisionService.java +++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java @@ -17,6 +17,7 @@ package com.android.server.supervision; import static android.Manifest.permission.INTERACT_ACROSS_USERS; +import static android.Manifest.permission.MANAGE_ROLE_HOLDERS; import static android.Manifest.permission.MANAGE_USERS; import static android.Manifest.permission.QUERY_USERS; import static android.content.pm.PackageManager.PERMISSION_GRANTED; @@ -171,6 +172,44 @@ public class SupervisionService extends ISupervisionManager.Stub { } @Override + public boolean shouldAllowBypassingSupervisionRoleQualification() { + enforcePermission(MANAGE_ROLE_HOLDERS); + + if (hasNonTestDefaultUsers()) { + return false; + } + + synchronized (getLockObject()) { + for (int i = 0; i < mUserData.size(); i++) { + if (mUserData.valueAt(i).supervisionEnabled) { + return false; + } + } + } + + return true; + } + + /** + * Returns true if there are any non-default non-test users. + * + * This excludes the system and main user(s) as those users are created by default. + */ + private boolean hasNonTestDefaultUsers() { + List<UserInfo> users = mInjector.getUserManagerInternal().getUsers(true); + for (var user : users) { + if (!user.isForTesting() && !user.isMain() && !isSystemUser(user)) { + return true; + } + } + return false; + } + + private static boolean isSystemUser(UserInfo userInfo) { + return (userInfo.flags & UserInfo.FLAG_SYSTEM) == UserInfo.FLAG_SYSTEM; + } + + @Override public void onShellCommand( @Nullable FileDescriptor in, @Nullable FileDescriptor out, diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/TamperedUpdatedSystemPackageTest.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/TamperedUpdatedSystemPackageTest.kt index 304f605d5b95..7c4515962669 100644 --- a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/TamperedUpdatedSystemPackageTest.kt +++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/TamperedUpdatedSystemPackageTest.kt @@ -16,8 +16,6 @@ package com.android.server.pm.test -import android.platform.test.annotations.RequiresFlagsEnabled -import android.platform.test.flag.junit.host.HostFlagsValueProvider import com.android.internal.util.test.SystemPreparer import com.android.tradefed.testtype.DeviceJUnit4ClassRunner import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test @@ -35,7 +33,6 @@ import org.junit.rules.TemporaryFolder import org.junit.runner.RunWith @RunWith(DeviceJUnit4ClassRunner::class) -@RequiresFlagsEnabled(android.security.Flags.FLAG_EXTEND_VB_CHAIN_TO_UPDATED_APK) class TamperedUpdatedSystemPackageTest : BaseHostJUnit4Test() { companion object { @@ -68,10 +65,6 @@ class TamperedUpdatedSystemPackageTest : BaseHostJUnit4Test() { @Rule @JvmField - val checkFlagsRule = HostFlagsValueProvider.createCheckFlagsRule({ getDevice() }) - - @Rule - @JvmField val rules = RuleChain.outerRule(tempFolder).around(preparer)!! @Before diff --git a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionStatesTest.kt b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionStatesTest.kt index 6b9c9c2b4abc..c0f0369d4774 100644 --- a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionStatesTest.kt +++ b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionStatesTest.kt @@ -29,6 +29,7 @@ import com.android.server.pm.pkg.PackageState import com.android.server.testutils.mock import com.android.server.testutils.whenever import com.google.common.truth.Truth.assertWithMessage +import org.junit.Assume.assumeTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -57,10 +58,10 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) val expectedNewFlags = oldFlags assertWithMessage( - "After $action is called for a package that requests a normal permission" + - " with an existing INSTALL_GRANTED flag, the actual permission flags $actualFlags" + - " should match the expected flags $expectedNewFlags" - ) + "After $action is called for a package that requests a normal permission" + + " with an existing INSTALL_GRANTED flag, the actual permission flags $actualFlags" + + " should match the expected flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @@ -71,16 +72,16 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { testEvaluatePermissionState( oldFlags, PermissionInfo.PROTECTION_NORMAL, - isNewInstall = true + isNewInstall = true, ) {} val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) val expectedNewFlags = PermissionFlags.INSTALL_GRANTED assertWithMessage( - "After $action is called for a package that requests a normal permission" + - " with no existing flags, the actual permission flags $actualFlags" + - " should match the expected flags $expectedNewFlags" - ) + "After $action is called for a package that requests a normal permission" + + " with no existing flags, the actual permission flags $actualFlags" + + " should match the expected flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @@ -90,16 +91,16 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { val oldFlags = PermissionFlags.ROLE or PermissionFlags.USER_SET testEvaluatePermissionState( oldFlags, - PermissionInfo.PROTECTION_NORMAL or PermissionInfo.PROTECTION_FLAG_APPOP + PermissionInfo.PROTECTION_NORMAL or PermissionInfo.PROTECTION_FLAG_APPOP, ) {} val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) val expectedNewFlags = PermissionFlags.INSTALL_GRANTED or oldFlags assertWithMessage( - "After $action is called for a package that requests a normal app op" + - " permission with existing ROLE and USER_SET flags, the actual permission flags" + - " $actualFlags should match the expected flags $expectedNewFlags" - ) + "After $action is called for a package that requests a normal app op" + + " permission with existing ROLE and USER_SET flags, the actual permission flags" + + " $actualFlags should match the expected flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @@ -115,21 +116,21 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) val expectedNewFlags = oldFlags assertWithMessage( - "After $action is called for a package that requests an internal permission" + - " with missing android package and $oldFlags flag, the actual permission flags" + - " $actualFlags should match the expected flags $expectedNewFlags" - ) + "After $action is called for a package that requests an internal permission" + + " with missing android package and $oldFlags flag, the actual permission flags" + + " $actualFlags should match the expected flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @Test fun testEvaluatePermissionState_internalAppOpPermission_getsRoleAndUserSetFlagsPreserved() { - val oldFlags = PermissionFlags.PROTECTION_GRANTED or PermissionFlags.ROLE or - PermissionFlags.USER_SET + val oldFlags = + PermissionFlags.PROTECTION_GRANTED or PermissionFlags.ROLE or PermissionFlags.USER_SET testEvaluatePermissionState( oldFlags, - PermissionInfo.PROTECTION_INTERNAL or PermissionInfo.PROTECTION_FLAG_APPOP + PermissionInfo.PROTECTION_INTERNAL or PermissionInfo.PROTECTION_FLAG_APPOP, ) { val packageStateWithMissingPackage = mockPackageState(APP_ID_1, MISSING_ANDROID_PACKAGE) addPackageState(packageStateWithMissingPackage) @@ -138,11 +139,11 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) val expectedNewFlags = oldFlags assertWithMessage( - "After $action is called for a package that requests an internal permission" + - " with missing android package and $oldFlags flag and the permission isAppOp," + - " the actual permission flags $actualFlags should match the expected" + - " flags $expectedNewFlags" - ) + "After $action is called for a package that requests an internal permission" + + " with missing android package and $oldFlags flag and the permission isAppOp," + + " the actual permission flags $actualFlags should match the expected" + + " flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @@ -152,7 +153,7 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { val oldFlags = PermissionFlags.PROTECTION_GRANTED or PermissionFlags.RUNTIME_GRANTED testEvaluatePermissionState( oldFlags, - PermissionInfo.PROTECTION_INTERNAL or PermissionInfo.PROTECTION_FLAG_DEVELOPMENT + PermissionInfo.PROTECTION_INTERNAL or PermissionInfo.PROTECTION_FLAG_DEVELOPMENT, ) { val packageStateWithMissingPackage = mockPackageState(APP_ID_1, MISSING_ANDROID_PACKAGE) addPackageState(packageStateWithMissingPackage) @@ -161,22 +162,24 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) val expectedNewFlags = oldFlags assertWithMessage( - "After $action is called for a package that requests an internal permission" + - " with missing android package and $oldFlags flag and permission isDevelopment," + - " the actual permission flags $actualFlags should match the expected" + - " flags $expectedNewFlags" - ) + "After $action is called for a package that requests an internal permission" + + " with missing android package and $oldFlags flag and permission isDevelopment," + + " the actual permission flags $actualFlags should match the expected" + + " flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @Test fun testEvaluatePermissionState_internalRolePermission_getsRoleAndRuntimeGrantedPreserved() { - val oldFlags = PermissionFlags.PROTECTION_GRANTED or PermissionFlags.ROLE or - PermissionFlags.RUNTIME_GRANTED + val oldFlags = + PermissionFlags.PROTECTION_GRANTED or + PermissionFlags.ROLE or + PermissionFlags.RUNTIME_GRANTED testEvaluatePermissionState( oldFlags, - PermissionInfo.PROTECTION_INTERNAL or PermissionInfo.PROTECTION_FLAG_ROLE + PermissionInfo.PROTECTION_INTERNAL or PermissionInfo.PROTECTION_FLAG_ROLE, ) { val packageStateWithMissingPackage = mockPackageState(APP_ID_1, MISSING_ANDROID_PACKAGE) addPackageState(packageStateWithMissingPackage) @@ -185,11 +188,11 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) val expectedNewFlags = oldFlags assertWithMessage( - "After $action is called for a package that requests an internal permission" + - " with missing android package and $oldFlags flag and the permission isRole," + - " the actual permission flags $actualFlags should match the expected" + - " flags $expectedNewFlags" - ) + "After $action is called for a package that requests an internal permission" + + " with missing android package and $oldFlags flag and the permission isRole," + + " the actual permission flags $actualFlags should match the expected" + + " flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @@ -205,12 +208,10 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { isInstalledPackageProduct = true, // To mock the return value of shouldGrantPrivilegedOrOemPermission() isInstalledPackageVendor = true, - isNewInstall = true + isNewInstall = true, ) { - val platformPackage = mockPackageState( - PLATFORM_APP_ID, - mockAndroidPackage(PLATFORM_PACKAGE_NAME) - ) + val platformPackage = + mockPackageState(PLATFORM_APP_ID, mockAndroidPackage(PLATFORM_PACKAGE_NAME)) setupAllowlist(PACKAGE_NAME_1, false) addPackageState(platformPackage) } @@ -218,10 +219,10 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) val expectedNewFlags = oldFlags assertWithMessage( - "After $action is called for a package that requests a signature privileged" + - " permission that's not allowlisted, the actual permission" + - " flags $actualFlags should match the expected flags $expectedNewFlags" - ) + "After $action is called for a package that requests a signature privileged" + + " permission that's not allowlisted, the actual permission" + + " flags $actualFlags should match the expected flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @@ -237,12 +238,13 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { isInstalledPackageProduct = true, isInstalledPackageSignatureMatching = true, isInstalledPackageVendor = true, - isNewInstall = true + isNewInstall = true, ) { - val platformPackage = mockPackageState( - PLATFORM_APP_ID, - mockAndroidPackage(PLATFORM_PACKAGE_NAME, isSignatureMatching = true) - ) + val platformPackage = + mockPackageState( + PLATFORM_APP_ID, + mockAndroidPackage(PLATFORM_PACKAGE_NAME, isSignatureMatching = true), + ) setupAllowlist(PACKAGE_NAME_1, false) addPackageState(platformPackage) } @@ -250,10 +252,10 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) val expectedNewFlags = PermissionFlags.PROTECTION_GRANTED assertWithMessage( - "After $action is called for a package that requests a signature" + - " non-privileged permission, the actual permission" + - " flags $actualFlags should match the expected flags $expectedNewFlags" - ) + "After $action is called for a package that requests a signature" + + " non-privileged permission, the actual permission" + + " flags $actualFlags should match the expected flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @@ -267,12 +269,10 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { isInstalledPackageSystem = true, isInstalledPackagePrivileged = true, isInstalledPackageProduct = true, - isNewInstall = true + isNewInstall = true, ) { - val platformPackage = mockPackageState( - PLATFORM_APP_ID, - mockAndroidPackage(PLATFORM_PACKAGE_NAME) - ) + val platformPackage = + mockPackageState(PLATFORM_APP_ID, mockAndroidPackage(PLATFORM_PACKAGE_NAME)) setupAllowlist(PACKAGE_NAME_1, true) addPackageState(platformPackage) } @@ -280,10 +280,10 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) val expectedNewFlags = PermissionFlags.PROTECTION_GRANTED assertWithMessage( - "After $action is called for a package that requests a signature privileged" + - " permission that's allowlisted and should grant by protection flags, the actual" + - " permission flags $actualFlags should match the expected flags $expectedNewFlags" - ) + "After $action is called for a package that requests a signature privileged" + + " permission that's allowlisted and should grant by protection flags, the actual" + + " permission flags $actualFlags should match the expected flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @@ -291,32 +291,36 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { private fun setupAllowlist( packageName: String, allowlistState: Boolean, - state: MutableAccessState = oldState + state: MutableAccessState = oldState, ) { - state.mutateExternalState().setPrivilegedPermissionAllowlistPackages( - MutableIndexedListSet<String>().apply { add(packageName) } - ) - val mockAllowlist = mock<PermissionAllowlist> { - whenever( - getProductPrivilegedAppAllowlistState(packageName, PERMISSION_NAME_0) - ).thenReturn(allowlistState) - } + state + .mutateExternalState() + .setPrivilegedPermissionAllowlistPackages( + MutableIndexedListSet<String>().apply { add(packageName) } + ) + val mockAllowlist = + mock<PermissionAllowlist> { + whenever(getProductPrivilegedAppAllowlistState(packageName, PERMISSION_NAME_0)) + .thenReturn(allowlistState) + } state.mutateExternalState().setPermissionAllowlist(mockAllowlist) } @Test fun testEvaluatePermissionState_nonRuntimeFlagsOnRuntimePermissions_getsCleared() { - val oldFlags = PermissionFlags.INSTALL_GRANTED or PermissionFlags.PREGRANT or - PermissionFlags.RUNTIME_GRANTED + val oldFlags = + PermissionFlags.INSTALL_GRANTED or + PermissionFlags.PREGRANT or + PermissionFlags.RUNTIME_GRANTED testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {} val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) val expectedNewFlags = PermissionFlags.PREGRANT or PermissionFlags.RUNTIME_GRANTED assertWithMessage( - "After $action is called for a package that requests a runtime permission" + - " with existing $oldFlags flags, the actual permission flags $actualFlags should" + - " match the expected flags $expectedNewFlags" - ) + "After $action is called for a package that requests a runtime permission" + + " with existing $oldFlags flags, the actual permission flags $actualFlags should" + + " match the expected flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @@ -328,16 +332,16 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { oldFlags, PermissionInfo.PROTECTION_DANGEROUS, installedPackageTargetSdkVersion = Build.VERSION_CODES.LOLLIPOP, - isNewInstall = true + isNewInstall = true, ) {} val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) val expectedNewFlags = PermissionFlags.LEGACY_GRANTED or PermissionFlags.IMPLICIT assertWithMessage( - "After $action is called for a package that requests a runtime permission" + - " with no existing flags in pre M, actual permission flags $actualFlags should" + - " match the expected flags $expectedNewFlags" - ) + "After $action is called for a package that requests a runtime permission" + + " with no existing flags in pre M, actual permission flags $actualFlags should" + + " match the expected flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @@ -348,20 +352,22 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { testEvaluatePermissionState( oldFlags, PermissionInfo.PROTECTION_DANGEROUS, - installedPackageTargetSdkVersion = Build.VERSION_CODES.LOLLIPOP + installedPackageTargetSdkVersion = Build.VERSION_CODES.LOLLIPOP, ) { setPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0, oldFlags) } val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) - val expectedNewFlags = PermissionFlags.LEGACY_GRANTED or PermissionFlags.USER_FIXED or - PermissionFlags.APP_OP_REVOKED + val expectedNewFlags = + PermissionFlags.LEGACY_GRANTED or + PermissionFlags.USER_FIXED or + PermissionFlags.APP_OP_REVOKED assertWithMessage( - "After $action is called for a package that requests a runtime permission" + - " that should be LEGACY_GRANTED or IMPLICIT_GRANTED that was previously revoked," + - " the actual permission flags $actualFlags should" + - " match the expected flags $expectedNewFlags" - ) + "After $action is called for a package that requests a runtime permission" + + " that should be LEGACY_GRANTED or IMPLICIT_GRANTED that was previously revoked," + + " the actual permission flags $actualFlags should" + + " match the expected flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @@ -374,11 +380,11 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) val expectedNewFlags = 0 assertWithMessage( - "After $action is called for a package that requests a runtime permission" + - " that used to require user review, the user review requirement should be removed" + - " if it's upgraded to post M. The actual permission flags $actualFlags should" + - " match the expected flags $expectedNewFlags" - ) + "After $action is called for a package that requests a runtime permission" + + " that used to require user review, the user review requirement should be removed" + + " if it's upgraded to post M. The actual permission flags $actualFlags should" + + " match the expected flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @@ -391,11 +397,11 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) val expectedNewFlags = PermissionFlags.RUNTIME_GRANTED assertWithMessage( - "After $action is called for a package that requests a runtime permission" + - " that was already reviewed by the user, the permission should be RUNTIME_GRANTED" + - " if it's upgraded to post M. The actual permission flags $actualFlags should" + - " match the expected flags $expectedNewFlags" - ) + "After $action is called for a package that requests a runtime permission" + + " that was already reviewed by the user, the permission should be RUNTIME_GRANTED" + + " if it's upgraded to post M. The actual permission flags $actualFlags should" + + " match the expected flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @@ -407,22 +413,19 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { oldFlags, PermissionInfo.PROTECTION_DANGEROUS, permissionName = PERMISSION_POST_NOTIFICATIONS, - isNewInstall = true + isNewInstall = true, ) { oldState.mutateExternalState().setLeanback(true) } - val actualFlags = getPermissionFlags( - APP_ID_1, - getUserIdEvaluated(), - PERMISSION_POST_NOTIFICATIONS - ) + val actualFlags = + getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_POST_NOTIFICATIONS) val expectedNewFlags = PermissionFlags.IMPLICIT_GRANTED assertWithMessage( - "After $action is called for a package that requests a runtime notification" + - " permission when isLeanback, the actual permission flags $actualFlags should" + - " match the expected flags $expectedNewFlags" - ) + "After $action is called for a package that requests a runtime notification" + + " permission when isLeanback, the actual permission flags $actualFlags should" + + " match the expected flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @@ -434,65 +437,73 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { oldFlags, PermissionInfo.PROTECTION_DANGEROUS, implicitPermissions = setOf(PERMISSION_NAME_0), - isNewInstall = true + isNewInstall = true, ) { - oldState.mutateExternalState().setImplicitToSourcePermissions( - MutableIndexedMap<String, IndexedListSet<String>>().apply { - put(PERMISSION_NAME_0, MutableIndexedListSet<String>().apply { - add(PERMISSION_NAME_1) - }) - } - ) + oldState + .mutateExternalState() + .setImplicitToSourcePermissions( + MutableIndexedMap<String, IndexedListSet<String>>().apply { + put( + PERMISSION_NAME_0, + MutableIndexedListSet<String>().apply { add(PERMISSION_NAME_1) }, + ) + } + ) addPermission(mockParsedPermission(PERMISSION_NAME_1, PACKAGE_NAME_0)) } val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) val expectedNewFlags = PermissionFlags.IMPLICIT_GRANTED or PermissionFlags.IMPLICIT assertWithMessage( - "After $action is called for a package that requests a runtime implicit" + - " permission that's source from a non-runtime permission, the actual permission" + - " flags $actualFlags should match the expected flags $expectedNewFlags" - ) + "After $action is called for a package that requests a runtime implicit" + + " permission that's source from a non-runtime permission, the actual permission" + + " flags $actualFlags should match the expected flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } /** * For a legacy granted or implicit permission during the app upgrade, when the permission - * should no longer be legacy or implicit granted, we want to remove the APP_OP_REVOKED flag - * so that the app can request the permission. + * should no longer be legacy or implicit granted, we want to remove the APP_OP_REVOKED flag so + * that the app can request the permission. */ @Test fun testEvaluatePermissionState_noLongerLegacyOrImplicitGranted_canBeRequested() { - val oldFlags = PermissionFlags.LEGACY_GRANTED or PermissionFlags.APP_OP_REVOKED or - PermissionFlags.RUNTIME_GRANTED + val oldFlags = + PermissionFlags.LEGACY_GRANTED or + PermissionFlags.APP_OP_REVOKED or + PermissionFlags.RUNTIME_GRANTED testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {} val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) val expectedNewFlags = 0 assertWithMessage( - "After $action is called for a package that requests a runtime permission" + - " that is no longer LEGACY_GRANTED or IMPLICIT_GRANTED, the actual permission" + - " flags $actualFlags should match the expected flags $expectedNewFlags" - ) + "After $action is called for a package that requests a runtime permission" + + " that is no longer LEGACY_GRANTED or IMPLICIT_GRANTED, the actual permission" + + " flags $actualFlags should match the expected flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @Test fun testEvaluatePermissionState_noLongerImplicit_getsRuntimeAndImplicitFlagsRemoved() { - val oldFlags = PermissionFlags.IMPLICIT or PermissionFlags.RUNTIME_GRANTED or - PermissionFlags.USER_SET or PermissionFlags.USER_FIXED + val oldFlags = + PermissionFlags.IMPLICIT or + PermissionFlags.RUNTIME_GRANTED or + PermissionFlags.USER_SET or + PermissionFlags.USER_FIXED testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {} val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) val expectedNewFlags = 0 assertWithMessage( - "After $action is called for a package that requests a runtime permission" + - " that is no longer implicit and we shouldn't retain as nearby device" + - " permissions, the actual permission flags $actualFlags should match the expected" + - " flags $expectedNewFlags" - ) + "After $action is called for a package that requests a runtime permission" + + " that is no longer implicit and we shouldn't retain as nearby device" + + " permissions, the actual permission flags $actualFlags should match the expected" + + " flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @@ -504,48 +515,481 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { oldFlags, PermissionInfo.PROTECTION_DANGEROUS, permissionName = PERMISSION_BLUETOOTH_CONNECT, - requestedPermissions = setOf( - PERMISSION_BLUETOOTH_CONNECT, - PERMISSION_ACCESS_BACKGROUND_LOCATION - ) + requestedPermissions = + setOf(PERMISSION_BLUETOOTH_CONNECT, PERMISSION_ACCESS_BACKGROUND_LOCATION), ) { setPermissionFlags( APP_ID_1, getUserIdEvaluated(), PERMISSION_ACCESS_BACKGROUND_LOCATION, - PermissionFlags.RUNTIME_GRANTED + PermissionFlags.RUNTIME_GRANTED, ) } - val actualFlags = getPermissionFlags( - APP_ID_1, - getUserIdEvaluated(), - PERMISSION_BLUETOOTH_CONNECT - ) + val actualFlags = + getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BLUETOOTH_CONNECT) val expectedNewFlags = PermissionFlags.RUNTIME_GRANTED assertWithMessage( - "After $action is called for a package that requests a runtime nearby device" + - " permission that was granted by implicit, the actual permission flags" + - " $actualFlags should match the expected flags $expectedNewFlags" - ) + "After $action is called for a package that requests a runtime nearby device" + + " permission that was granted by implicit, the actual permission flags" + + " $actualFlags should match the expected flags $expectedNewFlags" + ) + .that(actualFlags) + .isEqualTo(expectedNewFlags) + } + + /** Setup: BODY_SENSORS: granted, READ_HEART_RATE: not granted Result: BODY_SENSORS: revoked */ + @Test + fun testEvaluatePermissionState_bodySensorReadHrOutOfSync_revokesGrantedBodySensor() { + assumeTrue(action != Action.ON_USER_ADDED) + val oldFlags = PermissionFlags.RUNTIME_GRANTED + testEvaluatePermissionState( + oldFlags, + PermissionInfo.PROTECTION_DANGEROUS, + permissionName = PERMISSION_BODY_SENSORS, + requestedPermissions = setOf(PERMISSION_BODY_SENSORS, PERMISSION_READ_HEART_RATE), + ) {} + + val actualFlags = + getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS) + val expectedNewFlags = 0 + assertWithMessage( + "After $action is called for a package that requests a runtime body sensors" + + " permission that was granted while read hr was not, the actual permission" + + " flags $actualFlags should match the expected flags $expectedNewFlags" + ) + .that(actualFlags) + .isEqualTo(expectedNewFlags) + } + + /** + * Setup: BODY_SENSORS: not granted, READ_HEART_RATE: granted Result: READ_HEART_RATE: revoked + */ + @Test + fun testEvaluatePermissionState_bodySensorReadHrOutOfSync_revokesGrantedReadHr() { + assumeTrue(action != Action.ON_USER_ADDED) + val oldFlags = 0 + testEvaluatePermissionState( + oldFlags, + PermissionInfo.PROTECTION_DANGEROUS, + permissionName = PERMISSION_BODY_SENSORS, + requestedPermissions = setOf(PERMISSION_BODY_SENSORS, PERMISSION_READ_HEART_RATE), + ) { + setPermissionFlags( + APP_ID_1, + getUserIdEvaluated(), + PERMISSION_READ_HEART_RATE, + PermissionFlags.RUNTIME_GRANTED, + ) + } + + val actualFlags = + getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_READ_HEART_RATE) + val expectedNewFlags = 0 + assertWithMessage( + "After $action is called for a package that requests a runtime body sensors" + + " permission that was not granted while read hr was, the actual permission" + + "flags $actualFlags should match the expected flags $expectedNewFlags" + ) + .that(actualFlags) + .isEqualTo(expectedNewFlags) + } + + /** + * Setup: BODY_SENSORS: granted, READ_HEART_RATE: not granted Result: nothing revoked since the + * targetSdk is Baklava + */ + @Test + fun testEvaluatePermissionState_bodySensorReadHrOutOfSync_baklavaTargetSdk_nothingRevoked() { + assumeTrue(action != Action.ON_USER_ADDED) + val oldFlags = PermissionFlags.RUNTIME_GRANTED + testEvaluatePermissionState( + oldFlags, + PermissionInfo.PROTECTION_DANGEROUS, + permissionName = PERMISSION_BODY_SENSORS, + installedPackageTargetSdkVersion = Build.VERSION_CODES.BAKLAVA, + requestedPermissions = setOf(PERMISSION_BODY_SENSORS, PERMISSION_READ_HEART_RATE), + ) {} + + val actualFlags = + getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS) + val expectedNewFlags = oldFlags + assertWithMessage( + "After $action is called for a package that requests a runtime body sensors" + + " permission that was granted while read hr was not targeting Baklava," + + " the actual permission flags $actualFlags should match the expected" + + " flags $expectedNewFlags" + ) + .that(actualFlags) + .isEqualTo(expectedNewFlags) + } + + /** + * Setup: BODY_SENSORS_BACKGROUND: granted, BODY_SENSORS: not granted Result: + * BODY_SENSORS_BACKGROUND: revoked + */ + @Test + fun testEvaluatePermissionState_bodySensorBackgroundGrantMismatch_revokesBackground() { + assumeTrue(action != Action.ON_USER_ADDED) + val oldFlags = 0 + testEvaluatePermissionState( + oldFlags, + PermissionInfo.PROTECTION_DANGEROUS, + permissionName = PERMISSION_BODY_SENSORS, + requestedPermissions = + setOf(PERMISSION_BODY_SENSORS, PERMISSION_BODY_SENSORS_BACKGROUND), + ) { + setPermissionFlags( + APP_ID_1, + getUserIdEvaluated(), + PERMISSION_BODY_SENSORS_BACKGROUND, + PermissionFlags.RUNTIME_GRANTED, + ) + } + + val actualFlags = + getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS_BACKGROUND) + val expectedNewFlags = 0 + assertWithMessage( + "After $action is called for a package that requests a runtime body sensors" + + " permission that was not granted while body sensors background was," + + " the actual permission flags $actualFlags should match the expected" + + " flags $expectedNewFlags" + ) + .that(actualFlags) + .isEqualTo(expectedNewFlags) + } + + /** + * Setup: BODY_SENSORS_BACKGROUND: granted, BODY_SENSORS: not requested Result: + * BODY_SENSORS_BACKGROUND: revoked + */ + @Test + fun testEvaluatePermissionState_bodySensorBackgroundMissingForeground_baklavaTargetSdk_revokesBackground() { + assumeTrue(action != Action.ON_USER_ADDED) + val oldFlags = PermissionFlags.RUNTIME_GRANTED + testEvaluatePermissionState( + oldFlags, + PermissionInfo.PROTECTION_DANGEROUS, + permissionName = PERMISSION_BODY_SENSORS_BACKGROUND, + requestedPermissions = setOf(PERMISSION_BODY_SENSORS_BACKGROUND), + ) {} + + val actualFlags = + getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS_BACKGROUND) + val expectedNewFlags = 0 + assertWithMessage( + "After $action is called for a package that has runtime body sensors background" + + " permission granted but is not requesting the body sensors foreground" + + " permission, the actual permission flags $actualFlags should match the" + + " expected flags $expectedNewFlags" + ) + .that(actualFlags) + .isEqualTo(expectedNewFlags) + } + + /** + * Setup: BODY_SENSORS_BACKGROUND: granted, BODY_SENSORS: granted, READ_HEART_RATE: not granted + * Result: BODY_SENSORS_BACKGROUND: revoked + */ + @Test + fun testEvaluatePermissionState_bodySensorHeartRateOutOfSync_revokesGrantedBodySensorBackground() { + assumeTrue(action != Action.ON_USER_ADDED) + val oldFlags = PermissionFlags.RUNTIME_GRANTED + testEvaluatePermissionState( + oldFlags, + PermissionInfo.PROTECTION_DANGEROUS, + permissionName = PERMISSION_BODY_SENSORS, + installedPackageTargetSdkVersion = Build.VERSION_CODES.BAKLAVA, + requestedPermissions = + setOf(PERMISSION_BODY_SENSORS, PERMISSION_READ_HEART_RATE, PERMISSION_BODY_SENSORS), + ) { + setPermissionFlags( + APP_ID_1, + getUserIdEvaluated(), + PERMISSION_BODY_SENSORS_BACKGROUND, + PermissionFlags.RUNTIME_GRANTED, + ) + } + + val actualFlags = + getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS_BACKGROUND) + val expectedNewFlags = 0 + assertWithMessage( + "After $action is called for a package that requests a runtime body sensors" + + " permission that was granted while read hr was not targeting Baklava," + + " the actual permission flags $actualFlags should match the expected" + + " flags $expectedNewFlags" + ) + .that(actualFlags) + .isEqualTo(expectedNewFlags) + } + + /** + * Setup: BODY_SENSORS_BACKGROUND: granted, READ_HEALTH_DATA_IN_BACKGROUND: not granted Result: + * BODY_SENSORS_BACKGROUND: revoked + */ + @Test + fun testEvaluatePermissionState_bodySensorReadHealthBackgroundOutOfSync_revokesGrantedBodySensorBackground() { + assumeTrue(action != Action.ON_USER_ADDED) + val oldFlags = 0 + testEvaluatePermissionState( + oldFlags, + PermissionInfo.PROTECTION_DANGEROUS, + permissionName = PERMISSION_BODY_SENSORS_BACKGROUND, + requestedPermissions = + setOf(PERMISSION_BODY_SENSORS_BACKGROUND, PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND), + ) { + setPermissionFlags( + APP_ID_1, + getUserIdEvaluated(), + PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND, + PermissionFlags.RUNTIME_GRANTED, + ) + } + + val actualFlags = + getPermissionFlags( + APP_ID_1, + getUserIdEvaluated(), + PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND, + ) + val expectedNewFlags = 0 + assertWithMessage( + "After $action is called for a package that requests a runtime body sensors" + + " background permission that was not granted while read health data in" + + " background was, the actual permission flags $actualFlags should match" + + " the expected flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } + /** + * Setup: BODY_SENSORS_BACKGROUND: not granted, READ_HEALTH_DATA_IN_BACKGROUND: granted Result: + * READ_HEALTH_DATA_IN_BACKGROUND: revoked + */ + @Test + fun testEvaluatePermissionState_bodySensorReadHealthBackgroundOutOfSync_revokesGrantedReadHealthBackground() { + assumeTrue(action != Action.ON_USER_ADDED) + val oldFlags = PermissionFlags.RUNTIME_GRANTED + testEvaluatePermissionState( + oldFlags, + PermissionInfo.PROTECTION_DANGEROUS, + permissionName = PERMISSION_BODY_SENSORS_BACKGROUND, + requestedPermissions = + setOf(PERMISSION_BODY_SENSORS_BACKGROUND, PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND), + ) {} + + val actualFlags = + getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS_BACKGROUND) + val expectedNewFlags = 0 + assertWithMessage( + "After $action is called for a package that requests a runtime body sensors" + + " background permission that was granted while read health data in" + + " background was not, the actual permission flags $actualFlags should match" + + " the expected flags $expectedNewFlags" + ) + .that(actualFlags) + .isEqualTo(expectedNewFlags) + } + + /** + * The sequence of events here is: + * + * Starting: + * - READ_HR=not granted + * - BODY_SENSORS=granted + * - BODY_SENSORS_BACKGROUND=granted, + * - READ_HEALTH_DATA_IN_BACKGROUND=granted + * + * Actions: + * - BODY_SENSORS->revoked (due to READ_HR mismatch) + * - BODY_SENSORS_BACKGROUND->revoked (due to new BODY_SENSORS mismatch) + * - READ_HEALTH_DATA_IN_BACKGROUND->revoked (due to new BODY_SENSORS_BACKGROUND mismatch) + * + * End result: All permissions revoked. + */ + @Test + fun testEvaluatePermissionState_healthPermissionsSync_revocationChain() { + assumeTrue(action != Action.ON_USER_ADDED) + val oldFlags = 0 + testEvaluatePermissionState( + oldFlags, + PermissionInfo.PROTECTION_DANGEROUS, + permissionName = PERMISSION_READ_HEART_RATE, + requestedPermissions = + setOf( + PERMISSION_READ_HEART_RATE, + PERMISSION_BODY_SENSORS, + PERMISSION_BODY_SENSORS_BACKGROUND, + PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND, + ), + ) { + setPermissionFlags( + APP_ID_1, + getUserIdEvaluated(), + PERMISSION_BODY_SENSORS, + PermissionFlags.RUNTIME_GRANTED, + ) + setPermissionFlags( + APP_ID_1, + getUserIdEvaluated(), + PERMISSION_BODY_SENSORS_BACKGROUND, + PermissionFlags.RUNTIME_GRANTED, + ) + setPermissionFlags( + APP_ID_1, + getUserIdEvaluated(), + PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND, + PermissionFlags.RUNTIME_GRANTED, + ) + } + + val bodySensorsFlags = + getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS) + val bodySensorsBackgroundFlags = + getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS_BACKGROUND) + val readHealthDataInBackgroundFlags = + getPermissionFlags( + APP_ID_1, + getUserIdEvaluated(), + PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND, + ) + val expectedNewFlags = 0 + assertWithMessage( + "After $action is called for a package that has mismatching health permissions," + + " the actual permission flags for body sensors $bodySensorsFlags should" + + " match the expected flags $expectedNewFlags" + ) + .that(bodySensorsFlags) + .isEqualTo(expectedNewFlags) + assertWithMessage( + "After $action is called for a package that has mismatching health permissions," + + " the actual permission flags for body sensors background" + + " $bodySensorsBackgroundFlags should match the expected flags" + + " $expectedNewFlags" + ) + .that(bodySensorsBackgroundFlags) + .isEqualTo(expectedNewFlags) + assertWithMessage( + "After $action is called for a package that has mismatching health permissions," + + " the actual permission flags for read health data in background" + + " $readHealthDataInBackgroundFlags" + + " should match the expected flags $expectedNewFlags" + ) + .that(readHealthDataInBackgroundFlags) + .isEqualTo(expectedNewFlags) + } + + /** + * Similar to test case above but this time the READ_HR permission is going implicitly to + * explicitly granted (which causes it's grant to be revoked). + * + * Starting: + * - READ_HR=imlpicitly granted + * - BODY_SENSORS=granted + * - BODY_SENSORS_BACKGROUND=granted, + * - READ_HEALTH_DATA_IN_BACKGROUND=granted + * + * Actions: + * - READ_HR->revoked (due to implicit permission being explicitly requested) + * - BODY_SENSORS->revoked (due to READ_HR mismatch) + * - BODY_SENSORS_BACKGROUND->revoked (due to new BODY_SENSORS mismatch) + * - READ_HEALTH_DATA_IN_BACKGROUND->revoked (due to new BODY_SENSORS_BACKGROUND mismatch) + * + * End result: All permissions revoked. + */ + @Test + fun testEvaluatePermissionState_implicitHealthPermissionRequested_causesRevocationChain() { + assumeTrue(action != Action.ON_USER_ADDED) + val oldFlags = + PermissionFlags.IMPLICIT or + PermissionFlags.RUNTIME_GRANTED or + PermissionFlags.USER_SET or + PermissionFlags.USER_FIXED + testEvaluatePermissionState( + oldFlags, + PermissionInfo.PROTECTION_DANGEROUS, + permissionName = PERMISSION_READ_HEART_RATE, + requestedPermissions = + setOf( + PERMISSION_READ_HEART_RATE, + PERMISSION_BODY_SENSORS, + PERMISSION_BODY_SENSORS_BACKGROUND, + PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND, + ), + ) { + setPermissionFlags( + APP_ID_1, + getUserIdEvaluated(), + PERMISSION_BODY_SENSORS, + PermissionFlags.RUNTIME_GRANTED, + ) + setPermissionFlags( + APP_ID_1, + getUserIdEvaluated(), + PERMISSION_BODY_SENSORS_BACKGROUND, + PermissionFlags.RUNTIME_GRANTED, + ) + setPermissionFlags( + APP_ID_1, + getUserIdEvaluated(), + PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND, + PermissionFlags.RUNTIME_GRANTED, + ) + } + + val bodySensorsFlags = + getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS) + val bodySensorsBackgroundFlags = + getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS_BACKGROUND) + val readHealthDataInBackgroundFlags = + getPermissionFlags( + APP_ID_1, + getUserIdEvaluated(), + PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND, + ) + val expectedNewFlags = 0 + assertWithMessage( + "After $action is called for a package that has mismatching health permissions," + + "the actual permission flags for body sensors $bodySensorsFlags should match the" + + "expected flags $expectedNewFlags" + ) + .that(bodySensorsFlags) + .isEqualTo(expectedNewFlags) + assertWithMessage( + "After $action is called for a package that has mismatching health permissions," + + "the actual permission flags for body sensors background $bodySensorsBackgroundFlags should" + + "match the expected flags $expectedNewFlags" + ) + .that(bodySensorsBackgroundFlags) + .isEqualTo(expectedNewFlags) + assertWithMessage( + "After $action is called for a package that has mismatching health permissions," + + "the actual permission flags for read health data in background $readHealthDataInBackgroundFlags" + + "should match the expected flags $expectedNewFlags" + ) + .that(readHealthDataInBackgroundFlags) + .isEqualTo(expectedNewFlags) + } + @Test fun testEvaluatePermissionState_noLongerImplicitSystemOrPolicyFixedWasGranted_runtimeGranted() { - val oldFlags = PermissionFlags.IMPLICIT_GRANTED or PermissionFlags.IMPLICIT or - PermissionFlags.SYSTEM_FIXED + val oldFlags = + PermissionFlags.IMPLICIT_GRANTED or + PermissionFlags.IMPLICIT or + PermissionFlags.SYSTEM_FIXED testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {} val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) val expectedNewFlags = PermissionFlags.RUNTIME_GRANTED or PermissionFlags.SYSTEM_FIXED assertWithMessage( - "After $action is called for a package that requests a runtime permission" + - " that was granted and is no longer implicit and is SYSTEM_FIXED or POLICY_FIXED," + - " the actual permission flags $actualFlags should match the expected" + - " flags $expectedNewFlags" - ) + "After $action is called for a package that requests a runtime permission" + + " that was granted and is no longer implicit and is SYSTEM_FIXED or POLICY_FIXED," + + " the actual permission flags $actualFlags should match the expected" + + " flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @@ -556,16 +1000,16 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { testEvaluatePermissionState( oldFlags, PermissionInfo.PROTECTION_DANGEROUS, - permissionInfoFlags = PermissionInfo.FLAG_HARD_RESTRICTED + permissionInfoFlags = PermissionInfo.FLAG_HARD_RESTRICTED, ) {} val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) val expectedNewFlags = oldFlags assertWithMessage( - "After $action is called for a package that requests a runtime hard" + - " restricted permission that is not exempted, the actual permission flags" + - " $actualFlags should match the expected flags $expectedNewFlags" - ) + "After $action is called for a package that requests a runtime hard" + + " restricted permission that is not exempted, the actual permission flags" + + " $actualFlags should match the expected flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @@ -576,16 +1020,16 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { testEvaluatePermissionState( oldFlags, PermissionInfo.PROTECTION_DANGEROUS, - permissionInfoFlags = PermissionInfo.FLAG_SOFT_RESTRICTED + permissionInfoFlags = PermissionInfo.FLAG_SOFT_RESTRICTED, ) {} val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) val expectedNewFlags = PermissionFlags.UPGRADE_EXEMPT assertWithMessage( - "After $action is called for a package that requests a runtime soft" + - " restricted permission that is exempted, the actual permission flags" + - " $actualFlags should match the expected flags $expectedNewFlags" - ) + "After $action is called for a package that requests a runtime soft" + + " restricted permission that is exempted, the actual permission flags" + + " $actualFlags should match the expected flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @@ -595,18 +1039,20 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { val oldImplicitPermissionFlags = PermissionFlags.USER_FIXED testInheritImplicitPermissionStates( implicitPermissionFlags = oldImplicitPermissionFlags, - isNewInstallAndNewPermission = false + isNewInstallAndNewPermission = false, ) val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) - val expectedNewFlags = oldImplicitPermissionFlags or PermissionFlags.IMPLICIT_GRANTED or - PermissionFlags.APP_OP_REVOKED + val expectedNewFlags = + oldImplicitPermissionFlags or + PermissionFlags.IMPLICIT_GRANTED or + PermissionFlags.APP_OP_REVOKED assertWithMessage( - "After $action is called for a package that requests a permission that is" + - " implicit, existing and runtime, it should not inherit the runtime flags from" + - " the source permission. Hence the actual permission flags $actualFlags should" + - " match the expected flags $expectedNewFlags" - ) + "After $action is called for a package that requests a permission that is" + + " implicit, existing and runtime, it should not inherit the runtime flags from" + + " the source permission. Hence the actual permission flags $actualFlags should" + + " match the expected flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @@ -620,11 +1066,11 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) val expectedNewFlags = PermissionFlags.INSTALL_GRANTED assertWithMessage( - "After $action is called for a package that requests a permission that is" + - " implicit, new and non-runtime, it should not inherit the runtime flags from" + - " the source permission. Hence the actual permission flags $actualFlags should" + - " match the expected flags $expectedNewFlags" - ) + "After $action is called for a package that requests a permission that is" + + " implicit, new and non-runtime, it should not inherit the runtime flags from" + + " the source permission. Hence the actual permission flags $actualFlags should" + + " match the expected flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @@ -635,14 +1081,14 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { testInheritImplicitPermissionStates(sourceRuntimeFlags = sourceRuntimeFlags) val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) - val expectedNewFlags = sourceRuntimeFlags or PermissionFlags.IMPLICIT_GRANTED or - PermissionFlags.IMPLICIT + val expectedNewFlags = + sourceRuntimeFlags or PermissionFlags.IMPLICIT_GRANTED or PermissionFlags.IMPLICIT assertWithMessage( - "After $action is called for a package that requests a permission that is" + - " implicit, new and runtime, it should inherit the runtime flags from" + - " the source permission. Hence the actual permission flags $actualFlags should" + - " match the expected flags $expectedNewFlags" - ) + "After $action is called for a package that requests a permission that is" + + " implicit, new and runtime, it should inherit the runtime flags from" + + " the source permission. Hence the actual permission flags $actualFlags should" + + " match the expected flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @@ -653,17 +1099,17 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { testInheritImplicitPermissionStates( implicitPermissionFlags = PermissionFlags.POLICY_FIXED, sourceRuntimeFlags = sourceRuntimeFlags, - isAnySourcePermissionNonRuntime = false + isAnySourcePermissionNonRuntime = false, ) val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0) val expectedNewFlags = sourceRuntimeFlags or PermissionFlags.IMPLICIT assertWithMessage( - "After $action is called for a package that requests a permission that is" + - " implicit, existing, runtime and revoked, it should only inherit runtime flags" + - " from source permission. Hence the actual permission flags $actualFlags should" + - " match the expected flags $expectedNewFlags" - ) + "After $action is called for a package that requests a permission that is" + + " implicit, existing, runtime and revoked, it should only inherit runtime flags" + + " from source permission. Hence the actual permission flags $actualFlags should" + + " match the expected flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @@ -678,21 +1124,18 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { val sourceRuntimeFlags = PermissionFlags.RUNTIME_GRANTED or PermissionFlags.USER_SET testInheritImplicitPermissionStates( implicitPermissionName = PERMISSION_ACCESS_MEDIA_LOCATION, - sourceRuntimeFlags = sourceRuntimeFlags + sourceRuntimeFlags = sourceRuntimeFlags, ) - val actualFlags = getPermissionFlags( - APP_ID_1, - getUserIdEvaluated(), - PERMISSION_ACCESS_MEDIA_LOCATION - ) + val actualFlags = + getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_ACCESS_MEDIA_LOCATION) val expectedNewFlags = sourceRuntimeFlags or PermissionFlags.IMPLICIT_GRANTED assertWithMessage( - "After $action is called for a package that requests a media permission that" + - " is implicit, new and runtime, it should inherit the runtime flags from" + - " the source permission and have the IMPLICIT flag removed. Hence the actual" + - " permission flags $actualFlags should match the expected flags $expectedNewFlags" - ) + "After $action is called for a package that requests a media permission that" + + " is implicit, new and runtime, it should inherit the runtime flags from" + + " the source permission and have the IMPLICIT flag removed. Hence the actual" + + " permission flags $actualFlags should match the expected flags $expectedNewFlags" + ) .that(actualFlags) .isEqualTo(expectedNewFlags) } @@ -703,57 +1146,65 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { implicitPermissionProtectionLevel: Int = PermissionInfo.PROTECTION_DANGEROUS, sourceRuntimeFlags: Int = PermissionFlags.RUNTIME_GRANTED or PermissionFlags.USER_SET, isAnySourcePermissionNonRuntime: Boolean = true, - isNewInstallAndNewPermission: Boolean = true + isNewInstallAndNewPermission: Boolean = true, ) { val userId = getUserIdEvaluated() - val implicitPermission = mockParsedPermission( - implicitPermissionName, - PACKAGE_NAME_0, - protectionLevel = implicitPermissionProtectionLevel, - ) + val implicitPermission = + mockParsedPermission( + implicitPermissionName, + PACKAGE_NAME_0, + protectionLevel = implicitPermissionProtectionLevel, + ) // For source from non-runtime in order to grant by implicit - val sourcePermission1 = mockParsedPermission( - PERMISSION_NAME_1, - PACKAGE_NAME_0, - protectionLevel = if (isAnySourcePermissionNonRuntime) { - PermissionInfo.PROTECTION_NORMAL - } else { - PermissionInfo.PROTECTION_DANGEROUS - } - ) + val sourcePermission1 = + mockParsedPermission( + PERMISSION_NAME_1, + PACKAGE_NAME_0, + protectionLevel = + if (isAnySourcePermissionNonRuntime) { + PermissionInfo.PROTECTION_NORMAL + } else { + PermissionInfo.PROTECTION_DANGEROUS + }, + ) // For inheriting runtime flags - val sourcePermission2 = mockParsedPermission( - PERMISSION_NAME_2, - PACKAGE_NAME_0, - protectionLevel = PermissionInfo.PROTECTION_DANGEROUS, - ) - val permissionOwnerPackageState = mockPackageState( - APP_ID_0, - mockAndroidPackage( + val sourcePermission2 = + mockParsedPermission( + PERMISSION_NAME_2, PACKAGE_NAME_0, - permissions = listOf(implicitPermission, sourcePermission1, sourcePermission2) + protectionLevel = PermissionInfo.PROTECTION_DANGEROUS, ) - ) - val installedPackageState = mockPackageState( - APP_ID_1, - mockAndroidPackage( - PACKAGE_NAME_1, - requestedPermissions = setOf( - implicitPermissionName, - PERMISSION_NAME_1, - PERMISSION_NAME_2 + val permissionOwnerPackageState = + mockPackageState( + APP_ID_0, + mockAndroidPackage( + PACKAGE_NAME_0, + permissions = listOf(implicitPermission, sourcePermission1, sourcePermission2), ), - implicitPermissions = setOf(implicitPermissionName) ) - ) - oldState.mutateExternalState().setImplicitToSourcePermissions( - MutableIndexedMap<String, IndexedListSet<String>>().apply { - put(implicitPermissionName, MutableIndexedListSet<String>().apply { - add(PERMISSION_NAME_1) - add(PERMISSION_NAME_2) - }) - } - ) + val installedPackageState = + mockPackageState( + APP_ID_1, + mockAndroidPackage( + PACKAGE_NAME_1, + requestedPermissions = + setOf(implicitPermissionName, PERMISSION_NAME_1, PERMISSION_NAME_2), + implicitPermissions = setOf(implicitPermissionName), + ), + ) + oldState + .mutateExternalState() + .setImplicitToSourcePermissions( + MutableIndexedMap<String, IndexedListSet<String>>().apply { + put( + implicitPermissionName, + MutableIndexedListSet<String>().apply { + add(PERMISSION_NAME_1) + add(PERMISSION_NAME_2) + }, + ) + } + ) addPackageState(permissionOwnerPackageState) addPermission(implicitPermission) addPermission(sourcePermission1) @@ -772,7 +1223,7 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { userId, implicitPermissionName, implicitPermissionFlags, - newState + newState, ) } testAction(installedPackageState) @@ -781,18 +1232,17 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { /** * Setup simple package states for testing evaluatePermissionState(). - * permissionOwnerPackageState is definer of permissionName with APP_ID_0. - * installedPackageState is the installed package that requests permissionName with APP_ID_1. + * permissionOwnerPackageState is definer of permissionName with APP_ID_0. installedPackageState + * is the installed package that requests permissionName with APP_ID_1. * * @param oldFlags the existing permission flags for APP_ID_1, userId, permissionName * @param protectionLevel the protectionLevel for the permission * @param permissionName the name of the permission (1) being defined (2) of the oldFlags, and - * (3) requested by installedPackageState + * (3) requested by installedPackageState * @param requestedPermissions the permissions requested by installedPackageState * @param implicitPermissions the implicit permissions of installedPackageState * @param permissionInfoFlags the flags for the permission itself * @param isInstalledPackageSystem whether installedPackageState is a system package - * * @return installedPackageState */ private fun testEvaluatePermissionState( @@ -809,33 +1259,36 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { isInstalledPackageVendor: Boolean = false, installedPackageTargetSdkVersion: Int = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, isNewInstall: Boolean = false, - additionalSetup: () -> Unit + additionalSetup: () -> Unit, ) { val userId = getUserIdEvaluated() - val parsedPermission = mockParsedPermission( - permissionName, - PACKAGE_NAME_0, - protectionLevel = protectionLevel, - flags = permissionInfoFlags - ) - val permissionOwnerPackageState = mockPackageState( - APP_ID_0, - mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(parsedPermission)) - ) - val installedPackageState = mockPackageState( - APP_ID_1, - mockAndroidPackage( - PACKAGE_NAME_1, - requestedPermissions = requestedPermissions, - implicitPermissions = implicitPermissions, - targetSdkVersion = installedPackageTargetSdkVersion, - isSignatureMatching = isInstalledPackageSignatureMatching - ), - isSystem = isInstalledPackageSystem, - isPrivileged = isInstalledPackagePrivileged, - isProduct = isInstalledPackageProduct, - isVendor = isInstalledPackageVendor - ) + val parsedPermission = + mockParsedPermission( + permissionName, + PACKAGE_NAME_0, + protectionLevel = protectionLevel, + flags = permissionInfoFlags, + ) + val permissionOwnerPackageState = + mockPackageState( + APP_ID_0, + mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(parsedPermission)), + ) + val installedPackageState = + mockPackageState( + APP_ID_1, + mockAndroidPackage( + PACKAGE_NAME_1, + requestedPermissions = requestedPermissions, + implicitPermissions = implicitPermissions, + targetSdkVersion = installedPackageTargetSdkVersion, + isSignatureMatching = isInstalledPackageSignatureMatching, + ), + isSystem = isInstalledPackageSystem, + isPrivileged = isInstalledPackagePrivileged, + isProduct = isInstalledPackageProduct, + isVendor = isInstalledPackageVendor, + ) addPackageState(permissionOwnerPackageState) if (!isNewInstall) { addPackageState(installedPackageState) @@ -854,26 +1307,29 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() { } } - private fun getUserIdEvaluated(): Int = when (action) { - Action.ON_USER_ADDED -> USER_ID_NEW - Action.ON_STORAGE_VOLUME_ADDED, Action.ON_PACKAGE_ADDED -> USER_ID_0 - } + private fun getUserIdEvaluated(): Int = + when (action) { + Action.ON_USER_ADDED -> USER_ID_NEW + Action.ON_STORAGE_VOLUME_ADDED, + Action.ON_PACKAGE_ADDED -> USER_ID_0 + } private fun MutateStateScope.testAction(packageState: PackageState) { with(appIdPermissionPolicy) { when (action) { Action.ON_USER_ADDED -> onUserAdded(getUserIdEvaluated()) - Action.ON_STORAGE_VOLUME_ADDED -> onStorageVolumeMounted( - null, - listOf(packageState.packageName), - true - ) + Action.ON_STORAGE_VOLUME_ADDED -> + onStorageVolumeMounted(null, listOf(packageState.packageName), true) Action.ON_PACKAGE_ADDED -> onPackageAdded(packageState) } } } - enum class Action { ON_USER_ADDED, ON_STORAGE_VOLUME_ADDED, ON_PACKAGE_ADDED } + enum class Action { + ON_USER_ADDED, + ON_STORAGE_VOLUME_ADDED, + ON_PACKAGE_ADDED, + } companion object { @Parameterized.Parameters(name = "{0}") diff --git a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/BasePermissionPolicyTest.kt b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/BasePermissionPolicyTest.kt index 7b3f21603c0a..6dfd2611e0af 100644 --- a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/BasePermissionPolicyTest.kt +++ b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/BasePermissionPolicyTest.kt @@ -21,6 +21,7 @@ import android.content.pm.PackageManager import android.content.pm.PermissionGroupInfo import android.content.pm.PermissionInfo import android.content.pm.SigningDetails +import android.health.connect.HealthPermissions import android.os.Build import android.os.Bundle import android.util.ArrayMap @@ -49,32 +50,24 @@ import org.junit.Rule import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyLong -/** - * Mocking unit test for AppIdPermissionPolicy. - */ +/** Mocking unit test for AppIdPermissionPolicy. */ @RunWith(AndroidJUnit4::class) abstract class BasePermissionPolicyTest { protected lateinit var oldState: MutableAccessState protected lateinit var newState: MutableAccessState - protected val defaultPermissionGroup = mockParsedPermissionGroup( - PERMISSION_GROUP_NAME_0, - PACKAGE_NAME_0 - ) - protected val defaultPermissionTree = mockParsedPermission( - PERMISSION_TREE_NAME, - PACKAGE_NAME_0, - isTree = true - ) + protected val defaultPermissionGroup = + mockParsedPermissionGroup(PERMISSION_GROUP_NAME_0, PACKAGE_NAME_0) + protected val defaultPermissionTree = + mockParsedPermission(PERMISSION_TREE_NAME, PACKAGE_NAME_0, isTree = true) protected val defaultPermission = mockParsedPermission(PERMISSION_NAME_0, PACKAGE_NAME_0) protected val appIdPermissionPolicy = AppIdPermissionPolicy() @Rule @JvmField - val extendedMockitoRule = ExtendedMockitoRule.Builder(this) - .spyStatic(PackageInfoUtils::class.java) - .build() + val extendedMockitoRule = + ExtendedMockitoRule.Builder(this).spyStatic(PackageInfoUtils::class.java).build() @Before fun baseSetUp() { @@ -93,65 +86,76 @@ abstract class BasePermissionPolicyTest { private fun mockPackageInfoUtilsGeneratePermissionInfo() { wheneverStatic { - PackageInfoUtils.generatePermissionInfo(any(ParsedPermission::class.java), anyLong()) - }.thenAnswer { invocation -> - val parsedPermission = invocation.getArgument<ParsedPermission>(0) - val generateFlags = invocation.getArgument<Long>(1) - PermissionInfo(parsedPermission.backgroundPermission).apply { - name = parsedPermission.name - packageName = parsedPermission.packageName - metaData = if (generateFlags.toInt().hasBits(PackageManager.GET_META_DATA)) { - parsedPermission.metaData - } else { - null + PackageInfoUtils.generatePermissionInfo( + any(ParsedPermission::class.java), + anyLong(), + ) + } + .thenAnswer { invocation -> + val parsedPermission = invocation.getArgument<ParsedPermission>(0) + val generateFlags = invocation.getArgument<Long>(1) + PermissionInfo(parsedPermission.backgroundPermission).apply { + name = parsedPermission.name + packageName = parsedPermission.packageName + metaData = + if (generateFlags.toInt().hasBits(PackageManager.GET_META_DATA)) { + parsedPermission.metaData + } else { + null + } + @Suppress("DEPRECATION") + protectionLevel = parsedPermission.protectionLevel + group = parsedPermission.group + flags = parsedPermission.flags } - @Suppress("DEPRECATION") - protectionLevel = parsedPermission.protectionLevel - group = parsedPermission.group - flags = parsedPermission.flags } - } } private fun mockPackageInfoUtilsGeneratePermissionGroupInfo() { wheneverStatic { - PackageInfoUtils.generatePermissionGroupInfo( - any(ParsedPermissionGroup::class.java), - anyLong() - ) - }.thenAnswer { invocation -> - val parsedPermissionGroup = invocation.getArgument<ParsedPermissionGroup>(0) - val generateFlags = invocation.getArgument<Long>(1) - @Suppress("DEPRECATION") - PermissionGroupInfo().apply { - name = parsedPermissionGroup.name - packageName = parsedPermissionGroup.packageName - metaData = if (generateFlags.toInt().hasBits(PackageManager.GET_META_DATA)) { - parsedPermissionGroup.metaData - } else { - null + PackageInfoUtils.generatePermissionGroupInfo( + any(ParsedPermissionGroup::class.java), + anyLong(), + ) + } + .thenAnswer { invocation -> + val parsedPermissionGroup = invocation.getArgument<ParsedPermissionGroup>(0) + val generateFlags = invocation.getArgument<Long>(1) + @Suppress("DEPRECATION") + PermissionGroupInfo().apply { + name = parsedPermissionGroup.name + packageName = parsedPermissionGroup.packageName + metaData = + if (generateFlags.toInt().hasBits(PackageManager.GET_META_DATA)) { + parsedPermissionGroup.metaData + } else { + null + } + flags = parsedPermissionGroup.flags } - flags = parsedPermissionGroup.flags } - } } - /** - * Mock an AndroidPackage with PACKAGE_NAME_0, PERMISSION_NAME_0 and PERMISSION_GROUP_NAME_0 - */ + /** Mock an AndroidPackage with PACKAGE_NAME_0, PERMISSION_NAME_0 and PERMISSION_GROUP_NAME_0 */ protected fun mockSimpleAndroidPackage(): AndroidPackage = mockAndroidPackage( PACKAGE_NAME_0, permissionGroups = listOf(defaultPermissionGroup), - permissions = listOf(defaultPermissionTree, defaultPermission) + permissions = listOf(defaultPermissionTree, defaultPermission), ) protected fun createSimplePermission(isTree: Boolean = false): Permission { - val parsedPermission = if (isTree) { defaultPermissionTree } else { defaultPermission } - val permissionInfo = PackageInfoUtils.generatePermissionInfo( - parsedPermission, - PackageManager.GET_META_DATA.toLong() - )!! + val parsedPermission = + if (isTree) { + defaultPermissionTree + } else { + defaultPermission + } + val permissionInfo = + PackageInfoUtils.generatePermissionInfo( + parsedPermission, + PackageManager.GET_META_DATA.toLong(), + )!! return Permission(permissionInfo, true, Permission.TYPE_MANIFEST, APP_ID_0) } @@ -164,13 +168,12 @@ abstract class BasePermissionPolicyTest { appId: Int, packageName: String, isSystem: Boolean = false, - ): PackageState = - mock { - whenever(this.appId).thenReturn(appId) - whenever(this.packageName).thenReturn(packageName) - whenever(androidPackage).thenReturn(null) - whenever(this.isSystem).thenReturn(isSystem) - } + ): PackageState = mock { + whenever(this.appId).thenReturn(appId) + whenever(this.packageName).thenReturn(packageName) + whenever(androidPackage).thenReturn(null) + whenever(this.isSystem).thenReturn(isSystem) + } protected fun mockPackageState( appId: Int, @@ -179,22 +182,22 @@ abstract class BasePermissionPolicyTest { isPrivileged: Boolean = false, isProduct: Boolean = false, isInstantApp: Boolean = false, - isVendor: Boolean = false - ): PackageState = - mock { - whenever(this.appId).thenReturn(appId) - whenever(this.androidPackage).thenReturn(androidPackage) - val packageName = androidPackage.packageName - whenever(this.packageName).thenReturn(packageName) - whenever(this.isSystem).thenReturn(isSystem) - whenever(this.isPrivileged).thenReturn(isPrivileged) - whenever(this.isProduct).thenReturn(isProduct) - whenever(this.isVendor).thenReturn(isVendor) - val userStates = SparseArray<PackageUserState>().apply { + isVendor: Boolean = false, + ): PackageState = mock { + whenever(this.appId).thenReturn(appId) + whenever(this.androidPackage).thenReturn(androidPackage) + val packageName = androidPackage.packageName + whenever(this.packageName).thenReturn(packageName) + whenever(this.isSystem).thenReturn(isSystem) + whenever(this.isPrivileged).thenReturn(isPrivileged) + whenever(this.isProduct).thenReturn(isProduct) + whenever(this.isVendor).thenReturn(isVendor) + val userStates = + SparseArray<PackageUserState>().apply { put(USER_ID_0, mock { whenever(this.isInstantApp).thenReturn(isInstantApp) }) } - whenever(this.userStates).thenReturn(userStates) - } + whenever(this.userStates).thenReturn(userStates) + } protected fun mockAndroidPackage( packageName: String, @@ -205,28 +208,26 @@ abstract class BasePermissionPolicyTest { requestedPermissions: Set<String> = emptySet(), permissionGroups: List<ParsedPermissionGroup> = emptyList(), permissions: List<ParsedPermission> = emptyList(), - isSignatureMatching: Boolean = false - ): AndroidPackage = - mock { - whenever(this.packageName).thenReturn(packageName) - whenever(this.targetSdkVersion).thenReturn(targetSdkVersion) - whenever(this.isRequestLegacyExternalStorage).thenReturn(isRequestLegacyExternalStorage) - whenever(this.adoptPermissions).thenReturn(adoptPermissions) - whenever(this.implicitPermissions).thenReturn(implicitPermissions) - whenever(this.requestedPermissions).thenReturn(requestedPermissions) - whenever(this.permissionGroups).thenReturn(permissionGroups) - whenever(this.permissions).thenReturn(permissions) - val signingDetails = mock<SigningDetails> { - whenever( - hasCommonSignerWithCapability(any(), any()) - ).thenReturn(isSignatureMatching) + isSignatureMatching: Boolean = false, + ): AndroidPackage = mock { + whenever(this.packageName).thenReturn(packageName) + whenever(this.targetSdkVersion).thenReturn(targetSdkVersion) + whenever(this.isRequestLegacyExternalStorage).thenReturn(isRequestLegacyExternalStorage) + whenever(this.adoptPermissions).thenReturn(adoptPermissions) + whenever(this.implicitPermissions).thenReturn(implicitPermissions) + whenever(this.requestedPermissions).thenReturn(requestedPermissions) + whenever(this.permissionGroups).thenReturn(permissionGroups) + whenever(this.permissions).thenReturn(permissions) + val signingDetails = + mock<SigningDetails> { + whenever(hasCommonSignerWithCapability(any(), any())) + .thenReturn(isSignatureMatching) whenever(hasAncestorOrSelf(any())).thenReturn(isSignatureMatching) - whenever( - checkCapability(any<SigningDetails>(), any()) - ).thenReturn(isSignatureMatching) + whenever(checkCapability(any<SigningDetails>(), any())) + .thenReturn(isSignatureMatching) } - whenever(this.signingDetails).thenReturn(signingDetails) - } + whenever(this.signingDetails).thenReturn(signingDetails) + } protected fun mockParsedPermission( permissionName: String, @@ -235,72 +236,74 @@ abstract class BasePermissionPolicyTest { group: String? = null, protectionLevel: Int = PermissionInfo.PROTECTION_NORMAL, flags: Int = 0, - isTree: Boolean = false - ): ParsedPermission = - mock { - whenever(name).thenReturn(permissionName) - whenever(this.packageName).thenReturn(packageName) - whenever(metaData).thenReturn(Bundle()) - whenever(this.backgroundPermission).thenReturn(backgroundPermission) - whenever(this.group).thenReturn(group) - whenever(this.protectionLevel).thenReturn(protectionLevel) - whenever(this.flags).thenReturn(flags) - whenever(this.isTree).thenReturn(isTree) - } + isTree: Boolean = false, + ): ParsedPermission = mock { + whenever(name).thenReturn(permissionName) + whenever(this.packageName).thenReturn(packageName) + whenever(metaData).thenReturn(Bundle()) + whenever(this.backgroundPermission).thenReturn(backgroundPermission) + whenever(this.group).thenReturn(group) + whenever(this.protectionLevel).thenReturn(protectionLevel) + whenever(this.flags).thenReturn(flags) + whenever(this.isTree).thenReturn(isTree) + } protected fun mockParsedPermissionGroup( permissionGroupName: String, packageName: String, - ): ParsedPermissionGroup = - mock { - whenever(name).thenReturn(permissionGroupName) - whenever(this.packageName).thenReturn(packageName) - whenever(metaData).thenReturn(Bundle()) - } + ): ParsedPermissionGroup = mock { + whenever(name).thenReturn(permissionGroupName) + whenever(this.packageName).thenReturn(packageName) + whenever(metaData).thenReturn(Bundle()) + } protected fun addPackageState( packageState: PackageState, - state: MutableAccessState = oldState + state: MutableAccessState = oldState, ) { state.mutateExternalState().apply { setPackageStates( packageStates.toMutableMap().apply { put(packageState.packageName, packageState) } ) - mutateAppIdPackageNames().mutateOrPut(packageState.appId) { MutableIndexedListSet() } + mutateAppIdPackageNames() + .mutateOrPut(packageState.appId) { MutableIndexedListSet() } .add(packageState.packageName) } } protected fun removePackageState( packageState: PackageState, - state: MutableAccessState = oldState + state: MutableAccessState = oldState, ) { state.mutateExternalState().apply { setPackageStates( packageStates.toMutableMap().apply { remove(packageState.packageName) } ) - mutateAppIdPackageNames().mutateOrPut(packageState.appId) { MutableIndexedListSet() } + mutateAppIdPackageNames() + .mutateOrPut(packageState.appId) { MutableIndexedListSet() } .remove(packageState.packageName) } } protected fun addDisabledSystemPackageState( packageState: PackageState, - state: MutableAccessState = oldState - ) = state.mutateExternalState().apply { - (disabledSystemPackageStates as ArrayMap)[packageState.packageName] = packageState - } + state: MutableAccessState = oldState, + ) = + state.mutateExternalState().apply { + (disabledSystemPackageStates as ArrayMap)[packageState.packageName] = packageState + } protected fun addPermission( parsedPermission: ParsedPermission, type: Int = Permission.TYPE_MANIFEST, isReconciled: Boolean = true, - state: MutableAccessState = oldState + state: MutableAccessState = oldState, ) { - val permissionInfo = PackageInfoUtils.generatePermissionInfo( - parsedPermission, - PackageManager.GET_META_DATA.toLong() - )!! + val permissionInfo = + PackageInfoUtils.generatePermissionInfo( + parsedPermission, + PackageManager.GET_META_DATA.toLong(), + )!! val appId = state.externalState.packageStates[permissionInfo.packageName]!!.appId val permission = Permission(permissionInfo, isReconciled, type, appId) if (parsedPermission.isTree) { @@ -312,35 +315,35 @@ abstract class BasePermissionPolicyTest { protected fun addPermissionGroup( parsedPermissionGroup: ParsedPermissionGroup, - state: MutableAccessState = oldState + state: MutableAccessState = oldState, ) { state.mutateSystemState().mutatePermissionGroups()[parsedPermissionGroup.name] = PackageInfoUtils.generatePermissionGroupInfo( parsedPermissionGroup, - PackageManager.GET_META_DATA.toLong() + PackageManager.GET_META_DATA.toLong(), )!! } protected fun getPermission( permissionName: String, - state: MutableAccessState = newState + state: MutableAccessState = newState, ): Permission? = state.systemState.permissions[permissionName] protected fun getPermissionTree( permissionTreeName: String, - state: MutableAccessState = newState + state: MutableAccessState = newState, ): Permission? = state.systemState.permissionTrees[permissionTreeName] protected fun getPermissionGroup( permissionGroupName: String, - state: MutableAccessState = newState + state: MutableAccessState = newState, ): PermissionGroupInfo? = state.systemState.permissionGroups[permissionGroupName] protected fun getPermissionFlags( appId: Int, userId: Int, permissionName: String, - state: MutableAccessState = newState + state: MutableAccessState = newState, ): Int = state.userStates[userId]?.appIdPermissionFlags?.get(appId).getWithDefault(permissionName, 0) @@ -349,11 +352,13 @@ abstract class BasePermissionPolicyTest { userId: Int, permissionName: String, flags: Int, - state: MutableAccessState = oldState + state: MutableAccessState = oldState, ) = - state.mutateUserState(userId)!!.mutateAppIdPermissionFlags().mutateOrPut(appId) { - MutableIndexedMap() - }.put(permissionName, flags) + state + .mutateUserState(userId)!! + .mutateAppIdPermissionFlags() + .mutateOrPut(appId) { MutableIndexedMap() } + .put(permissionName, flags) companion object { @JvmStatic protected val PACKAGE_NAME_0 = "packageName0" @@ -375,16 +380,25 @@ abstract class BasePermissionPolicyTest { @JvmStatic protected val PERMISSION_NAME_1 = "permissionName1" @JvmStatic protected val PERMISSION_NAME_2 = "permissionName2" @JvmStatic protected val PERMISSION_BELONGS_TO_A_TREE = "permissionTree.permission" - @JvmStatic protected val PERMISSION_READ_EXTERNAL_STORAGE = - Manifest.permission.READ_EXTERNAL_STORAGE - @JvmStatic protected val PERMISSION_POST_NOTIFICATIONS = - Manifest.permission.POST_NOTIFICATIONS - @JvmStatic protected val PERMISSION_BLUETOOTH_CONNECT = - Manifest.permission.BLUETOOTH_CONNECT - @JvmStatic protected val PERMISSION_ACCESS_BACKGROUND_LOCATION = + @JvmStatic + protected val PERMISSION_READ_EXTERNAL_STORAGE = Manifest.permission.READ_EXTERNAL_STORAGE + @JvmStatic + protected val PERMISSION_POST_NOTIFICATIONS = Manifest.permission.POST_NOTIFICATIONS + @JvmStatic + protected val PERMISSION_BLUETOOTH_CONNECT = Manifest.permission.BLUETOOTH_CONNECT + @JvmStatic + protected val PERMISSION_ACCESS_BACKGROUND_LOCATION = Manifest.permission.ACCESS_BACKGROUND_LOCATION - @JvmStatic protected val PERMISSION_ACCESS_MEDIA_LOCATION = - Manifest.permission.ACCESS_MEDIA_LOCATION + @JvmStatic + protected val PERMISSION_ACCESS_MEDIA_LOCATION = Manifest.permission.ACCESS_MEDIA_LOCATION + @JvmStatic protected val PERMISSION_BODY_SENSORS = Manifest.permission.BODY_SENSORS + @JvmStatic + protected val PERMISSION_BODY_SENSORS_BACKGROUND = + Manifest.permission.BODY_SENSORS_BACKGROUND + @JvmStatic protected val PERMISSION_READ_HEART_RATE = HealthPermissions.READ_HEART_RATE + @JvmStatic + protected val PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND = + HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND @JvmStatic protected val USER_ID_0 = 0 @JvmStatic protected val USER_ID_NEW = 1 diff --git a/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java index ab3784b07e10..259ba989a021 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java @@ -34,25 +34,32 @@ import static com.android.server.am.PendingIntentRecord.FLAG_ACTIVITY_SENDER; import static com.android.server.am.PendingIntentRecord.cancelReasonToString; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import android.annotation.NonNull; import android.app.ActivityManager; import android.app.ActivityManagerInternal; +import android.app.ActivityOptions; import android.app.AppGlobals; import android.app.BackgroundStartPrivileges; import android.app.PendingIntent; import android.content.Intent; import android.content.pm.IPackageManager; import android.os.Binder; +import android.os.Bundle; import android.os.Looper; import android.os.UserHandle; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.server.AlarmManagerInternal; import com.android.server.LocalServices; @@ -108,7 +115,8 @@ public class PendingIntentControllerTest { mPendingIntentController.onActivityManagerInternalAdded(); } - private PendingIntentRecord createPendingIntentRecord(int flags) { + @NonNull + private PendingIntentRecord createPendingIntentRecord(@PendingIntent.Flags int flags) { return mPendingIntentController.getIntentSender(ActivityManager.INTENT_SENDER_BROADCAST, TEST_PACKAGE_NAME, TEST_FEATURE_ID, TEST_CALLING_UID, TEST_USER_ID, null, null, 0, TEST_INTENTS, null, flags, null); @@ -219,6 +227,58 @@ public class PendingIntentControllerTest { allowlistDurationLocked.type); } + @Test + public void testSendWithBundleExtras() { + final PendingIntentRecord pir = createPendingIntentRecord(0); + final ActivityOptions activityOptions = ActivityOptions.makeBasic(); + activityOptions.setLaunchDisplayId(2); + activityOptions.setLaunchTaskId(123); + final Bundle options = activityOptions.toBundle(); + options.putString("testKey", "testValue"); + + pir.send(0, null, null, null, null, null, options); + + final ArgumentCaptor<Bundle> resultExtrasCaptor = ArgumentCaptor.forClass(Bundle.class); + verify(mActivityManagerInternal).broadcastIntentInPackage( + eq(TEST_PACKAGE_NAME), + eq(TEST_FEATURE_ID), + eq(TEST_CALLING_UID), + eq(TEST_CALLING_UID), // realCallingUid + anyInt(), // realCallingPid + any(), // intent + any(), // resolvedType + any(), // resultToThread + any(), // resultTo + anyInt(), // resultCode + any(), // resultData + resultExtrasCaptor.capture(), // resultExtras + any(), // requiredPermission + eq(options), // bOptions + anyBoolean(), // serialized + anyBoolean(), // sticky + anyInt(), // userId + any(), // backgroundStartPrivileges + any() // broadcastAllowList + ); + final Bundle result = resultExtrasCaptor.getValue(); + if (com.android.window.flags.Flags.supportWidgetIntentsOnConnectedDisplay()) { + // Check that only launchDisplayId in ActivityOptions is passed via resultExtras. + final ActivityOptions expected = ActivityOptions.makeBasic().setLaunchDisplayId(2); + assertBundleEquals(expected.toBundle(), result); + // Check that launchTaskId is dropped in resultExtras. + assertNotEquals(123, ActivityOptions.fromBundle(result).getLaunchTaskId()); + } else { + assertNull(result); + } + } + + private void assertBundleEquals(@NonNull Bundle expected, @NonNull Bundle observed) { + assertEquals(expected.size(), observed.size()); + for (String key : expected.keySet()) { + assertEquals(expected.get(key), observed.get(key)); + } + } + private void assertCancelReason(int expectedReason, int actualReason) { final String errMsg = "Expected: " + cancelReasonToString(expectedReason) + "; Actual: " + cancelReasonToString(actualReason); diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java index 026e72f117b4..d0226805224e 100644 --- a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java @@ -26,15 +26,12 @@ import static android.app.AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUD import static android.app.AppOpsManager.OP_RECORD_AUDIO; import static android.app.AppOpsManager.OP_WIFI_SCAN; import static android.app.AppOpsManager.UID_STATE_BACKGROUND; -import static android.app.AppOpsManager.UID_STATE_CACHED; import static android.app.AppOpsManager.UID_STATE_FOREGROUND; import static android.app.AppOpsManager.UID_STATE_FOREGROUND_SERVICE; import static android.app.AppOpsManager.UID_STATE_MAX_LAST_NON_RESTRICTED; import static android.app.AppOpsManager.UID_STATE_TOP; import static android.permission.flags.Flags.delayUidStateChangesFromCapabilityUpdates; -import static com.android.server.appop.AppOpsUidStateTracker.processStateToUidState; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -42,7 +39,6 @@ import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -64,7 +60,6 @@ import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.quality.Strictness; import java.util.PriorityQueue; @@ -94,7 +89,6 @@ public class AppOpsUidStateTrackerTest { public void setUp() { mSession = ExtendedMockito.mockitoSession() .initMocks(this) - .strictness(Strictness.LENIENT) .startMocking(); mConstants.TOP_STATE_SETTLE_TIME = 10 * 1000L; mConstants.FG_SERVICE_STATE_SETTLE_TIME = 5 * 1000L; @@ -591,221 +585,6 @@ public class AppOpsUidStateTrackerTest { } @Test - public void testUidStateChangedCallbackCachedToBackground() { - testUidStateChangedCallback( - ActivityManager.PROCESS_STATE_CACHED_ACTIVITY, - ActivityManager.PROCESS_STATE_RECEIVER); - } - - @Test - public void testUidStateChangedCallbackCachedToForeground() { - testUidStateChangedCallback( - ActivityManager.PROCESS_STATE_CACHED_ACTIVITY, - ActivityManager.PROCESS_STATE_BOUND_TOP); - } - - @Test - public void testUidStateChangedCallbackCachedToForegroundService() { - testUidStateChangedCallback( - ActivityManager.PROCESS_STATE_CACHED_ACTIVITY, - ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); - } - - @Test - public void testUidStateChangedCallbackCachedToTop() { - testUidStateChangedCallback( - ActivityManager.PROCESS_STATE_CACHED_ACTIVITY, - ActivityManager.PROCESS_STATE_TOP); - } - - @Test - public void testUidStateChangedCallbackBackgroundToCached() { - testUidStateChangedCallback( - ActivityManager.PROCESS_STATE_RECEIVER, - ActivityManager.PROCESS_STATE_CACHED_ACTIVITY); - } - - @Test - public void testUidStateChangedCallbackBackgroundToForeground() { - testUidStateChangedCallback( - ActivityManager.PROCESS_STATE_RECEIVER, - ActivityManager.PROCESS_STATE_BOUND_TOP); - } - - @Test - public void testUidStateChangedCallbackBackgroundToForegroundService() { - testUidStateChangedCallback( - ActivityManager.PROCESS_STATE_RECEIVER, - ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); - } - - @Test - public void testUidStateChangedCallbackBackgroundToTop() { - testUidStateChangedCallback( - ActivityManager.PROCESS_STATE_RECEIVER, - ActivityManager.PROCESS_STATE_TOP); - } - - @Test - public void testUidStateChangedCallbackForegroundToCached() { - testUidStateChangedCallback( - ActivityManager.PROCESS_STATE_BOUND_TOP, - ActivityManager.PROCESS_STATE_CACHED_ACTIVITY); - } - - @Test - public void testUidStateChangedCallbackForegroundToBackground() { - testUidStateChangedCallback( - ActivityManager.PROCESS_STATE_BOUND_TOP, - ActivityManager.PROCESS_STATE_RECEIVER); - } - - @Test - public void testUidStateChangedCallbackForegroundToForegroundService() { - testUidStateChangedCallback( - ActivityManager.PROCESS_STATE_BOUND_TOP, - ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); - } - - @Test - public void testUidStateChangedCallbackForegroundToTop() { - testUidStateChangedCallback( - ActivityManager.PROCESS_STATE_BOUND_TOP, - ActivityManager.PROCESS_STATE_TOP); - } - - @Test - public void testUidStateChangedCallbackForegroundServiceToCached() { - testUidStateChangedCallback( - ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, - ActivityManager.PROCESS_STATE_CACHED_ACTIVITY); - } - - @Test - public void testUidStateChangedCallbackForegroundServiceToBackground() { - testUidStateChangedCallback( - ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, - ActivityManager.PROCESS_STATE_RECEIVER); - } - - @Test - public void testUidStateChangedCallbackForegroundServiceToForeground() { - testUidStateChangedCallback( - ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, - ActivityManager.PROCESS_STATE_BOUND_TOP); - } - - @Test - public void testUidStateChangedCallbackForegroundServiceToTop() { - testUidStateChangedCallback( - ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, - ActivityManager.PROCESS_STATE_TOP); - } - - @Test - public void testUidStateChangedCallbackTopToCached() { - testUidStateChangedCallback( - ActivityManager.PROCESS_STATE_TOP, - ActivityManager.PROCESS_STATE_CACHED_ACTIVITY); - } - - @Test - public void testUidStateChangedCallbackTopToBackground() { - testUidStateChangedCallback( - ActivityManager.PROCESS_STATE_TOP, - ActivityManager.PROCESS_STATE_RECEIVER); - } - - @Test - public void testUidStateChangedCallbackTopToForeground() { - testUidStateChangedCallback( - ActivityManager.PROCESS_STATE_TOP, - ActivityManager.PROCESS_STATE_BOUND_TOP); - } - - @Test - public void testUidStateChangedCallbackTopToForegroundService() { - testUidStateChangedCallback( - ActivityManager.PROCESS_STATE_TOP, - ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); - } - - @Test - public void testUidStateChangedCallbackCachedToNonexistent() { - UidStateChangedCallback cb = addUidStateChangeCallback(); - - procStateBuilder(UID) - .cachedState() - .update(); - - procStateBuilder(UID) - .nonExistentState() - .update(); - - verify(cb, never()).onUidStateChanged(anyInt(), anyInt(), anyBoolean()); - } - - @Test - public void testUidStateChangedCallbackBackgroundToNonexistent() { - UidStateChangedCallback cb = addUidStateChangeCallback(); - - procStateBuilder(UID) - .backgroundState() - .update(); - - procStateBuilder(UID) - .nonExistentState() - .update(); - - verify(cb, atLeastOnce()).onUidStateChanged(eq(UID), eq(UID_STATE_CACHED), eq(false)); - } - - @Test - public void testUidStateChangedCallbackForegroundToNonexistent() { - UidStateChangedCallback cb = addUidStateChangeCallback(); - - procStateBuilder(UID) - .foregroundState() - .update(); - - procStateBuilder(UID) - .nonExistentState() - .update(); - - verify(cb, atLeastOnce()).onUidStateChanged(eq(UID), eq(UID_STATE_CACHED), eq(true)); - } - - @Test - public void testUidStateChangedCallbackForegroundServiceToNonexistent() { - UidStateChangedCallback cb = addUidStateChangeCallback(); - - procStateBuilder(UID) - .foregroundServiceState() - .update(); - - procStateBuilder(UID) - .nonExistentState() - .update(); - - verify(cb, atLeastOnce()).onUidStateChanged(eq(UID), eq(UID_STATE_CACHED), eq(true)); - } - - @Test - public void testUidStateChangedCallbackTopToNonexistent() { - UidStateChangedCallback cb = addUidStateChangeCallback(); - - procStateBuilder(UID) - .topState() - .update(); - - procStateBuilder(UID) - .nonExistentState() - .update(); - - verify(cb, atLeastOnce()).onUidStateChanged(eq(UID), eq(UID_STATE_CACHED), eq(true)); - } - - @Test public void testUidStateChangedBackgroundThenForegroundImmediately() { procStateBuilder(UID) .topState() @@ -882,32 +661,6 @@ public class AppOpsUidStateTrackerTest { assertEquals(UID_STATE_TOP, mIntf.getUidState(UID)); } - public void testUidStateChangedCallback(int initialState, int finalState) { - int initialUidState = processStateToUidState(initialState); - int finalUidState = processStateToUidState(finalState); - boolean foregroundChange = initialUidState <= UID_STATE_MAX_LAST_NON_RESTRICTED - != finalUidState <= UID_STATE_MAX_LAST_NON_RESTRICTED; - boolean finalUidStateIsBackgroundAndLessImportant = - finalUidState > UID_STATE_MAX_LAST_NON_RESTRICTED - && finalUidState > initialUidState; - - UidStateChangedCallback cb = addUidStateChangeCallback(); - - procStateBuilder(UID) - .setState(initialState) - .update(); - - procStateBuilder(UID) - .setState(finalState) - .update(); - - if (finalUidStateIsBackgroundAndLessImportant) { - mClock.advanceTime(mConstants.TOP_STATE_SETTLE_TIME + 1); - } - - verify(cb, atLeastOnce()) - .onUidStateChanged(eq(UID), eq(finalUidState), eq(foregroundChange)); - } private UidStateChangedCallback addUidStateChangeCallback() { UidStateChangedCallback cb = diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTransitionCallbackTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTransitionCallbackTest.java new file mode 100644 index 000000000000..60cdfee0834c --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTransitionCallbackTest.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.appop; + +import static android.app.AppOpsManager.UID_STATE_CACHED; +import static android.app.AppOpsManager.UID_STATE_MAX_LAST_NON_RESTRICTED; +import static android.app.AppOpsManager.UID_STATE_NONEXISTENT; +import static android.permission.flags.Flags.finishRunningOpsForKilledPackages; + +import static com.android.server.appop.AppOpsUidStateTracker.processStateToUidState; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager; +import android.app.ActivityManagerInternal; + +import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.internal.os.Clock; +import com.android.server.appop.AppOpsUidStateTracker.UidStateChangedCallback; +import com.android.server.appop.AppOpsUidStateTrackerImpl.DelayableExecutor; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.Mock; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.PriorityQueue; + +@RunWith(Parameterized.class) +public class AppOpsUidStateTrackerTransitionCallbackTest { + + private static final int UID = 10001; + + private static final int STATE_TOP = ActivityManager.PROCESS_STATE_TOP; + private static final int STATE_FOREGROUND_SERVICE = + ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE; + private static final int STATE_FOREGROUND = ActivityManager.PROCESS_STATE_BOUND_TOP; + private static final int STATE_BACKGROUND = ActivityManager.PROCESS_STATE_SERVICE; + private static final int STATE_CACHED = ActivityManager.PROCESS_STATE_CACHED_ACTIVITY; + private static final int STATE_NONEXISTENT = ActivityManager.PROCESS_STATE_NONEXISTENT; + + private static final int[] STATES = {STATE_TOP, STATE_FOREGROUND_SERVICE, STATE_FOREGROUND, + STATE_BACKGROUND, STATE_CACHED, STATE_NONEXISTENT}; + private static final String[] STATES_NAMES = {"TOP", "FOREGROUND_SERVICE", "FOREGROUND", + "BACKGROUND", "CACHED", "NONEXISTENT"}; + + @Mock + ActivityManagerInternal mAmi; + + @Mock + AppOpsService.Constants mConstants; + + AppOpsUidStateTrackerTestExecutor mExecutor = new AppOpsUidStateTrackerTestExecutor(); + + AppOpsUidStateTrackerTestClock mClock = new AppOpsUidStateTrackerTestClock(mExecutor); + + AppOpsUidStateTracker mIntf; + + StaticMockitoSession mSession; + + private final int mInitialState; + private final int mMiddleState; + private final int mFinalState; + + @Parameterized.Parameters(name = "{3} -> {4} -> {5}") + public static Collection<Object[]> getParameters() { + ArrayList<Object[]> parameters = new ArrayList<>(); + + for (int i = 0; i < STATES.length; i++) { + for (int j = 0; j < STATES.length; j++) { + for (int k = 0; k < STATES.length; k++) { + parameters + .add(new Object[]{STATES[i], STATES[j], STATES[k], STATES_NAMES[i], + STATES_NAMES[j], STATES_NAMES[k]}); + } + } + } + + return parameters; + } + + public AppOpsUidStateTrackerTransitionCallbackTest(int initialState, int middleState, + int finalState, String ignoredInitialStateName, String ignoredMiddleStateName, + String ignoredFinalStateName) { + mInitialState = initialState; + mMiddleState = middleState; + mFinalState = finalState; + } + + @Before + public void setUp() { + mSession = ExtendedMockito.mockitoSession() + .initMocks(this) + .startMocking(); + mConstants.TOP_STATE_SETTLE_TIME = 10 * 1000L; + mConstants.FG_SERVICE_STATE_SETTLE_TIME = 5 * 1000L; + mConstants.BG_STATE_SETTLE_TIME = 1 * 1000L; + mIntf = new AppOpsUidStateTrackerImpl(mAmi, mExecutor, mClock, mConstants, + Thread.currentThread()); + } + + @After + public void tearDown() { + mSession.finishMocking(); + } + + @Test + public void testUidStateChangedCallback() { + testUidStateTransition(mInitialState, mMiddleState, true); + testUidStateTransition(mMiddleState, mFinalState, false); + } + + private void testUidStateTransition(int initialState, int finalState, + boolean initializeState) { + int initialUidState = processStateToUidState(initialState); + int finalUidState = processStateToUidState(finalState); + + boolean expectUidProcessDeath = + finalUidState == UID_STATE_NONEXISTENT + && initialUidState != UID_STATE_NONEXISTENT + && finishRunningOpsForKilledPackages(); + + boolean foregroundChange = initialUidState <= UID_STATE_MAX_LAST_NON_RESTRICTED + != finalUidState <= UID_STATE_MAX_LAST_NON_RESTRICTED; + boolean finalUidStateIsBackgroundAndLessImportant = + finalUidState > UID_STATE_MAX_LAST_NON_RESTRICTED + && finalUidState > initialUidState; + + if (initializeState) { + mIntf.updateUidProcState(UID, initialState, ActivityManager.PROCESS_CAPABILITY_NONE); + } + + UidStateChangedCallback cb = addUidStateChangeCallback(); + + mIntf.updateUidProcState(UID, finalState, ActivityManager.PROCESS_CAPABILITY_NONE); + + if (finalUidStateIsBackgroundAndLessImportant) { + mClock.advanceTime(mConstants.TOP_STATE_SETTLE_TIME + 1); + } + + int expectedInitialUidState = initialUidState == UID_STATE_NONEXISTENT + ? UID_STATE_CACHED : initialUidState; + int expectedFinalUidState = finalUidState == UID_STATE_NONEXISTENT + ? UID_STATE_CACHED : finalUidState; + + if (expectedInitialUidState != expectedFinalUidState) { + verify(cb, times(1)) + .onUidStateChanged(eq(UID), eq(expectedFinalUidState), eq(foregroundChange)); + verify(cb, times(1)) + .onUidStateChanged(anyInt(), anyInt(), anyBoolean()); + } else { + verify(cb, never()).onUidStateChanged(anyInt(), anyInt(), anyBoolean()); + } + if (expectUidProcessDeath) { + verify(cb, times(1)).onUidProcessDeath(eq(UID)); + verify(cb, times(1)).onUidProcessDeath(anyInt()); + } else { + verify(cb, never()).onUidProcessDeath(anyInt()); + } + } + + private UidStateChangedCallback addUidStateChangeCallback() { + UidStateChangedCallback cb = + Mockito.mock(UidStateChangedCallback.class); + mIntf.addUidStateChangedCallback(r -> r.run(), cb); + return cb; + } + + private static class AppOpsUidStateTrackerTestClock extends Clock { + + private AppOpsUidStateTrackerTestExecutor mExecutor; + long mElapsedRealTime = 0x5f3759df; + + AppOpsUidStateTrackerTestClock(AppOpsUidStateTrackerTestExecutor executor) { + mExecutor = executor; + executor.setUptime(mElapsedRealTime); + } + + @Override + public long elapsedRealtime() { + return mElapsedRealTime; + } + + void advanceTime(long time) { + mElapsedRealTime += time; + mExecutor.setUptime(mElapsedRealTime); // assume uptime == elapsedtime + } + } + + private static class AppOpsUidStateTrackerTestExecutor implements DelayableExecutor { + + private static class QueueElement implements Comparable<QueueElement> { + + private long mExecutionTime; + private Runnable mRunnable; + + private QueueElement(long executionTime, Runnable runnable) { + mExecutionTime = executionTime; + mRunnable = runnable; + } + + @Override + public int compareTo(QueueElement queueElement) { + return Long.compare(mExecutionTime, queueElement.mExecutionTime); + } + } + + private long mUptime = 0; + + private PriorityQueue<QueueElement> mDelayedMessages = new PriorityQueue(); + + @Override + public void execute(Runnable runnable) { + runnable.run(); + } + + @Override + public void executeDelayed(Runnable runnable, long delay) { + if (delay <= 0) { + execute(runnable); + } + + mDelayedMessages.add(new QueueElement(mUptime + delay, runnable)); + } + + private void setUptime(long uptime) { + while (!mDelayedMessages.isEmpty() + && mDelayedMessages.peek().mExecutionTime <= uptime) { + mDelayedMessages.poll().mRunnable.run(); + } + + mUptime = uptime; + } + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java index 6b8ef88c556c..f8f08a51b375 100644 --- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java @@ -64,6 +64,7 @@ import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.pm.ServiceInfo; +import android.content.res.Resources; import android.graphics.Color; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManager.DisplayListener; @@ -94,6 +95,7 @@ import com.android.internal.R; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.server.LocalServices; +import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.DesktopModeHelper; import com.android.server.wm.WindowManagerInternal; @@ -169,6 +171,8 @@ public class WallpaperManagerServiceTests { private static WindowManagerInternal sWindowManagerInternal; + private static ActivityTaskManagerInternal sActivityTaskManagerInternal; + @BeforeClass public static void setUpClass() { sMockitoSession = mockitoSession() @@ -181,6 +185,9 @@ public class WallpaperManagerServiceTests { sWindowManagerInternal = mock(WindowManagerInternal.class); LocalServices.addService(WindowManagerInternal.class, sWindowManagerInternal); + sActivityTaskManagerInternal = mock(ActivityTaskManagerInternal.class); + LocalServices.addService(ActivityTaskManagerInternal.class, + sActivityTaskManagerInternal); sContext.addMockSystemService(Context.APP_OPS_SERVICE, mock(AppOpsManager.class)); @@ -232,6 +239,7 @@ public class WallpaperManagerServiceTests { sMockitoSession = null; } LocalServices.removeServiceForTest(WindowManagerInternal.class); + LocalServices.removeServiceForTest(ActivityTaskManagerInternal.class); sImageWallpaperComponentName = null; sDefaultWallpaperComponent = null; sFallbackWallpaperComponentName = null; @@ -1181,6 +1189,10 @@ public class WallpaperManagerServiceTests { @Test @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER) public void setWallpaperComponent_systemAndLockWallpapers_multiDisplays_shouldHaveExpectedConnections() { + Resources resources = sContext.getResources(); + spyOn(resources); + doReturn(true).when(resources).getBoolean( + R.bool.config_isLiveWallpaperSupportedInDesktopExperience); final int incompatibleDisplayId = 2; final int compatibleDisplayId = 3; setUpDisplays(Map.of( @@ -1220,6 +1232,71 @@ public class WallpaperManagerServiceTests { .isTrue(); } + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER) + public void isWallpaperCompatibleForDisplay_liveWallpaperSupported_desktopExperienceEnabled_shouldReturnTrue() { + Resources resources = sContext.getResources(); + spyOn(resources); + doReturn(true).when(resources).getBoolean( + R.bool.config_isLiveWallpaperSupportedInDesktopExperience); + + final int displayId = 2; + setUpDisplays(Map.of( + DEFAULT_DISPLAY, true, + displayId, true)); + final int testUserId = USER_SYSTEM; + mService.switchUser(testUserId, null); + mService.setWallpaperComponent(TEST_WALLPAPER_COMPONENT, sContext.getOpPackageName(), + FLAG_SYSTEM | FLAG_LOCK, testUserId); + + assertThat(mService.isWallpaperCompatibleForDisplay(displayId, + mService.mLastWallpaper.connection)).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER) + public void isWallpaperCompatibleForDisplay_liveWallpaperUnsupported_desktopExperienceEnabled_shouldReturnFalse() { + Resources resources = sContext.getResources(); + spyOn(resources); + doReturn(false).when(resources).getBoolean( + R.bool.config_isLiveWallpaperSupportedInDesktopExperience); + + final int displayId = 2; + setUpDisplays(Map.of( + DEFAULT_DISPLAY, true, + displayId, true)); + final int testUserId = USER_SYSTEM; + mService.switchUser(testUserId, null); + mService.setWallpaperComponent(TEST_WALLPAPER_COMPONENT, sContext.getOpPackageName(), + FLAG_SYSTEM | FLAG_LOCK, testUserId); + + assertThat(mService.isWallpaperCompatibleForDisplay(displayId, + mService.mLastWallpaper.connection)).isFalse(); + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER) + public void isWallpaperCompatibleForDisplay_liveWallpaperUnsupported_desktopExperienceDisabled_shouldReturnTrue() { + Resources resources = sContext.getResources(); + spyOn(resources); + doReturn(false).when(resources).getBoolean( + R.bool.config_isLiveWallpaperSupportedInDesktopExperience); + + final int displayId = 2; + setUpDisplays(Map.of( + DEFAULT_DISPLAY, true, + displayId, true)); + final int testUserId = USER_SYSTEM; + mService.switchUser(testUserId, null); + mService.setWallpaperComponent(TEST_WALLPAPER_COMPONENT, sContext.getOpPackageName(), + FLAG_SYSTEM | FLAG_LOCK, testUserId); + + // config_isLiveWallpaperSupportedInDesktopExperience is not used if the desktop experience + // flag for wallpaper is disabled. + assertThat(mService.isWallpaperCompatibleForDisplay(displayId, + mService.mLastWallpaper.connection)).isTrue(); + } + // Verify that after continue switch user from userId 0 to lastUserId, the wallpaper data for // non-current user must not bind to wallpaper service. private void verifyNoConnectionBeforeLastUser(int lastUserId) { diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java index 2b152315eec4..a1cf94b994ab 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java @@ -26,16 +26,10 @@ import static com.android.server.power.stats.BatteryStatsImpl.ExternalStatsSync. import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; import android.content.Context; -import android.hardware.power.stats.Channel; -import android.hardware.power.stats.EnergyConsumer; import android.hardware.power.stats.EnergyConsumerResult; import android.hardware.power.stats.EnergyConsumerType; -import android.hardware.power.stats.EnergyMeasurement; -import android.hardware.power.stats.PowerEntity; -import android.hardware.power.stats.StateResidencyResult; import android.os.Handler; import android.os.Looper; import android.os.connectivity.WifiActivityEnergyInfo; @@ -54,7 +48,6 @@ import org.junit.Before; import org.junit.Test; import java.util.Arrays; -import java.util.concurrent.CompletableFuture; /** * Tests for {@link BatteryExternalStatsWorker}. @@ -66,7 +59,7 @@ import java.util.concurrent.CompletableFuture; @android.platform.test.annotations.DisabledOnRavenwood public class BatteryExternalStatsWorkerTest { private BatteryExternalStatsWorker mBatteryExternalStatsWorker; - private TestPowerStatsInternal mPowerStatsInternal; + private MockPowerStatsInternal mPowerStatsInternal; private Handler mHandler; @Before @@ -80,7 +73,7 @@ public class BatteryExternalStatsWorkerTest { mHandler, null, null, null, new PowerProfile(context, true /* forTest */), buildScalingPolicies(), new PowerStatsUidResolver()); - mPowerStatsInternal = new TestPowerStatsInternal(); + mPowerStatsInternal = new MockPowerStatsInternal(); mBatteryExternalStatsWorker = new BatteryExternalStatsWorker(new TestInjector(context), batteryStats, mHandler); } @@ -260,102 +253,4 @@ public class BatteryExternalStatsWorkerTest { freqsByPolicy.put(4, new int[]{300000, 1000000, 2500000, 3000000}); return new CpuScalingPolicies(freqsByPolicy, freqsByPolicy); } - - private static class TestPowerStatsInternal extends PowerStatsInternal { - private final SparseArray<EnergyConsumer> mEnergyConsumers = new SparseArray(); - private final SparseArray<EnergyConsumerResult> mEnergyConsumerResults = new SparseArray(); - private final int mTimeSinceBoot = 0; - - @Override - public EnergyConsumer[] getEnergyConsumerInfo() { - final int size = mEnergyConsumers.size(); - final EnergyConsumer[] consumers = new EnergyConsumer[size]; - for (int i = 0; i < size; i++) { - consumers[i] = mEnergyConsumers.valueAt(i); - } - return consumers; - } - - @Override - public CompletableFuture<EnergyConsumerResult[]> getEnergyConsumedAsync( - int[] energyConsumerIds) { - final CompletableFuture<EnergyConsumerResult[]> future = new CompletableFuture(); - final EnergyConsumerResult[] results; - final int length = energyConsumerIds.length; - if (length == 0) { - final int size = mEnergyConsumerResults.size(); - results = new EnergyConsumerResult[size]; - for (int i = 0; i < size; i++) { - results[i] = mEnergyConsumerResults.valueAt(i); - } - } else { - results = new EnergyConsumerResult[length]; - for (int i = 0; i < length; i++) { - results[i] = mEnergyConsumerResults.get(energyConsumerIds[i]); - } - } - future.complete(results); - return future; - } - - @Override - public PowerEntity[] getPowerEntityInfo() { - return new PowerEntity[0]; - } - - @Override - public CompletableFuture<StateResidencyResult[]> getStateResidencyAsync( - int[] powerEntityIds) { - return new CompletableFuture<>(); - } - - @Override - public Channel[] getEnergyMeterInfo() { - return new Channel[0]; - } - - @Override - public CompletableFuture<EnergyMeasurement[]> readEnergyMeterAsync( - int[] channelIds) { - return new CompletableFuture<>(); - } - - /** - * Util method to add a new EnergyConsumer for testing - * - * @return the EnergyConsumer id of the new EnergyConsumer - */ - public int addEnergyConsumer(@EnergyConsumerType byte type, int ordinal, String name) { - final EnergyConsumer consumer = new EnergyConsumer(); - final int id = getNextAvailableId(); - consumer.id = id; - consumer.type = type; - consumer.ordinal = ordinal; - consumer.name = name; - mEnergyConsumers.put(id, consumer); - - final EnergyConsumerResult result = new EnergyConsumerResult(); - result.id = id; - result.timestampMs = mTimeSinceBoot; - result.energyUWs = 0; - mEnergyConsumerResults.put(id, result); - return id; - } - - public void incrementEnergyConsumption(int id, long energyUWs) { - EnergyConsumerResult result = mEnergyConsumerResults.get(id, null); - assertNotNull(result); - result.energyUWs += energyUWs; - } - - private int getNextAvailableId() { - final int size = mEnergyConsumers.size(); - // Just return the first index that does not match the key (aka the EnergyConsumer id) - for (int i = size - 1; i >= 0; i--) { - if (mEnergyConsumers.keyAt(i) == i) return i + 1; - } - // Otherwise return the lowest id - return 0; - } - } } diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryHistoryStepDetailsProviderTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryHistoryStepDetailsProviderTest.java new file mode 100644 index 000000000000..850cd8868e67 --- /dev/null +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryHistoryStepDetailsProviderTest.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.power.stats; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; + +import android.content.Context; +import android.os.BatteryManager; +import android.os.BatteryStats; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.Looper; +import android.os.Process; +import android.power.PowerStatsInternal; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.internal.os.BatteryStatsHistoryIterator; +import com.android.internal.os.MonotonicClock; +import com.android.internal.os.PowerProfile; +import com.android.server.LocalServices; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Random; + +@RunWith(AndroidJUnit4.class) +@android.platform.test.annotations.DisabledOnRavenwood(reason = + "PowerStatsInternal is not supported under Ravenwood") +@SuppressWarnings("GuardedBy") +public class BatteryHistoryStepDetailsProviderTest { + private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42; + + private final MockClock mMockClock = new MockClock(); + private MockBatteryStatsImpl mBatteryStats; + private final Random mRandom = new Random(); + private Handler mHandler; + + @Before + public void setup() { + mMockClock.currentTime = 3000; + mHandler = new Handler(Looper.getMainLooper()); + mBatteryStats = new MockBatteryStatsImpl(mMockClock, null, mHandler, + mock(PowerProfile.class)); + mBatteryStats.setRecordAllHistoryLocked(true); + mBatteryStats.forceRecordAllHistory(); + mBatteryStats.setNoAutoReset(true); + } + + @Test + public void update() { + MockPowerStatsInternal powerStatsService = new MockPowerStatsInternal(); + powerStatsService.addPowerEntity(42, "foo"); + powerStatsService.addPowerEntityState(42, 0, "off"); + powerStatsService.addPowerEntityState(42, 1, "on"); + LocalServices.addService(PowerStatsInternal.class, powerStatsService); + mBatteryStats.onSystemReady(mock(Context.class)); + + mockUpdateCpuStats(100, 1_000_010, 1_000_010); + powerStatsService.addStateResidencyResult(42, 0, 1000, 2000, 3000); + powerStatsService.addStateResidencyResult(42, 1, 4000, 5000, 6000); + + mBatteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, + 100, /* plugType */ 0, 90, 72, 3700, 3_600_000, 4_000_000, 0, + 1_000_000, 1_000_000, 1_000_000); + awaitCompletion(); + + mockUpdateCpuStats(200, 5_000_010, 5_000_010); + powerStatsService.reset(); + powerStatsService.addStateResidencyResult(42, 0, 1111, 2222, 3333); + powerStatsService.addStateResidencyResult(42, 1, 4444, 5555, 6666); + + // Battery level is unchanged, so we don't write battery level details in history + mBatteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, + 100, /* plugType */ 0, 80, 72, 3700, 2_400_000, 4_000_000, 0, + 5_500_000, 5_500_000, 5_000_000); + awaitCompletion(); + + // Not a battery state change event, so details are not written + mBatteryStats.noteAlarmStartLocked("wakeup", null, APP_UID, 6_000_000, 6_000_000); + + mockUpdateCpuStats(300, 6_000_010, 6_000_010); + powerStatsService.reset(); + + // Battery level drops, so we write the accumulated battery level details + mBatteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, + 100, /* plugType */ 0, 70, 72, 3700, 2_000_000, 4_000_000, 0, + 6_000_000, 6_000_000, 6_000_000); + awaitCompletion(); + + final BatteryStatsHistoryIterator iterator = + mBatteryStats.iterateBatteryStatsHistory(0, MonotonicClock.UNDEFINED); + + BatteryStats.HistoryItem item; + assertThat(item = iterator.next()).isNotNull(); + assertThat(item.cmd).isEqualTo((int) BatteryStats.HistoryItem.CMD_RESET); + assertThat(item.stepDetails).isNull(); + + assertThat(item = iterator.next()).isNotNull(); + assertThat(item.batteryLevel).isEqualTo(90); + assertThat(item.stepDetails.userTime).isEqualTo(100); + assertThat(item.stepDetails.statSubsystemPowerState).contains("subsystem_0 name=foo " + + "state_0 name=off time=1000 count=2000 last entry=3000 " + + "state_1 name=on time=4000 count=5000 last entry=6000"); + + assertThat(item = iterator.next()).isNotNull(); + assertThat(item.batteryLevel).isEqualTo(80); + assertThat(item.stepDetails.userTime).isEqualTo(200); + assertThat(item.stepDetails.statSubsystemPowerState).contains("subsystem_0 name=foo " + + "state_0 name=off time=1111 count=2222 last entry=3333 " + + "state_1 name=on time=4444 count=5555 last entry=6666"); + + assertThat(item = iterator.next()).isNotNull(); + assertThat(item.batteryLevel).isEqualTo(80); + assertThat(item.stepDetails).isNull(); + + assertThat(item = iterator.next()).isNotNull(); + assertThat(item.batteryLevel).isEqualTo(70); + assertThat(item.stepDetails.userTime).isEqualTo(300); + assertThat(item.stepDetails.statSubsystemPowerState).isNull(); + + assertThat(iterator.next()).isNull(); + } + + private void mockUpdateCpuStats(int totalUTimeMs, long elapsedRealtime, long uptime) { + BatteryStatsImpl.BatteryCallback callback = mock(BatteryStatsImpl.BatteryCallback.class); + doAnswer(inv -> { + mMockClock.realtime = elapsedRealtime; + mMockClock.uptime = uptime; + synchronized (mBatteryStats) { + mBatteryStats.addCpuStatsLocked(totalUTimeMs, 0, 0, 0, 0, 0, 0, 0); + mBatteryStats.finishAddingCpuStatsLocked(); + } + return null; + }).when(callback).batteryNeedsCpuUpdate(); + mBatteryStats.setCallback(callback); + } + + private void awaitCompletion() { + ConditionVariable done = new ConditionVariable(); + mHandler.post(done::open); + done.block(); + } +} diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java index 503e23347cf6..1b82ffdcf3e8 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java @@ -47,14 +47,12 @@ public class BatteryStatsHistoryIteratorTest { private final MockClock mMockClock = new MockClock(); private MockBatteryStatsImpl mBatteryStats; private final Random mRandom = new Random(); - private final MockExternalStatsSync mExternalStatsSync = new MockExternalStatsSync(); @Before public void setup() { final File historyDir = createTemporaryDirectory(getClass().getSimpleName()); mMockClock.currentTime = 3000; mBatteryStats = new MockBatteryStatsImpl(mMockClock, historyDir); - mBatteryStats.setDummyExternalStatsSync(mExternalStatsSync); mBatteryStats.setRecordAllHistoryLocked(true); mBatteryStats.forceRecordAllHistory(); mBatteryStats.setNoAutoReset(true); @@ -227,72 +225,53 @@ public class BatteryStatsHistoryIteratorTest { 100, /* plugType */ 0, 90, 72, 3700, 3_600_000, 4_000_000, 0, 1_000_000, 1_000_000, 1_000_000); - mExternalStatsSync.updateCpuStats(100, 1_100_000, 1_100_000); - // Device was suspended for 3_000 seconds, note the difference in elapsed time and uptime mBatteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100, /* plugType */ 0, 80, 72, 3700, 2_400_000, 4_000_000, 0, 5_000_000, 2_000_000, 5_000_000); - mExternalStatsSync.updateCpuStats(200, 5_100_000, 2_100_000); - // Battery level is unchanged, so we don't write battery level details in history - mBatteryStats.noteAlarmStartLocked("wakeup", null, APP_UID, 6_000_000, 3_000_000); + mBatteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, + 100, /* plugType */ 0, 80, 72, 3700, 2_400_000, 4_000_000, 0, + 5_500_000, 2_500_000, 5_000_000); - assertThat(mExternalStatsSync.isSyncScheduled()).isFalse(); + // Not a battery state change event, so details are not written + mBatteryStats.noteAlarmStartLocked("wakeup", null, APP_UID, 6_000_000, 3_000_000); // Battery level drops, so we write the accumulated battery level details mBatteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, - 100, /* plugType */ 0, 79, 72, 3700, 2_000_000, 4_000_000, 0, + 100, /* plugType */ 0, 70, 72, 3700, 2_000_000, 4_000_000, 0, 7_000_000, 4_000_000, 6_000_000); - mExternalStatsSync.updateCpuStats(300, 7_100_000, 4_100_000); - final BatteryStatsHistoryIterator iterator = mBatteryStats.iterateBatteryStatsHistory(0, MonotonicClock.UNDEFINED); BatteryStats.HistoryItem item; assertThat(item = iterator.next()).isNotNull(); assertThat(item.cmd).isEqualTo((int) BatteryStats.HistoryItem.CMD_RESET); - assertThat(item.stepDetails).isNull(); assertThat(item = iterator.next()).isNotNull(); assertThat(item.batteryLevel).isEqualTo(90); - assertThat(item.stepDetails).isNull(); - - assertThat(item = iterator.next()).isNotNull(); - assertThat(item.batteryLevel).isEqualTo(90); - assertThat(item.eventCode).isEqualTo(BatteryStats.HistoryItem.EVENT_COLLECT_EXTERNAL_STATS); + assertThat(item.states & BatteryStats.HistoryItem.STATE_CPU_RUNNING_FLAG).isNotEqualTo(0); assertThat(item = iterator.next()).isNotNull(); assertThat(item.batteryLevel).isEqualTo(90); assertThat(item.states & BatteryStats.HistoryItem.STATE_CPU_RUNNING_FLAG).isEqualTo(0); - assertThat(item.stepDetails.userTime).isEqualTo(100); assertThat(item = iterator.next()).isNotNull(); assertThat(item.batteryLevel).isEqualTo(80); assertThat(item.states & BatteryStats.HistoryItem.STATE_CPU_RUNNING_FLAG).isNotEqualTo(0); - assertThat(item.stepDetails.userTime).isEqualTo(0); - - assertThat(item = iterator.next()).isNotNull(); - assertThat(item.batteryLevel).isEqualTo(80); - assertThat(item.eventCode).isEqualTo(BatteryStats.HistoryItem.EVENT_COLLECT_EXTERNAL_STATS); assertThat(item = iterator.next()).isNotNull(); assertThat(item.batteryLevel).isEqualTo(80); assertThat(item.eventCode).isEqualTo(BatteryStats.HistoryItem.EVENT_ALARM_START); - assertThat(item.stepDetails).isNull(); - - assertThat(item = iterator.next()).isNotNull(); - assertThat(item.batteryLevel).isEqualTo(79); assertThat(item.states & BatteryStats.HistoryItem.STATE_CPU_RUNNING_FLAG).isNotEqualTo(0); - assertThat(item.stepDetails.userTime).isEqualTo(200); assertThat(item = iterator.next()).isNotNull(); - assertThat(item.batteryLevel).isEqualTo(79); - assertThat(item.eventCode).isEqualTo(BatteryStats.HistoryItem.EVENT_COLLECT_EXTERNAL_STATS); + assertThat(item.batteryLevel).isEqualTo(70); + assertThat(item.states & BatteryStats.HistoryItem.STATE_CPU_RUNNING_FLAG).isNotEqualTo(0); - assertThat(item = iterator.next()).isNull(); + assertThat(iterator.next()).isNull(); } @Test @@ -394,26 +373,4 @@ public class BatteryStatsHistoryIteratorTest { assertThat(item.time).isEqualTo(elapsedTimeMs); } - - private class MockExternalStatsSync extends MockBatteryStatsImpl.DummyExternalStatsSync { - private boolean mSyncScheduled; - - @Override - public void scheduleCpuSyncDueToWakelockChange(long delayMillis) { - mSyncScheduled = true; - } - - public boolean isSyncScheduled() { - return mSyncScheduled; - } - - public void updateCpuStats(int totalUTimeMs, long elapsedRealtime, long uptime) { - assertThat(mExternalStatsSync.mSyncScheduled).isTrue(); - mBatteryStats.recordHistoryEventLocked(elapsedRealtime, uptime, - BatteryStats.HistoryItem.EVENT_COLLECT_EXTERNAL_STATS, "wakelock-update", 0); - mBatteryStats.addCpuStatsLocked(totalUTimeMs, 0, 0, 0, 0, 0, 0, 0); - mBatteryStats.finishAddingCpuStatsLocked(); - mExternalStatsSync.mSyncScheduled = false; - } - } } diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java index 3ed4a52bed23..3f937331b52e 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java @@ -26,7 +26,6 @@ import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; import android.os.BatteryConsumer; import android.os.BatteryManager; @@ -58,7 +57,8 @@ import org.junit.runner.RunWith; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.io.File; import java.io.IOException; @@ -67,10 +67,14 @@ import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.nio.file.Files; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Date; import java.util.List; +import java.util.Locale; import java.util.TimeZone; +import java.util.concurrent.TimeUnit; /** * Test BatteryStatsHistory. @@ -85,6 +89,8 @@ public class BatteryStatsHistoryTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); private final Parcel mHistoryBuffer = Parcel.obtain(); private File mSystemDir; @@ -97,14 +103,11 @@ public class BatteryStatsHistoryTest { @Mock private BatteryStatsHistory.TraceDelegate mTracer; @Mock - private BatteryStatsHistory.HistoryStepDetailsCalculator mStepDetailsCalculator; - @Mock private BatteryStatsHistory.EventLogger mEventLogger; private List<String> mReadFiles = new ArrayList<>(); @Before public void setUp() throws IOException { - MockitoAnnotations.initMocks(this); mSystemDir = Files.createTempDirectory("BatteryStatsHistoryTest").toFile(); mHistoryDir = new File(mSystemDir, "battery-history"); String[] files = mHistoryDir.list(); @@ -138,15 +141,11 @@ public class BatteryStatsHistoryTest { mClock.currentTime = 1743645660000L; // 2025-04-03, 2:01:00 AM mHistory = new BatteryStatsHistory(mHistoryBuffer, MAX_HISTORY_BUFFER_SIZE, mDirectory, - mStepDetailsCalculator, mClock, mMonotonicClock, mTracer, + mClock, mMonotonicClock, mTracer, mEventLogger); mHistory.forceRecordAllHistory(); mHistory.startRecordingHistory(mClock.realtime, mClock.uptime, false); - - when(mStepDetailsCalculator.getHistoryStepDetails()) - .thenReturn(new BatteryStats.HistoryStepDetails()); - - mHistoryPrinter = new BatteryStats.HistoryPrinter(TimeZone.getTimeZone("GMT")); + mHistoryPrinter = new BatteryStats.HistoryPrinter(TimeZone.getTimeZone("GMT"), 0); } @Test @@ -288,7 +287,7 @@ public class BatteryStatsHistoryTest { // create a new BatteryStatsHistory object, it will pick up existing history files. BatteryStatsHistory history2 = new BatteryStatsHistory(mHistoryBuffer, 1024, mDirectory, - null, mClock, mMonotonicClock, mTracer, mEventLogger); + mClock, mMonotonicClock, mTracer, mEventLogger); // verify constructor can pick up all files from file system. verifyFileNames(history2, fileList); verifyActiveFile(history2, "33000.bh"); @@ -595,7 +594,7 @@ public class BatteryStatsHistoryTest { // Keep the preserved part of history short - we only need to capture the very tail of // history. mHistory = new BatteryStatsHistory(mHistoryBuffer, 6000, mDirectory, - mStepDetailsCalculator, mClock, mMonotonicClock, mTracer, mEventLogger); + mClock, mMonotonicClock, mTracer, mEventLogger); mHistory.forceRecordAllHistory(); @@ -870,6 +869,39 @@ public class BatteryStatsHistoryTest { return events; } + + @Test + public void historyLogTimeFormatter() { + historyLogTimeFormatting("GMT"); + historyLogTimeFormatting("PST"); + historyLogTimeFormatting("NST"); // UTC−03:30 + historyLogTimeFormatting("NPT"); // UTC+05:45 + } + + private void historyLogTimeFormatting(String timeZoneId) { + TimeZone timeZone = TimeZone.getTimeZone(timeZoneId); + BatteryStats.HistoryPrinter printer = new BatteryStats.HistoryPrinter(timeZone, 0); + SimpleDateFormat simpleDateFormat = + new SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US); + simpleDateFormat.getCalendar().setTimeZone(timeZone); + Date date = new Date(); + + HistoryItem item = new HistoryItem(); + long base = 1738746000000L; + for (long offset = 1; offset < TimeUnit.DAYS.toMillis(365 * 100); offset *= 7) { + item.currentTime = base + offset; + date.setTime(item.currentTime); + String expected = simpleDateFormat.format(date); + StringWriter sw = new StringWriter(); + PrintWriter writer = new PrintWriter(sw); + printer.printNextItem(writer, item, 0, false, false); + writer.flush(); + String actual = sw.toString().trim().substring(0, "MM-dd HH:mm:ss.SSS".length()); + + assertThat(actual).isEqualTo(expected); + } + } + private static void awaitCompletion() { ConditionVariable done = new ConditionVariable(); BackgroundThread.getHandler().post(done::open); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java index 8a1d37b55255..90b3e132f9d3 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java @@ -575,7 +575,7 @@ public class BatteryUsageStatsProviderTest { accumulateBatteryUsageStats(batteryStats, 10000000, 0); // Accumulate every 200 bytes of battery history accumulateBatteryUsageStats(batteryStats, 200, 1); - accumulateBatteryUsageStats(batteryStats, 50, 5); + accumulateBatteryUsageStats(batteryStats, 50, 4); // Accumulate on every invocation of accumulateBatteryUsageStats accumulateBatteryUsageStats(batteryStats, 0, 7); } diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java index 53a2522d299e..f29708496940 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java @@ -340,7 +340,7 @@ public class BatteryUsageStatsRule implements TestRule { } private void before() { - BatteryUsageStats.DEBUG_INSTANCE_COUNT = true; + BatteryUsageStats.enableInstanceLeakDetection(); HandlerThread bgThread = new HandlerThread("bg thread"); bgThread.setUncaughtExceptionHandler((thread, throwable)-> { mThrowable = throwable; diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java index c7a19ce7b233..8c3bd17c5acd 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java @@ -329,10 +329,6 @@ public class MockBatteryStatsImpl extends BatteryStatsImpl { } @Override - public void scheduleSyncDueToBatteryLevelChange(long delayMillis) { - } - - @Override public void scheduleSyncDueToProcessStateChange(int flags, long delayMillis) { } } diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MockPowerStatsInternal.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MockPowerStatsInternal.java new file mode 100644 index 000000000000..dc5da8c2841f --- /dev/null +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MockPowerStatsInternal.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.power.stats; + +import static org.junit.Assert.assertNotNull; + +import android.hardware.power.stats.Channel; +import android.hardware.power.stats.EnergyConsumer; +import android.hardware.power.stats.EnergyConsumerResult; +import android.hardware.power.stats.EnergyConsumerType; +import android.hardware.power.stats.EnergyMeasurement; +import android.hardware.power.stats.PowerEntity; +import android.hardware.power.stats.State; +import android.hardware.power.stats.StateResidency; +import android.hardware.power.stats.StateResidencyResult; +import android.power.PowerStatsInternal; +import android.util.SparseArray; + +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; + +class MockPowerStatsInternal extends PowerStatsInternal { + private final SparseArray<PowerEntity> mPowerEntities = new SparseArray<>(); + private final SparseArray<StateResidencyResult> mStateResidencyResults = new SparseArray<>(); + private final SparseArray<EnergyConsumer> mEnergyConsumers = new SparseArray<>(); + private final SparseArray<EnergyConsumerResult> mEnergyConsumerResults = new SparseArray<>(); + private final int mTimeSinceBoot = 0; + + @Override + public EnergyConsumer[] getEnergyConsumerInfo() { + final int size = mEnergyConsumers.size(); + final EnergyConsumer[] consumers = new EnergyConsumer[size]; + for (int i = 0; i < size; i++) { + consumers[i] = mEnergyConsumers.valueAt(i); + } + return consumers; + } + + @Override + public CompletableFuture<EnergyConsumerResult[]> getEnergyConsumedAsync( + int[] energyConsumerIds) { + final CompletableFuture<EnergyConsumerResult[]> future = new CompletableFuture(); + final EnergyConsumerResult[] results; + final int length = energyConsumerIds.length; + if (length == 0) { + final int size = mEnergyConsumerResults.size(); + results = new EnergyConsumerResult[size]; + for (int i = 0; i < size; i++) { + results[i] = mEnergyConsumerResults.valueAt(i); + } + } else { + results = new EnergyConsumerResult[length]; + for (int i = 0; i < length; i++) { + results[i] = mEnergyConsumerResults.get(energyConsumerIds[i]); + } + } + future.complete(results); + return future; + } + + @Override + public PowerEntity[] getPowerEntityInfo() { + final int size = mPowerEntities.size(); + final PowerEntity[] entities = new PowerEntity[size]; + for (int i = 0; i < size; i++) { + entities[i] = mPowerEntities.valueAt(i); + } + return entities; + } + + @Override + public CompletableFuture<StateResidencyResult[]> getStateResidencyAsync( + int[] powerEntityIds) { + final CompletableFuture<StateResidencyResult[]> future = new CompletableFuture<>(); + final StateResidencyResult[] results; + final int length = powerEntityIds.length; + if (length == 0) { + final int size = mStateResidencyResults.size(); + results = new StateResidencyResult[size]; + for (int i = 0; i < size; i++) { + results[i] = mStateResidencyResults.valueAt(i); + } + } else { + results = new StateResidencyResult[length]; + for (int i = 0; i < length; i++) { + results[i] = mStateResidencyResults.get(powerEntityIds[i]); + } + } + future.complete(results); + return future; + } + + @Override + public Channel[] getEnergyMeterInfo() { + return new Channel[0]; + } + + @Override + public CompletableFuture<EnergyMeasurement[]> readEnergyMeterAsync( + int[] channelIds) { + return new CompletableFuture<>(); + } + + public void reset() { + mStateResidencyResults.clear(); + mEnergyConsumerResults.clear(); + } + + public void addPowerEntity(int id, String name) { + PowerEntity powerEntity = new PowerEntity(); + powerEntity.id = id; + powerEntity.name = name; + powerEntity.states = new State[0]; + mPowerEntities.put(id, powerEntity); + } + + public void addPowerEntityState(int powerEntityId, int stateId, String name) { + State state = new State(); + state.id = stateId; + state.name = name; + + PowerEntity powerEntity = mPowerEntities.get(powerEntityId); + powerEntity.states = Arrays.copyOf(powerEntity.states, powerEntity.states.length + 1); + powerEntity.states[powerEntity.states.length - 1] = state; + } + + public void addStateResidencyResult(int entityId, int stateId, long totalTimeInStateMs, + long totalStateEntryCount, long lastEntryTimestampMs) { + StateResidencyResult result = mStateResidencyResults.get(entityId); + if (result == null) { + result = new StateResidencyResult(); + result.id = entityId; + result.stateResidencyData = new StateResidency[0]; + mStateResidencyResults.put(entityId, result); + } + + StateResidency residency = new StateResidency(); + residency.id = stateId; + residency.totalTimeInStateMs = totalTimeInStateMs; + residency.totalStateEntryCount = totalStateEntryCount; + residency.lastEntryTimestampMs = lastEntryTimestampMs; + + result.stateResidencyData = Arrays.copyOf(result.stateResidencyData, + result.stateResidencyData.length + 1); + result.stateResidencyData[result.stateResidencyData.length - 1] = residency; + } + + /** + * Util method to add a new EnergyConsumer for testing + * + * @return the EnergyConsumer id of the new EnergyConsumer + */ + public int addEnergyConsumer(@EnergyConsumerType byte type, int ordinal, String name) { + final EnergyConsumer consumer = new EnergyConsumer(); + final int id = getNextAvailableId(); + consumer.id = id; + consumer.type = type; + consumer.ordinal = ordinal; + consumer.name = name; + mEnergyConsumers.put(id, consumer); + + final EnergyConsumerResult result = new EnergyConsumerResult(); + result.id = id; + result.timestampMs = mTimeSinceBoot; + result.energyUWs = 0; + mEnergyConsumerResults.put(id, result); + return id; + } + + public void incrementEnergyConsumption(int id, long energyUWs) { + EnergyConsumerResult result = mEnergyConsumerResults.get(id, null); + assertNotNull(result); + result.energyUWs += energyUWs; + } + + private int getNextAvailableId() { + final int size = mEnergyConsumers.size(); + // Just return the first index that does not match the key (aka the EnergyConsumer id) + for (int i = size - 1; i >= 0; i--) { + if (mEnergyConsumers.keyAt(i) == i) return i + 1; + } + // Otherwise return the lowest id + return 0; + } +} diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsAggregatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsAggregatorTest.java index 73d491c93bb5..7aa59bd03898 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsAggregatorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsAggregatorTest.java @@ -59,9 +59,8 @@ public class PowerStatsAggregatorTest { @Before public void setup() throws ParseException { - mHistory = new BatteryStatsHistory(null, 1024, null, - mock(BatteryStatsHistory.HistoryStepDetailsCalculator.class), mClock, - mMonotonicClock, mock(BatteryStatsHistory.TraceDelegate.class), null); + mHistory = new BatteryStatsHistory(null, 1024, null, mClock, mMonotonicClock, + mock(BatteryStatsHistory.TraceDelegate.class), null); AggregatedPowerStatsConfig config = new AggregatedPowerStatsConfig(); config.trackPowerComponent(TEST_POWER_COMPONENT) diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsExporterTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsExporterTest.java index a5a29f5883b1..b33cb7e6739f 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsExporterTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsExporterTest.java @@ -114,8 +114,7 @@ public class PowerStatsExporterTest { mPowerStatsStore = new PowerStatsStore(storeDirectory, new TestHandler()); mDirectory = new BatteryHistoryDirectory(storeDirectory, 0); - mHistory = new BatteryStatsHistory(Parcel.obtain(), 10000, mDirectory, - mock(BatteryStatsHistory.HistoryStepDetailsCalculator.class), mClock, + mHistory = new BatteryStatsHistory(Parcel.obtain(), 10000, mDirectory, mClock, mMonotonicClock, null, null); mPowerStatsAggregator = new PowerStatsAggregator(config); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WakelockPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WakelockPowerStatsProcessorTest.java index 5ac7216194a4..b90019f9537a 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WakelockPowerStatsProcessorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WakelockPowerStatsProcessorTest.java @@ -29,8 +29,6 @@ import static com.android.server.power.stats.processor.AggregatedPowerStatsConfi import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.mock; - import android.annotation.SuppressLint; import android.os.BatteryConsumer; import android.os.PersistableBundle; @@ -89,7 +87,6 @@ public class WakelockPowerStatsProcessorTest { long[] uidStats = new long[descriptor.uidStatsArrayLength]; BatteryStatsHistory history = new BatteryStatsHistory(null, 10000, null, - mock(BatteryStatsHistory.HistoryStepDetailsCalculator.class), mStatsRule.getMockClock(), new MonotonicClock(START_TIME, mStatsRule.getMockClock()), null, null); history.forceRecordAllHistory(); diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index 64e6d323bdfd..a99e1b1f28e1 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -63,6 +63,7 @@ android_test { "androidx.test.runner", "androidx.test.rules", "androidx.test.ext.junit", + "CtsAccessibilityCommon", "cts-wm-util", "platform-compat-test-rules", "platform-parametric-runner-lib", @@ -347,6 +348,17 @@ test_module_config { include_filters: ["com.android.server.om."], } +test_module_config { + name: "FrameworksServicesTests_theme", + base: "FrameworksServicesTests", + test_suites: [ + "device-tests", + "automotive-tests", + ], + + include_filters: ["com.android.server.theming."], +} + // Used by contexthub TEST_MAPPING test_module_config { name: "FrameworksServicesTests_contexthub_presubmit", diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml index 4531b3948495..ef478e8ff1e1 100644 --- a/services/tests/servicestests/AndroidManifest.xml +++ b/services/tests/servicestests/AndroidManifest.xml @@ -129,6 +129,21 @@ <application android:testOnly="true" android:debuggable="true"> <uses-library android:name="android.test.runner"/> + <service + android:name="com.android.server.accessibility.integration.FullScreenMagnificationMouseFollowingTest$TestMagnificationAccessibilityService" + android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" + android:exported="true"> + <intent-filter> + <action android:name="android.accessibilityservice.AccessibilityService" /> + <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC" /> + </intent-filter> + + <meta-data + android:name="android.accessibilityservice" + android:resource="@xml/test_magnification_a11y_service" /> + </service> + <activity android:name="com.android.server.accessibility.integration.FullScreenMagnificationMouseFollowingTest$TestActivity" /> + <service android:name="com.android.server.accounts.TestAccountType1AuthenticatorService" android:exported="false"> <intent-filter> diff --git a/services/tests/servicestests/res/xml/test_magnification_a11y_service.xml b/services/tests/servicestests/res/xml/test_magnification_a11y_service.xml new file mode 100644 index 000000000000..d28cdca6a26a --- /dev/null +++ b/services/tests/servicestests/res/xml/test_magnification_a11y_service.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" + android:accessibilityEventTypes="typeAllMask" + android:accessibilityFeedbackType="feedbackGeneric" + android:canRetrieveWindowContent="true" + android:canRequestTouchExplorationMode="true" + android:canRequestEnhancedWebAccessibility="true" + android:canRequestFilterKeyEvents="true" + android:canControlMagnification="true" + android:canPerformGestures="true"/>
\ No newline at end of file diff --git a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java index cc0d5e4710d2..73e5f8232faf 100644 --- a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java @@ -1787,46 +1787,6 @@ public class GestureLauncherServiceTest { } /** - * If processPowerKeyDown is called instead of interceptPowerKeyDown (meaning the double tap - * gesture isn't performed), the emergency gesture is still launched. - */ - @Test - public void testProcessPowerKeyDown_fiveInboundPresses_emergencyGestureLaunches() { - enableCameraGesture(); - enableEmergencyGesture(); - - // First event - long eventTime = INITIAL_EVENT_TIME_MILLIS; - sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); - - //Second event; call processPowerKeyDown without calling interceptPowerKeyDown - final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; - eventTime += interval; - KeyEvent keyEvent = - new KeyEvent( - IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); - mGestureLauncherService.processPowerKeyDown(keyEvent); - - verify(mMetricsLogger, never()) - .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); - verify(mUiEventLogger, never()).log(any()); - - // Presses 3 and 4 should not trigger any gesture - for (int i = 0; i < 2; i++) { - eventTime += interval; - sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, false); - } - - // Fifth button press should still trigger the emergency flow - eventTime += interval; - sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, true); - - verify(mUiEventLogger, times(1)) - .log(GestureLauncherService.GestureLauncherEvent.GESTURE_EMERGENCY_TAP_POWER); - verify(mStatusBarManagerInternal).onEmergencyActionLaunchGestureDetected(); - } - - /** * Helper method to trigger emergency gesture by pressing button for 5 times. * * @return last event time. diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java index df77866b5e7f..900d5ad58719 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java @@ -88,6 +88,23 @@ public class AutoclickControllerTest { } } + public static class ScrollEventCaptor extends BaseEventStreamTransformation { + public MotionEvent scrollEvent; + public int eventCount = 0; + + @Override + public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + if (event.getAction() == MotionEvent.ACTION_SCROLL) { + if (scrollEvent != null) { + scrollEvent.recycle(); + } + scrollEvent = MotionEvent.obtain(event); + eventCount++; + } + super.onMotionEvent(event, rawEvent, policyFlags); + } + } + @Before public void setUp() { mTestableLooper = TestableLooper.get(this); @@ -314,15 +331,7 @@ public class AutoclickControllerTest { injectFakeMouseActionHoverMoveEvent(); // Send hover enter event. - MotionEvent hoverEnter = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 100, - /* action= */ MotionEvent.ACTION_HOVER_ENTER, - /* x= */ 30f, - /* y= */ 0f, - /* metaState= */ 0); - hoverEnter.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(hoverEnter, hoverEnter, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_ENTER); // Verify there is no pending click. assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse(); @@ -334,15 +343,7 @@ public class AutoclickControllerTest { injectFakeMouseActionHoverMoveEvent(); // Send hover move event. - MotionEvent hoverMove = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 100, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 30f, - /* y= */ 0f, - /* metaState= */ 0); - hoverMove.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE); // Verify there is a pending click. assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue(); @@ -351,39 +352,15 @@ public class AutoclickControllerTest { @Test public void smallJitteryMovement_doesNotTriggerClick() { // Initial hover move to set an anchor point. - MotionEvent initialHoverMove = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 100, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 30f, - /* y= */ 40f, - /* metaState= */ 0); - initialHoverMove.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(initialHoverMove, initialHoverMove, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 40f, MotionEvent.ACTION_HOVER_MOVE); // Get the initial scheduled click time. long initialScheduledTime = mController.mClickScheduler.getScheduledClickTimeForTesting(); // Simulate small, jittery movements (all within the default slop). - MotionEvent jitteryMove1 = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 150, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 31f, // Small change in x - /* y= */ 41f, // Small change in y - /* metaState= */ 0); - jitteryMove1.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(jitteryMove1, jitteryMove1, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 31f, /* y= */ 41f, MotionEvent.ACTION_HOVER_MOVE); - MotionEvent jitteryMove2 = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 200, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 30.5f, // Small change in x - /* y= */ 39.8f, // Small change in y - /* metaState= */ 0); - jitteryMove2.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(jitteryMove2, jitteryMove2, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 30.5f, /* y= */ 39.8f, MotionEvent.ACTION_HOVER_MOVE); // Verify that the scheduled click time has NOT changed. assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()) @@ -393,29 +370,13 @@ public class AutoclickControllerTest { @Test public void singleSignificantMovement_triggersClick() { // Initial hover move to set an anchor point. - MotionEvent initialHoverMove = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 100, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 30f, - /* y= */ 40f, - /* metaState= */ 0); - initialHoverMove.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(initialHoverMove, initialHoverMove, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 40f, MotionEvent.ACTION_HOVER_MOVE); // Get the initial scheduled click time. long initialScheduledTime = mController.mClickScheduler.getScheduledClickTimeForTesting(); - // Simulate a single, significant movement (greater than the default slop). - MotionEvent significantMove = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 150, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 60f, // Significant change in x (30f difference) - /* y= */ 70f, // Significant change in y (30f difference) - /* metaState= */ 0); - significantMove.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(significantMove, significantMove, /* policyFlags= */ 0); + // Significant change in x (30f difference) and y (30f difference) + injectFakeMouseMoveEvent(/* x= */ 60f, /* y= */ 70f, MotionEvent.ACTION_HOVER_MOVE); // Verify that the scheduled click time has changed (click was rescheduled). assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()) @@ -442,15 +403,7 @@ public class AutoclickControllerTest { // Move the mouse down, less than customSize radius so a click is not triggered. float moveDownY = customSize - 25; - MotionEvent hoverMove = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 150, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 0f, - /* y= */ moveDownY, - /* metaState= */ 0); - hoverMove.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 0, /* y= */ moveDownY, MotionEvent.ACTION_HOVER_MOVE); assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse(); } @@ -474,15 +427,7 @@ public class AutoclickControllerTest { // Move the mouse right, greater than customSize radius so a click is triggered. float moveRightX = customSize + 100; - MotionEvent hoverMove = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 200, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ moveRightX, - /* y= */ 0, - /* metaState= */ 0); - hoverMove.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ moveRightX, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE); assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue(); } @@ -505,15 +450,7 @@ public class AutoclickControllerTest { // Move the mouse down less than customSize radius but ignore custom movement is not enabled // so a click is triggered. float moveDownY = customSize - 100; - MotionEvent hoverMove = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 150, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 0f, - /* y= */ moveDownY, - /* metaState= */ 0); - hoverMove.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 0, /* y= */ moveDownY, MotionEvent.ACTION_HOVER_MOVE); assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue(); } @@ -537,15 +474,7 @@ public class AutoclickControllerTest { // After enabling ignore custom movement, move the mouse right, less than customSize radius // so a click won't be triggered. float moveRightX = customSize - 100; - MotionEvent hoverMove = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 200, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ moveRightX, - /* y= */ 0, - /* metaState= */ 0); - hoverMove.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ moveRightX, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE); assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse(); } @@ -583,15 +512,7 @@ public class AutoclickControllerTest { mController.mAutoclickTypePanel = mockAutoclickTypePanel; // Send hover move event. - MotionEvent hoverMove = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 100, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 30f, - /* y= */ 0f, - /* metaState= */ 0); - hoverMove.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE); // Verify there is not a pending click. assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse(); @@ -607,7 +528,7 @@ public class AutoclickControllerTest { assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()).isEqualTo(-1); // Send move again to trigger click and verify there is now a pending click. - mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE); assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue(); assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()).isNotEqualTo(-1); } @@ -624,15 +545,7 @@ public class AutoclickControllerTest { mController.mAutoclickTypePanel = mockAutoclickTypePanel; // Send hover move event. - MotionEvent hoverMove = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 100, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 30f, - /* y= */ 0f, - /* metaState= */ 0); - hoverMove.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE); // Verify click is not triggered. assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse(); @@ -651,15 +564,7 @@ public class AutoclickControllerTest { mController.mAutoclickTypePanel = mockAutoclickTypePanel; // Send hover move event. - MotionEvent hoverMove = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 100, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 30f, - /* y= */ 0f, - /* metaState= */ 0); - hoverMove.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE); // Verify click is triggered. assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue(); @@ -678,15 +583,7 @@ public class AutoclickControllerTest { mController.mAutoclickTypePanel = mockAutoclickTypePanel; // Send hover move event. - MotionEvent hoverMove = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 100, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 30f, - /* y= */ 0f, - /* metaState= */ 0); - hoverMove.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE); // Verify click is triggered. assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue(); @@ -764,15 +661,7 @@ public class AutoclickControllerTest { mController.mClickScheduler.updateDelay(0); // Send hover move event. - MotionEvent hoverMove = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 100, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 30f, - /* y= */ 0f, - /* metaState= */ 0); - hoverMove.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE); mTestableLooper.processAllMessages(); // Verify left click sent. @@ -797,15 +686,7 @@ public class AutoclickControllerTest { mController.mAutoclickTypePanel = mockAutoclickTypePanel; // Send hover move event. - MotionEvent hoverMove = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 100, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 30f, - /* y= */ 0f, - /* metaState= */ 0); - hoverMove.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE); mTestableLooper.processAllMessages(); // Verify right click sent. @@ -833,15 +714,7 @@ public class AutoclickControllerTest { mController.mAutoclickScrollPanel = mockScrollPanel; // First hover move event. - MotionEvent hoverMove1 = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 100, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 30f, - /* y= */ 0f, - /* metaState= */ 0); - hoverMove1.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(hoverMove1, hoverMove1, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE); mTestableLooper.processAllMessages(); // Verify scroll panel is shown once. @@ -849,15 +722,7 @@ public class AutoclickControllerTest { assertThat(motionEventCaptor.downEvent).isNull(); // Second significant hover move event to trigger another autoclick. - MotionEvent hoverMove2 = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 200, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 100f, - /* y= */ 100f, - /* metaState= */ 0); - hoverMove2.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(hoverMove2, hoverMove2, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 100f, /* y= */ 100f, MotionEvent.ACTION_HOVER_MOVE); mTestableLooper.processAllMessages(); // Verify scroll panel is still only shown once (not called again). @@ -901,15 +766,7 @@ public class AutoclickControllerTest { mController.mAutoclickTypePanel = mockAutoclickTypePanel; // Send hover move event. - MotionEvent hoverMove = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 100, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 30f, - /* y= */ 0f, - /* metaState= */ 0); - hoverMove.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 100f, MotionEvent.ACTION_HOVER_MOVE); mTestableLooper.processAllMessages(); // Verify left click is sent due to the mouse hovering the panel. @@ -918,27 +775,108 @@ public class AutoclickControllerTest { MotionEvent.BUTTON_PRIMARY); } - private void injectFakeMouseActionHoverMoveEvent() { - MotionEvent event = getFakeMotionHoverMoveEvent(); - event.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(event, event, /* policyFlags= */ 0); - } + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void sendClick_updateLastCursorAndScrollAtThatLocation() { + // Set up event capturer to track scroll events. + ScrollEventCaptor scrollCaptor = new ScrollEventCaptor(); + mController.setNext(scrollCaptor); - private void injectFakeNonMouseActionHoverMoveEvent() { - MotionEvent event = getFakeMotionHoverMoveEvent(); - event.setSource(InputDevice.SOURCE_KEYBOARD); - mController.onMotionEvent(event, event, /* policyFlags= */ 0); + // Initialize controller with mouse event. + injectFakeMouseActionHoverMoveEvent(); + + // Mock the scroll panel. + AutoclickScrollPanel mockScrollPanel = mock(AutoclickScrollPanel.class); + mController.mAutoclickScrollPanel = mockScrollPanel; + + // Set click type to scroll. + mController.clickPanelController.handleAutoclickTypeChange( + AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL); + + // Set cursor position. + float expectedX = 75f; + float expectedY = 125f; + mController.mLastCursorX = expectedX; + mController.mLastCursorY = expectedY; + + // Trigger scroll action in up direction. + mController.mScrollPanelController.onHoverButtonChange( + AutoclickScrollPanel.DIRECTION_UP, true); + + // Verify scroll event happens at last cursor location. + assertThat(scrollCaptor.scrollEvent).isNotNull(); + assertThat(scrollCaptor.scrollEvent.getX()).isEqualTo(expectedX); + assertThat(scrollCaptor.scrollEvent.getY()).isEqualTo(expectedY); } - private void injectFakeKeyEvent(int keyCode, int modifiers) { - KeyEvent keyEvent = new KeyEvent( - /* downTime= */ 0, - /* eventTime= */ 0, - /* action= */ KeyEvent.ACTION_DOWN, - /* code= */ keyCode, - /* repeat= */ 0, - /* metaState= */ modifiers); - mController.onKeyEvent(keyEvent, /* policyFlags= */ 0); + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void handleScroll_generatesCorrectScrollEvents() { + ScrollEventCaptor scrollCaptor = new ScrollEventCaptor(); + mController.setNext(scrollCaptor); + + // Initialize controller. + injectFakeMouseActionHoverMoveEvent(); + + // Set cursor position. + final float expectedX = 100f; + final float expectedY = 200f; + mController.mLastCursorX = expectedX; + mController.mLastCursorY = expectedY; + + // Test UP direction. + mController.mScrollPanelController.onHoverButtonChange( + AutoclickScrollPanel.DIRECTION_UP, true); + + // Verify basic event properties. + assertThat(scrollCaptor.eventCount).isEqualTo(1); + assertThat(scrollCaptor.scrollEvent).isNotNull(); + assertThat(scrollCaptor.scrollEvent.getAction()).isEqualTo(MotionEvent.ACTION_SCROLL); + assertThat(scrollCaptor.scrollEvent.getX()).isEqualTo(expectedX); + assertThat(scrollCaptor.scrollEvent.getY()).isEqualTo(expectedY); + + // Verify UP direction uses correct axis values. + float vScrollUp = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_VSCROLL); + float hScrollUp = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_HSCROLL); + assertThat(vScrollUp).isGreaterThan(0); + assertThat(hScrollUp).isEqualTo(0); + + // Test DOWN direction. + mController.mScrollPanelController.onHoverButtonChange( + AutoclickScrollPanel.DIRECTION_DOWN, true); + + // Verify DOWN direction uses correct axis values. + assertThat(scrollCaptor.eventCount).isEqualTo(2); + float vScrollDown = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_VSCROLL); + float hScrollDown = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_HSCROLL); + assertThat(vScrollDown).isLessThan(0); + assertThat(hScrollDown).isEqualTo(0); + + // Test LEFT direction. + mController.mScrollPanelController.onHoverButtonChange( + AutoclickScrollPanel.DIRECTION_LEFT, true); + + // Verify LEFT direction uses correct axis values. + assertThat(scrollCaptor.eventCount).isEqualTo(3); + float vScrollLeft = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_VSCROLL); + float hScrollLeft = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_HSCROLL); + assertThat(hScrollLeft).isGreaterThan(0); + assertThat(vScrollLeft).isEqualTo(0); + + // Test RIGHT direction. + mController.mScrollPanelController.onHoverButtonChange( + AutoclickScrollPanel.DIRECTION_RIGHT, true); + + // Verify RIGHT direction uses correct axis values. + assertThat(scrollCaptor.eventCount).isEqualTo(4); + float vScrollRight = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_VSCROLL); + float hScrollRight = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_HSCROLL); + assertThat(hScrollRight).isLessThan(0); + assertThat(vScrollRight).isEqualTo(0); + + // Verify scroll cursor position is preserved. + assertThat(scrollCaptor.scrollEvent.getX()).isEqualTo(expectedX); + assertThat(scrollCaptor.scrollEvent.getY()).isEqualTo(expectedY); } @Test @@ -958,15 +896,7 @@ public class AutoclickControllerTest { mController.mAutoclickTypePanel = mockAutoclickTypePanel; // Send hover move event. - MotionEvent hoverMove = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 100, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 30f, - /* y= */ 0f, - /* metaState= */ 0); - hoverMove.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 100f, MotionEvent.ACTION_HOVER_MOVE); mTestableLooper.processAllMessages(); // Verify left click sent. @@ -976,6 +906,45 @@ public class AutoclickControllerTest { assertThat(motionEventCaptor.eventCount).isEqualTo(2); } + /** + * ========================================================================= + * Helper Functions + * ========================================================================= + */ + + private void injectFakeMouseActionHoverMoveEvent() { + injectFakeMouseMoveEvent(0, 0, MotionEvent.ACTION_HOVER_MOVE); + } + + private void injectFakeMouseMoveEvent(float x, float y, int action) { + MotionEvent event = MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 0, + /* action= */ action, + /* x= */ x, + /* y= */ y, + /* metaState= */ 0); + event.setSource(InputDevice.SOURCE_MOUSE); + mController.onMotionEvent(event, event, /* policyFlags= */ 0); + } + + private void injectFakeNonMouseActionHoverMoveEvent() { + MotionEvent event = getFakeMotionHoverMoveEvent(); + event.setSource(InputDevice.SOURCE_KEYBOARD); + mController.onMotionEvent(event, event, /* policyFlags= */ 0); + } + + private void injectFakeKeyEvent(int keyCode, int modifiers) { + KeyEvent keyEvent = new KeyEvent( + /* downTime= */ 0, + /* eventTime= */ 0, + /* action= */ KeyEvent.ACTION_DOWN, + /* code= */ keyCode, + /* repeat= */ 0, + /* metaState= */ modifiers); + mController.onKeyEvent(keyEvent, /* policyFlags= */ 0); + } + private MotionEvent getFakeMotionHoverMoveEvent() { return MotionEvent.obtain( /* downTime= */ 0, diff --git a/services/tests/servicestests/src/com/android/server/accessibility/integration/FullScreenMagnificationMouseFollowingTest.kt b/services/tests/servicestests/src/com/android/server/accessibility/integration/FullScreenMagnificationMouseFollowingTest.kt new file mode 100644 index 000000000000..679bba4017fb --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/integration/FullScreenMagnificationMouseFollowingTest.kt @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.accessibility.integration + +import android.Manifest +import android.accessibility.cts.common.AccessibilityDumpOnFailureRule +import android.accessibility.cts.common.InstrumentedAccessibilityService +import android.accessibility.cts.common.InstrumentedAccessibilityServiceTestRule +import android.accessibilityservice.AccessibilityService +import android.accessibilityservice.AccessibilityServiceInfo +import android.accessibilityservice.MagnificationConfig +import android.app.Activity +import android.app.Instrumentation +import android.app.UiAutomation +import android.companion.virtual.VirtualDeviceManager +import android.graphics.PointF +import android.hardware.display.DisplayManager +import android.hardware.display.VirtualDisplay +import android.hardware.input.InputManager +import android.hardware.input.VirtualMouse +import android.hardware.input.VirtualMouseConfig +import android.hardware.input.VirtualMouseRelativeEvent +import android.os.Handler +import android.os.Looper +import android.os.OutcomeReceiver +import android.platform.test.annotations.RequiresFlagsEnabled +import android.testing.PollingCheck +import android.view.Display +import android.view.InputDevice +import android.view.MotionEvent +import android.virtualdevice.cts.common.VirtualDeviceRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.accessibility.Flags +import com.google.common.truth.Truth.assertThat +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import kotlin.math.abs +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.runner.RunWith + +// Convenient extension functions for float. +private const val EPS = 0.00001f +private fun Float.nearEq(other: Float) = abs(this - other) < EPS +private fun PointF.nearEq(other: PointF) = this.x.nearEq(other.x) && this.y.nearEq(other.y) + +/** End-to-end tests for full screen magnification following mouse cursor. */ +@RunWith(AndroidJUnit4::class) +@RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_WITH_POINTER_MOTION_FILTER) +class FullScreenMagnificationMouseFollowingTest { + + private lateinit var instrumentation: Instrumentation + private lateinit var uiAutomation: UiAutomation + + private val magnificationAccessibilityServiceRule = + InstrumentedAccessibilityServiceTestRule<TestMagnificationAccessibilityService>( + TestMagnificationAccessibilityService::class.java, false + ) + private lateinit var service: TestMagnificationAccessibilityService + + // virtualDeviceRule tears down `virtualDevice` and `virtualDisplay`. + // Note that CheckFlagsRule is a part of VirtualDeviceRule. See its javadoc. + val virtualDeviceRule: VirtualDeviceRule = + VirtualDeviceRule.withAdditionalPermissions(Manifest.permission.MANAGE_ACTIVITY_TASKS) + private lateinit var virtualDevice: VirtualDeviceManager.VirtualDevice + private lateinit var virtualDisplay: VirtualDisplay + + // Once created, it's our responsibility to close the mouse. + private lateinit var virtualMouse: VirtualMouse + + @get:Rule + val ruleChain: RuleChain = + RuleChain.outerRule(virtualDeviceRule) + .around(magnificationAccessibilityServiceRule) + .around(AccessibilityDumpOnFailureRule()) + + @Before + fun setUp() { + instrumentation = InstrumentationRegistry.getInstrumentation() + uiAutomation = + instrumentation.getUiAutomation(UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES) + uiAutomation.serviceInfo = + uiAutomation.serviceInfo!!.apply { + flags = flags or AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS + } + + prepareVirtualDevices() + + launchTestActivityFullscreen(virtualDisplay.display.displayId) + + service = magnificationAccessibilityServiceRule.enableService() + service.observingDisplayId = virtualDisplay.display.displayId + } + + @After + fun cleanUp() { + if (this::virtualMouse.isInitialized) { + virtualMouse.close() + } + } + + // Note on continuous movement: + // Assume that the entire display is magnified, and the zoom level is z. + // In continuous movement, mouse speed relative to the unscaled physical display is the same as + // unmagnified speed. While, when a cursor moves from the left edge to the right edge of the + // screen, the magnification center moves from the left bound to the right bound, which is + // (display width) * (z - 1) / z. + // + // Similarly, when the mouse cursor moves by d in unscaled, display coordinates, + // the magnification center moves by d * (z - 1) / z. + + @Test + fun testContinuous_toBottomRight() { + ensureMouseAtCenter() + + val controller = service.getMagnificationController(virtualDisplay.display.displayId) + + scaleTo(controller, 2f) + assertMagnification(controller, scale = 2f, CENTER_X, CENTER_Y) + + // Move cursor by (10, 15) + // This will move magnification center by (5, 7.5) + sendMouseMove(10f, 15f) + assertCursorLocation(CENTER_X + 10, CENTER_Y + 15) + assertMagnification(controller, scale = 2f, CENTER_X + 5, CENTER_Y + 7.5f) + + // Move cursor to the rest of the way to the edge. + sendMouseMove(DISPLAY_WIDTH - 10, DISPLAY_HEIGHT - 15) + assertCursorLocation(DISPLAY_WIDTH - 1, DISPLAY_HEIGHT - 1) + assertMagnification(controller, scale = 2f, DISPLAY_WIDTH * 3 / 4, DISPLAY_HEIGHT * 3 / 4) + + // Move cursor further won't move the magnification. + sendMouseMove(100f, 100f) + assertCursorLocation(DISPLAY_WIDTH - 1, DISPLAY_HEIGHT - 1) + } + + @Test + fun testContinuous_toTopLeft() { + ensureMouseAtCenter() + + val controller = service.getMagnificationController(virtualDisplay.display.displayId) + + scaleTo(controller, 3f) + assertMagnification(controller, scale = 3f, CENTER_X, CENTER_Y) + + // Move cursor by (-30, -15) + // This will move magnification center by (-20, -10) + sendMouseMove(-30f, -15f) + assertCursorLocation(CENTER_X - 30, CENTER_Y - 15) + assertMagnification(controller, scale = 3f, CENTER_X - 20, CENTER_Y - 10) + + // Move cursor to the rest of the way to the edge. + sendMouseMove(-CENTER_X + 30, -CENTER_Y + 15) + assertCursorLocation(0f, 0f) + assertMagnification(controller, scale = 3f, DISPLAY_WIDTH / 6, DISPLAY_HEIGHT / 6) + + // Move cursor further won't move the magnification. + sendMouseMove(-100f, -100f) + assertCursorLocation(0f, 0f) + assertMagnification(controller, scale = 3f, DISPLAY_WIDTH / 6, DISPLAY_HEIGHT / 6) + } + + private fun ensureMouseAtCenter() { + val displayCenter = PointF(320f, 240f) + val cursorLocation = virtualMouse.cursorPosition + if (!cursorLocation.nearEq(displayCenter)) { + sendMouseMove(displayCenter.x - cursorLocation.x, displayCenter.y - cursorLocation.y) + assertCursorLocation(320f, 240f) + } + } + + private fun sendMouseMove(dx: Float, dy: Float) { + virtualMouse.sendRelativeEvent( + VirtualMouseRelativeEvent.Builder().setRelativeX(dx).setRelativeY(dy).build() + ) + } + + /** + * Asserts that the cursor location is at the specified coordinates. The coordinates + * are in the non-scaled, display coordinates. + */ + private fun assertCursorLocation(x: Float, y: Float) { + PollingCheck.check("Wait for the cursor at ($x, $y)", CURSOR_TIMEOUT.inWholeMilliseconds) { + service.lastObservedCursorLocation?.let { it.x.nearEq(x) && it.y.nearEq(y) } ?: false + } + } + + private fun scaleTo(controller: AccessibilityService.MagnificationController, scale: Float) { + val config = + MagnificationConfig.Builder() + .setActivated(true) + .setMode(MagnificationConfig.MAGNIFICATION_MODE_FULLSCREEN) + .setScale(scale) + .build() + val setResult = BooleanArray(1) + service.runOnServiceSync { setResult[0] = controller.setMagnificationConfig(config, false) } + assertThat(setResult[0]).isTrue() + } + + private fun assertMagnification( + controller: AccessibilityService.MagnificationController, + scale: Float = Float.NaN, centerX: Float = Float.NaN, centerY: Float = Float.NaN + ) { + PollingCheck.check( + "Wait for the magnification to scale=$scale, centerX=$centerX, centerY=$centerY", + MAGNIFICATION_TIMEOUT.inWholeMilliseconds + ) check@{ + val actual = controller.getMagnificationConfig() ?: return@check false + actual.isActivated && + (actual.mode == MagnificationConfig.MAGNIFICATION_MODE_FULLSCREEN) && + (scale.isNaN() || scale.nearEq(actual.scale)) && + (centerX.isNaN() || centerX.nearEq(actual.centerX)) && + (centerY.isNaN() || centerY.nearEq(actual.centerY)) + } + } + + /** + * Sets up a virtual display and a virtual mouse for the test. The virtual mouse is associated + * with the virtual display. + */ + private fun prepareVirtualDevices() { + val deviceLatch = CountDownLatch(1) + val im = instrumentation.context.getSystemService(InputManager::class.java) + val inputDeviceListener = + object : InputManager.InputDeviceListener { + override fun onInputDeviceAdded(deviceId: Int) { + onInputDeviceChanged(deviceId) + } + + override fun onInputDeviceRemoved(deviceId: Int) {} + + override fun onInputDeviceChanged(deviceId: Int) { + val device = im.getInputDevice(deviceId) ?: return + if (device.vendorId == VIRTUAL_MOUSE_VENDOR_ID && + device.productId == VIRTUAL_MOUSE_PRODUCT_ID + ) { + deviceLatch.countDown() + } + } + } + im.registerInputDeviceListener(inputDeviceListener, Handler(Looper.getMainLooper())) + + virtualDevice = virtualDeviceRule.createManagedVirtualDevice() + virtualDisplay = + virtualDeviceRule.createManagedVirtualDisplay( + virtualDevice, + VirtualDeviceRule + .createDefaultVirtualDisplayConfigBuilder( + DISPLAY_WIDTH.toInt(), + DISPLAY_HEIGHT.toInt() + ) + .setFlags( + DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC + or DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED + or DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY + ) + )!! + virtualMouse = + virtualDevice.createVirtualMouse( + VirtualMouseConfig.Builder() + .setVendorId(VIRTUAL_MOUSE_VENDOR_ID) + .setProductId(VIRTUAL_MOUSE_PRODUCT_ID) + .setAssociatedDisplayId(virtualDisplay.display.displayId) + .setInputDeviceName("VirtualMouse") + .build() + ) + + deviceLatch.await(UI_IDLE_GLOBAL_TIMEOUT.inWholeSeconds, TimeUnit.SECONDS) + im.unregisterInputDeviceListener(inputDeviceListener) + } + + /** + * Launches a test (empty) activity and makes it fullscreen on the specified display. This + * ensures that system bars are hidden and the full screen magnification enlarges the entire + * display. + */ + private fun launchTestActivityFullscreen(displayId: Int) { + val future = CompletableFuture<Void?>() + val fullscreenCallback = + object : OutcomeReceiver<Void, Throwable> { + override fun onResult(result: Void?) { + future.complete(null) + } + + override fun onError(error: Throwable) { + future.completeExceptionally(error) + } + } + + val activity = + virtualDeviceRule.startActivityOnDisplaySync<TestActivity>( + displayId, + TestActivity::class.java + ) + instrumentation.runOnMainSync { + activity.requestFullscreenMode( + Activity.FULLSCREEN_MODE_REQUEST_ENTER, + fullscreenCallback + ) + } + future.get(UI_IDLE_GLOBAL_TIMEOUT.inWholeSeconds, TimeUnit.SECONDS) + + uiAutomation.waitForIdle( + UI_IDLE_TIMEOUT.inWholeMilliseconds, UI_IDLE_GLOBAL_TIMEOUT.inWholeMilliseconds + ) + } + + class TestMagnificationAccessibilityService : InstrumentedAccessibilityService() { + private val lock = Any() + + var observingDisplayId = Display.INVALID_DISPLAY + set(v) { + synchronized(lock) { field = v } + } + + var lastObservedCursorLocation: PointF? = null + private set + get() { + synchronized(lock) { + return field + } + } + + override fun onServiceConnected() { + serviceInfo = + getServiceInfo()!!.apply { setMotionEventSources(InputDevice.SOURCE_MOUSE) } + + super.onServiceConnected() + } + + override fun onMotionEvent(event: MotionEvent) { + super.onMotionEvent(event) + + synchronized(lock) { + if (event.displayId == observingDisplayId) { + lastObservedCursorLocation = PointF(event.x, event.y) + } + } + } + } + + class TestActivity : Activity() + + companion object { + private const val VIRTUAL_MOUSE_VENDOR_ID = 123 + private const val VIRTUAL_MOUSE_PRODUCT_ID = 456 + + private val CURSOR_TIMEOUT = 1.seconds + private val MAGNIFICATION_TIMEOUT = 3.seconds + private val UI_IDLE_TIMEOUT = 500.milliseconds + private val UI_IDLE_GLOBAL_TIMEOUT = 5.seconds + + private const val DISPLAY_WIDTH = 640.0f + private const val DISPLAY_HEIGHT = 480.0f + private const val CENTER_X = DISPLAY_WIDTH / 2f + private const val CENTER_Y = DISPLAY_HEIGHT / 2f + } +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java index 9eeb4f3f218f..579114bc6577 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java @@ -16,6 +16,7 @@ package com.android.server.biometrics; +import static com.android.server.biometrics.AuthenticationStatsCollector.FRR_MINIMAL_DURATION; import static com.android.server.biometrics.AuthenticationStatsCollector.MAXIMUM_ENROLLMENT_NOTIFICATIONS; import static com.google.common.truth.Truth.assertThat; @@ -37,9 +38,13 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Resources; +import android.hardware.biometrics.BiometricsProtoEnums; import android.hardware.face.FaceManager; import android.hardware.fingerprint.FingerprintManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.filters.SmallTest; @@ -54,6 +59,7 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import java.io.File; +import java.time.Clock; @Presubmit @SmallTest @@ -61,6 +67,8 @@ public class AuthenticationStatsCollectorTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private AuthenticationStatsCollector mAuthenticationStatsCollector; private static final float FRR_THRESHOLD = 0.2f; @@ -82,6 +90,8 @@ public class AuthenticationStatsCollectorTest { private SharedPreferences.Editor mEditor; @Mock private BiometricNotification mBiometricNotification; + @Mock + private Clock mClock; @Before public void setUp() { @@ -107,9 +117,12 @@ public class AuthenticationStatsCollectorTest { when(mSharedPreferences.edit()).thenReturn(mEditor); when(mEditor.putFloat(anyString(), anyFloat())).thenReturn(mEditor); when(mEditor.putStringSet(anyString(), anySet())).thenReturn(mEditor); + when(mBiometricNotification.sendCustomizeFpFrrNotification(eq(mContext))) + .thenReturn(true); + when(mClock.millis()).thenReturn(Clock.systemUTC().millis()); mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext, - 0 /* modality */, mBiometricNotification); + 0 /* modality */, mBiometricNotification, mClock); } @Test @@ -130,6 +143,8 @@ public class AuthenticationStatsCollectorTest { assertThat(authenticationStats.getTotalAttempts()).isEqualTo(1); assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0); assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0); + assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(0L); + assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(0L); } @Test @@ -151,6 +166,8 @@ public class AuthenticationStatsCollectorTest { assertThat(authenticationStats.getTotalAttempts()).isEqualTo(1); assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(1); assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0); + assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(0L); + assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(0L); } /** @@ -165,6 +182,7 @@ public class AuthenticationStatsCollectorTest { new AuthenticationStats(USER_ID_1, 400 /* totalAttempts */, 40 /* rejectedAttempts */, MAXIMUM_ENROLLMENT_NOTIFICATIONS /* enrollmentNotifications */, + 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */, 0 /* modality */)); mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */); @@ -178,14 +196,18 @@ public class AuthenticationStatsCollectorTest { assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(40); assertThat(authenticationStats.getEnrollmentNotifications()) .isEqualTo(MAXIMUM_ENROLLMENT_NOTIFICATIONS); + assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L); + assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(200L); } + // TODO WIP @Test public void authenticate_frrNotExceeded_notificationNotExceeded_shouldNotSendNotification() { mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1, new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */, 40 /* rejectedAttempts */, 0 /* enrollmentNotifications */, + 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */, 0 /* modality */)); when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) @@ -196,6 +218,7 @@ public class AuthenticationStatsCollectorTest { mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */); // Assert that no notification should be sent. + verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any()); verify(mBiometricNotification, never()).sendFaceEnrollNotification(any()); verify(mBiometricNotification, never()).sendFpEnrollNotification(any()); // Assert that data has been reset. @@ -205,6 +228,9 @@ public class AuthenticationStatsCollectorTest { assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0); assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0); assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f); + // lastEnrollmentTime and lastFrrNotificationTime shall be kept + assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L); + assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(200L); } @Test @@ -214,11 +240,13 @@ public class AuthenticationStatsCollectorTest { new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */, 400 /* rejectedAttempts */, MAXIMUM_ENROLLMENT_NOTIFICATIONS /* enrollmentNotifications */, + 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */, 0 /* modality */)); mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */); // Assert that no notification should be sent. + verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any()); verify(mBiometricNotification, never()).sendFaceEnrollNotification(any()); verify(mBiometricNotification, never()).sendFpEnrollNotification(any()); // Assert that data hasn't been reset. @@ -228,15 +256,88 @@ public class AuthenticationStatsCollectorTest { assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(400); assertThat(authenticationStats.getEnrollmentNotifications()) .isEqualTo(MAXIMUM_ENROLLMENT_NOTIFICATIONS); + assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L); + assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(200L); assertThat(authenticationStats.getFrr()).isWithin(0f).of(0.8f); } @Test + @DisableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT) public void authenticate_frrExceeded_bothBiometricsEnrolled_shouldNotSendNotification() { mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1, new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */, 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */, + 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */, + 0 /* modality */)); + + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) + .thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true); + when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + + mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */); + + // Assert that no notification should be sent. + verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any()); + verify(mBiometricNotification, never()).sendFaceEnrollNotification(any()); + verify(mBiometricNotification, never()).sendFpEnrollNotification(any()); + // Assert that data hasn't been reset. + AuthenticationStats authenticationStats = mAuthenticationStatsCollector + .getAuthenticationStatsForUser(USER_ID_1); + assertThat(authenticationStats.getTotalAttempts()).isEqualTo(500); + assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(400); + assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0); + assertThat(authenticationStats.getFrr()).isWithin(0f).of(0.8f); + assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L); + assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(200L); + } + + @Test + @EnableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT) + public void authenticate_enrollTimeNotPass_bothBiometricsEnrolled_shouldNotSendNotification() { + + long lastEnrollmentTime = 60L * 60L * 1000L; + mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1, + new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */, + 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */, + lastEnrollmentTime, 0L /* lastFrrNotificationTime */, + 0 /* modality */)); + + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) + .thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true); + when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + when(mClock.millis()).thenReturn(lastEnrollmentTime + FRR_MINIMAL_DURATION.toMillis()); + + mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */); + + // Assert that no notification should be sent. + verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any()); + verify(mBiometricNotification, never()).sendFaceEnrollNotification(any()); + verify(mBiometricNotification, never()).sendFpEnrollNotification(any()); + // Assert that data hasn't been reset. + AuthenticationStats authenticationStats = mAuthenticationStatsCollector + .getAuthenticationStatsForUser(USER_ID_1); + assertThat(authenticationStats.getTotalAttempts()).isEqualTo(500); + assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(400); + assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0); + assertThat(authenticationStats.getFrr()).isWithin(0f).of(0.8f); + assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(lastEnrollmentTime); + assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(0L); + } + + @Test + @EnableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT) + public void authenticate_lastFrrTimeNotPass_bothBiometricsEnrolled_shouldNotSendNotification() { + + long lastFrrNotificationTime = 200L; + mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1, + new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */, + 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */, + 100L /* lastEnrollmentTime */, lastFrrNotificationTime, 0 /* modality */)); when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) @@ -244,10 +345,12 @@ public class AuthenticationStatsCollectorTest { when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true); when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true); when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + when(mClock.millis()).thenReturn(lastFrrNotificationTime + FRR_MINIMAL_DURATION.toMillis()); mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */); // Assert that no notification should be sent. + verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any()); verify(mBiometricNotification, never()).sendFaceEnrollNotification(any()); verify(mBiometricNotification, never()).sendFpEnrollNotification(any()); // Assert that data hasn't been reset. @@ -257,6 +360,9 @@ public class AuthenticationStatsCollectorTest { assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(400); assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0); assertThat(authenticationStats.getFrr()).isWithin(0f).of(0.8f); + assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L); + assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo( + lastFrrNotificationTime); } @Test @@ -265,6 +371,7 @@ public class AuthenticationStatsCollectorTest { mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1, new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */, 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */, + 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */, 0 /* modality */)); when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) @@ -276,6 +383,7 @@ public class AuthenticationStatsCollectorTest { mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */); // Assert that no notification should be sent. + verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any()); verify(mBiometricNotification, never()).sendFaceEnrollNotification(any()); verify(mBiometricNotification, never()).sendFpEnrollNotification(any()); // Assert that data hasn't been reset. @@ -285,26 +393,75 @@ public class AuthenticationStatsCollectorTest { assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(400); assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0); assertThat(authenticationStats.getFrr()).isWithin(0f).of(0.8f); + assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L); + assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(200L); } @Test + @DisableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT) public void authenticate_frrExceeded_faceEnrolled_shouldSendFpNotification() { + // Use correct modality + mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext, + BiometricsProtoEnums.MODALITY_FACE, mBiometricNotification, mClock); + mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1, new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */, 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */, - 0 /* modality */)); + 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */, + BiometricsProtoEnums.MODALITY_FACE /* modality */)); + + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) + .thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true); + when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(false); + when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + when(mClock.millis()).thenReturn(3344L); + + mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */); + + // Assert that fingerprint enrollment notification should be sent. + verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any()); + verify(mBiometricNotification, times(1)).sendFpEnrollNotification(mContext); + verify(mBiometricNotification, never()).sendFaceEnrollNotification(any()); + // Assert that data has been reset. + AuthenticationStats authenticationStats = mAuthenticationStatsCollector + .getAuthenticationStatsForUser(USER_ID_1); + assertThat(authenticationStats.getTotalAttempts()).isEqualTo(0); + assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0); + assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f); + // Assert that notification count has been updated. + assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(1); + assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L); + // Assert that lastFrrNotificationTime has been updated. + assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(3344L); + } + + @Test + @EnableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT) + public void authenticate_frrExceeded_faceEnrolled_shouldSendFpNotification_withFrrFlag() { + // Use correct modality + mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext, + BiometricsProtoEnums.MODALITY_FACE, mBiometricNotification, mClock); + + mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1, + new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */, + 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */, + 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */, + BiometricsProtoEnums.MODALITY_FACE /* modality */)); when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) .thenReturn(true); when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true); when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(false); when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + long newLastFrrNotificationTime = 200L + FRR_MINIMAL_DURATION.toMillis() + 1; + when(mClock.millis()).thenReturn(newLastFrrNotificationTime); mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */); // Assert that fingerprint enrollment notification should be sent. - verify(mBiometricNotification, times(1)) - .sendFpEnrollNotification(mContext); + verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any()); + verify(mBiometricNotification, times(1)).sendFpEnrollNotification(mContext); verify(mBiometricNotification, never()).sendFaceEnrollNotification(any()); // Assert that data has been reset. AuthenticationStats authenticationStats = mAuthenticationStatsCollector @@ -314,26 +471,116 @@ public class AuthenticationStatsCollectorTest { assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f); // Assert that notification count has been updated. assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(1); + assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L); + // Assert that lastFrrNotificationTime has been updated. + assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo( + newLastFrrNotificationTime); } @Test + @DisableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT) public void authenticate_frrExceeded_fpEnrolled_shouldSendFaceNotification() { mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1, new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */, 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */, - 0 /* modality */)); + 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */, + BiometricsProtoEnums.MODALITY_FINGERPRINT /* modality */)); + + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) + .thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true); + when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(false); + when(mClock.millis()).thenReturn(3344L); + + mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */); + + // Assert that fingerprint enrollment notification should be sent. + verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any()); + verify(mBiometricNotification, times(1)).sendFaceEnrollNotification(mContext); + verify(mBiometricNotification, never()).sendFpEnrollNotification(any()); + // Assert that data has been reset. + AuthenticationStats authenticationStats = mAuthenticationStatsCollector + .getAuthenticationStatsForUser(USER_ID_1); + assertThat(authenticationStats.getTotalAttempts()).isEqualTo(0); + assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0); + assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f); + // Assert that notification count has been updated. + assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(1); + assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L); + // Assert that lastFrrNotificationTime has been updated. + assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(3344L); + } + + @Test + @EnableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT) + public void authenticate_frrExceeded_fpEnrolled_shouldSendCustNotification_withFrrFlag() { + // Use correct modality + mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext, + BiometricsProtoEnums.MODALITY_FINGERPRINT, mBiometricNotification, mClock); + + mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1, + new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */, + 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */, + 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */, + BiometricsProtoEnums.MODALITY_FINGERPRINT /* modality */)); + + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) + .thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true); + when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(false); + long newFrrNotificationTime = 200L + FRR_MINIMAL_DURATION.toMillis() + 1; + when(mClock.millis()).thenReturn(newFrrNotificationTime); + + mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */); + + // Assert that fingerprint enrollment notification should be sent. + verify(mBiometricNotification, times(1)).sendCustomizeFpFrrNotification(mContext); + verify(mBiometricNotification, never()).sendFaceEnrollNotification(any()); + verify(mBiometricNotification, never()).sendFpEnrollNotification(any()); + // Assert that data has been reset. + AuthenticationStats authenticationStats = mAuthenticationStatsCollector + .getAuthenticationStatsForUser(USER_ID_1); + assertThat(authenticationStats.getTotalAttempts()).isEqualTo(0); + assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0); + assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f); + // Assert that notification count has been updated. + assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(1); + assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L); + // Assert that lastFrrNotificationTime has been updated. + assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo( + newFrrNotificationTime); + } + + @Test + @EnableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT) + public void authenticate_frrExceeded_fpEnrolled_shouldSendFaceNotification_withFrrFlag() { + // Use correct modality + mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext, + BiometricsProtoEnums.MODALITY_FINGERPRINT, mBiometricNotification, mClock); + + mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1, + new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */, + 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */, + 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */, + BiometricsProtoEnums.MODALITY_FINGERPRINT /* modality */)); when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) .thenReturn(true); when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true); when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true); when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(false); + long newFrrNotificationTime = 200L + FRR_MINIMAL_DURATION.toMillis() + 1; + when(mClock.millis()).thenReturn(newFrrNotificationTime); + when(mBiometricNotification.sendCustomizeFpFrrNotification(eq(mContext))) + .thenReturn(false); mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */); // Assert that fingerprint enrollment notification should be sent. - verify(mBiometricNotification, times(1)) - .sendFaceEnrollNotification(mContext); + verify(mBiometricNotification, times(1)).sendCustomizeFpFrrNotification(mContext); + verify(mBiometricNotification, times(1)).sendFaceEnrollNotification(mContext); verify(mBiometricNotification, never()).sendFpEnrollNotification(any()); // Assert that data has been reset. AuthenticationStats authenticationStats = mAuthenticationStatsCollector @@ -343,6 +590,10 @@ public class AuthenticationStatsCollectorTest { assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f); // Assert that notification count has been updated. assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(1); + assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L); + // Assert that lastFrrNotificationTime has been updated. + assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo( + newFrrNotificationTime); } @Test @@ -352,11 +603,12 @@ public class AuthenticationStatsCollectorTest { .thenReturn(false); AuthenticationStatsCollector authenticationStatsCollector = new AuthenticationStatsCollector(mContext, 0 /* modality */, - mBiometricNotification); + mBiometricNotification, Clock.systemUTC()); authenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1, new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */, 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */, + 0L /* lastEnrollmentTime */, 0L /* lastFrrNotificationTime */, 0 /* modality */)); authenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsPersisterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsPersisterTest.java index 32c55ebcb674..67da3ed144fc 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsPersisterTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsPersisterTest.java @@ -60,8 +60,13 @@ public class AuthenticationStatsPersisterTest { private static final String USER_ID = "user_id"; private static final String FACE_ATTEMPTS = "face_attempts"; private static final String FACE_REJECTIONS = "face_rejections"; + private static final String FACE_LAST_ENROLL_TIME = "face_last_enroll_time"; + private static final String FACE_LAST_FRR_NOTIFICATION_TIME = "face_last_notification_time"; private static final String FINGERPRINT_ATTEMPTS = "fingerprint_attempts"; private static final String FINGERPRINT_REJECTIONS = "fingerprint_rejections"; + private static final String FINGERPRINT_LAST_ENROLL_TIME = "fingerprint_last_enroll_time"; + private static final String FINGERPRINT_LAST_FRR_NOTIFICATION_TIME = + "fingerprint_last_notification_time"; private static final String ENROLLMENT_NOTIFICATIONS = "enrollment_notifications"; private static final String KEY = "frr_stats"; private static final String THRESHOLD_KEY = "frr_threshold"; @@ -95,10 +100,12 @@ public class AuthenticationStatsPersisterTest { public void getAllFrrStats_face_shouldListAllFrrStats() throws JSONException { AuthenticationStats stats1 = new AuthenticationStats(USER_ID_1, 300 /* totalAttempts */, 10 /* rejectedAttempts */, - 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); + 0 /* enrollmentNotifications */, 100L /* lastEnrollmentTime */, + 200L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE); AuthenticationStats stats2 = new AuthenticationStats(USER_ID_2, 200 /* totalAttempts */, 20 /* rejectedAttempts */, - 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT); + 0 /* enrollmentNotifications */, 200L /* lastEnrollmentTime */, + 300L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FINGERPRINT); when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn( Set.of(buildFrrStats(stats1), buildFrrStats(stats2))); @@ -108,7 +115,8 @@ public class AuthenticationStatsPersisterTest { assertThat(authenticationStatsList.size()).isEqualTo(2); AuthenticationStats expectedStats2 = new AuthenticationStats(USER_ID_2, 0 /* totalAttempts */, 0 /* rejectedAttempts */, - 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); + 0 /* enrollmentNotifications */, 0 /* lastEnrollmentTime */, + 0 /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE); assertThat(authenticationStatsList).contains(stats1); assertThat(authenticationStatsList).contains(expectedStats2); } @@ -118,11 +126,13 @@ public class AuthenticationStatsPersisterTest { // User 1 with fingerprint authentication stats. AuthenticationStats stats1 = new AuthenticationStats(USER_ID_1, 200 /* totalAttempts */, 20 /* rejectedAttempts */, - 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT); + 0 /* enrollmentNotifications */, 100L /* lastEnrollmentTime */, + 200L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FINGERPRINT); // User 2 without fingerprint authentication stats. AuthenticationStats stats2 = new AuthenticationStats(USER_ID_2, 300 /* totalAttempts */, 10 /* rejectedAttempts */, - 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); + 0 /* enrollmentNotifications */, 200L /* lastEnrollmentTime */, + 300L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE); when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn( Set.of(buildFrrStats(stats1), buildFrrStats(stats2))); @@ -133,7 +143,8 @@ public class AuthenticationStatsPersisterTest { assertThat(authenticationStatsList.size()).isEqualTo(2); AuthenticationStats expectedStats2 = new AuthenticationStats(USER_ID_2, 0 /* totalAttempts */, 0 /* rejectedAttempts */, - 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT); + 0 /* enrollmentNotifications */, 0 /* lastEnrollmentTime */, + 0 /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FINGERPRINT); assertThat(authenticationStatsList).contains(stats1); assertThat(authenticationStatsList).contains(expectedStats2); } @@ -142,12 +153,15 @@ public class AuthenticationStatsPersisterTest { public void persistFrrStats_newUser_face_shouldSuccess() throws JSONException { AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1, 300 /* totalAttempts */, 10 /* rejectedAttempts */, - 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); + 0 /* enrollmentNotifications */, 200L /* lastEnrollmentTime */, + 300L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE); mAuthenticationStatsPersister.persistFrrStats(authenticationStats.getUserId(), authenticationStats.getTotalAttempts(), authenticationStats.getRejectedAttempts(), authenticationStats.getEnrollmentNotifications(), + authenticationStats.getLastEnrollmentTime(), + authenticationStats.getLastFrrNotificationTime(), authenticationStats.getModality()); verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture()); @@ -159,12 +173,15 @@ public class AuthenticationStatsPersisterTest { public void persistFrrStats_newUser_fingerprint_shouldSuccess() throws JSONException { AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1, 300 /* totalAttempts */, 10 /* rejectedAttempts */, - 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT); + 0 /* enrollmentNotifications */, 100L /* lastEnrollmentTime */, + 200L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FINGERPRINT); mAuthenticationStatsPersister.persistFrrStats(authenticationStats.getUserId(), authenticationStats.getTotalAttempts(), authenticationStats.getRejectedAttempts(), authenticationStats.getEnrollmentNotifications(), + authenticationStats.getLastEnrollmentTime(), + authenticationStats.getLastFrrNotificationTime(), authenticationStats.getModality()); verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture()); @@ -176,10 +193,12 @@ public class AuthenticationStatsPersisterTest { public void persistFrrStats_existingUser_shouldUpdateRecord() throws JSONException { AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1, 300 /* totalAttempts */, 10 /* rejectedAttempts */, - 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); + 0 /* enrollmentNotifications */, 100L /* lastEnrollmentTime */, + 200L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE); AuthenticationStats newAuthenticationStats = new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */, 30 /* rejectedAttempts */, - 1 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); + 1 /* enrollmentNotifications */, 200L /* lastEnrollmentTime */, + 300L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE); when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn( Set.of(buildFrrStats(authenticationStats))); @@ -187,6 +206,8 @@ public class AuthenticationStatsPersisterTest { newAuthenticationStats.getTotalAttempts(), newAuthenticationStats.getRejectedAttempts(), newAuthenticationStats.getEnrollmentNotifications(), + newAuthenticationStats.getLastEnrollmentTime(), + newAuthenticationStats.getLastFrrNotificationTime(), newAuthenticationStats.getModality()); verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture()); @@ -200,11 +221,13 @@ public class AuthenticationStatsPersisterTest { // User with fingerprint authentication stats. AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1, 200 /* totalAttempts */, 20 /* rejectedAttempts */, - 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT); + 0 /* enrollmentNotifications */, 100L /* lastEnrollmentTime */, + 200L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FINGERPRINT); // The same user with face authentication stats. AuthenticationStats newAuthenticationStats = new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */, 30 /* rejectedAttempts */, - 1 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); + 1 /* enrollmentNotifications */, 200L /* lastEnrollmentTime */, + 300L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE); when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn( Set.of(buildFrrStats(authenticationStats))); @@ -212,12 +235,18 @@ public class AuthenticationStatsPersisterTest { newAuthenticationStats.getTotalAttempts(), newAuthenticationStats.getRejectedAttempts(), newAuthenticationStats.getEnrollmentNotifications(), + newAuthenticationStats.getLastEnrollmentTime(), + newAuthenticationStats.getLastFrrNotificationTime(), newAuthenticationStats.getModality()); String expectedFrrStats = new JSONObject(buildFrrStats(authenticationStats)) .put(ENROLLMENT_NOTIFICATIONS, newAuthenticationStats.getEnrollmentNotifications()) .put(FACE_ATTEMPTS, newAuthenticationStats.getTotalAttempts()) - .put(FACE_REJECTIONS, newAuthenticationStats.getRejectedAttempts()).toString(); + .put(FACE_REJECTIONS, newAuthenticationStats.getRejectedAttempts()) + .put(FACE_LAST_ENROLL_TIME, newAuthenticationStats.getLastEnrollmentTime()) + .put(FACE_LAST_FRR_NOTIFICATION_TIME, + newAuthenticationStats.getLastFrrNotificationTime()) + .toString(); verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture()); assertThat(mStringSetArgumentCaptor.getValue()).contains(expectedFrrStats); } @@ -226,10 +255,12 @@ public class AuthenticationStatsPersisterTest { public void persistFrrStats_multiUser_newUser_shouldUpdateRecord() throws JSONException { AuthenticationStats authenticationStats1 = new AuthenticationStats(USER_ID_1, 300 /* totalAttempts */, 10 /* rejectedAttempts */, - 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); + 0 /* enrollmentNotifications */, 100L /* lastEnrollmentTime */, + 200L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE); AuthenticationStats authenticationStats2 = new AuthenticationStats(USER_ID_2, 100 /* totalAttempts */, 5 /* rejectedAttempts */, - 1 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT); + 1 /* enrollmentNotifications */, 200L /* lastEnrollmentTime */, + 300L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FINGERPRINT); // Sets up the shared preference with user 1 only. when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn( @@ -240,6 +271,8 @@ public class AuthenticationStatsPersisterTest { authenticationStats2.getTotalAttempts(), authenticationStats2.getRejectedAttempts(), authenticationStats2.getEnrollmentNotifications(), + authenticationStats2.getLastEnrollmentTime(), + authenticationStats2.getLastFrrNotificationTime(), authenticationStats2.getModality()); verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture()); @@ -251,7 +284,8 @@ public class AuthenticationStatsPersisterTest { public void removeFrrStats_existingUser_shouldUpdateRecord() throws JSONException { AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1, 300 /* totalAttempts */, 10 /* rejectedAttempts */, - 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); + 0 /* enrollmentNotifications */, 200L /* lastEnrollmentTime */, + 300L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE); when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn( Set.of(buildFrrStats(authenticationStats))); @@ -277,6 +311,9 @@ public class AuthenticationStatsPersisterTest { .put(FACE_ATTEMPTS, authenticationStats.getTotalAttempts()) .put(FACE_REJECTIONS, authenticationStats.getRejectedAttempts()) .put(ENROLLMENT_NOTIFICATIONS, authenticationStats.getEnrollmentNotifications()) + .put(FACE_LAST_ENROLL_TIME, authenticationStats.getLastEnrollmentTime()) + .put(FACE_LAST_FRR_NOTIFICATION_TIME, + authenticationStats.getLastFrrNotificationTime()) .toString(); } else if (authenticationStats.getModality() == BiometricsProtoEnums.MODALITY_FINGERPRINT) { return new JSONObject() @@ -284,6 +321,9 @@ public class AuthenticationStatsPersisterTest { .put(FINGERPRINT_ATTEMPTS, authenticationStats.getTotalAttempts()) .put(FINGERPRINT_REJECTIONS, authenticationStats.getRejectedAttempts()) .put(ENROLLMENT_NOTIFICATIONS, authenticationStats.getEnrollmentNotifications()) + .put(FINGERPRINT_LAST_ENROLL_TIME, authenticationStats.getLastEnrollmentTime()) + .put(FINGERPRINT_LAST_FRR_NOTIFICATION_TIME, + authenticationStats.getLastFrrNotificationTime()) .toString(); } return ""; diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsTest.java index e8e72cb81838..ca7b83caf20f 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsTest.java @@ -27,6 +27,7 @@ public class AuthenticationStatsTest { AuthenticationStats authenticationStats = new AuthenticationStats(1 /* userId */ , 0 /* totalAttempts */, 0 /* rejectedAttempts */, 0 /* enrollmentNotifications */, + 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */, 0 /* modality */); authenticationStats.authenticate(true /* authenticated */); @@ -38,5 +39,8 @@ public class AuthenticationStatsTest { assertEquals(authenticationStats.getTotalAttempts(), 2); assertEquals(authenticationStats.getRejectedAttempts(), 1); + + assertEquals(authenticationStats.getLastEnrollmentTime(), 100L); + assertEquals(authenticationStats.getLastFrrNotificationTime(), 200L); } } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClientTest.java index 276da39615af..c000f2f4c7a4 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClientTest.java @@ -19,6 +19,8 @@ package com.android.server.biometrics.sensors.face.aidl; import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_START; import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_TOO_DARK; +import static com.android.server.biometrics.AuthenticationStatsCollector.ACTION_LAST_ENROLL_TIME_CHANGED; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -33,6 +35,10 @@ import static org.mockito.Mockito.same; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.hardware.biometrics.BiometricFaceConstants; import android.hardware.biometrics.BiometricRequestConstants; import android.hardware.biometrics.BiometricSourceType; @@ -56,6 +62,7 @@ import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; import com.android.internal.R; +import com.android.server.biometrics.AuthenticationStatsCollector; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.log.OperationContextExt; @@ -74,6 +81,8 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @Presubmit @@ -200,6 +209,26 @@ public class FaceEnrollClientTest { eq(BiometricsProtoEnums.ENROLLMENT_SOURCE_SUW), eq(1)); } + @Test + public void testEnrollWithBroadcastEnrollTime() throws RemoteException, InterruptedException { + final FaceEnrollClient client = createClient(4); + final CountDownLatch countDownLatch = new CountDownLatch(1); + final EnrollmentTimeReceiver receiver = new EnrollmentTimeReceiver(countDownLatch); + mContext.registerReceiver(receiver, new IntentFilter(ACTION_LAST_ENROLL_TIME_CHANGED), + Context.RECEIVER_NOT_EXPORTED); + + client.start(mCallback); + client.onEnrollResult(new Face("face", 1 /* faceId */, 20 /* deviceId */), 0); + + assertThat(countDownLatch.await(2, TimeUnit.SECONDS)).isTrue(); + final Intent intent = receiver.mIntent; + assertThat(intent).isNotNull(); + assertThat(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)).isEqualTo(USER_ID); + assertThat(intent.getIntExtra(AuthenticationStatsCollector.EXTRA_MODALITY, + BiometricsProtoEnums.MODALITY_UNKNOWN)) + .isEqualTo(BiometricsProtoEnums.MODALITY_FACE); + } + private FaceEnrollClient createClient() throws RemoteException { return createClient(200 /* version */); } @@ -295,4 +324,18 @@ public class FaceEnrollClientTest { ); } + static final class EnrollmentTimeReceiver extends BroadcastReceiver { + final CountDownLatch mLatch; + Intent mIntent; + + EnrollmentTimeReceiver(CountDownLatch latch) { + mLatch = latch; + } + + @Override + public void onReceive(Context context, Intent intent) { + mIntent = intent; + mLatch.countDown(); + } + } } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java index ea96d193c762..4d6fd7ef02c4 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java @@ -20,6 +20,8 @@ import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPR import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_TOO_FAST; import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_TIMEOUT; +import static com.android.server.biometrics.AuthenticationStatsCollector.ACTION_LAST_ENROLL_TIME_CHANGED; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -34,6 +36,10 @@ import static org.mockito.Mockito.same; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.hardware.biometrics.BiometricRequestConstants; import android.hardware.biometrics.BiometricSourceType; import android.hardware.biometrics.BiometricsProtoEnums; @@ -62,6 +68,7 @@ import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; import com.android.internal.R; +import com.android.server.biometrics.AuthenticationStatsCollector; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.log.CallbackWithProbe; @@ -83,6 +90,8 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @Presubmit @@ -312,6 +321,27 @@ public class FingerprintEnrollClientTest { eq(BiometricsProtoEnums.ENROLLMENT_SOURCE_SUW), eq(1)); } + @Test + public void testEnrollWithBroadcastEnrollTime() throws RemoteException, InterruptedException { + final FingerprintEnrollClient client = createClient(4); + final CountDownLatch countDownLatch = new CountDownLatch(1); + final EnrollmentTimeReceiver receiver = new EnrollmentTimeReceiver(countDownLatch); + mContext.registerReceiver(receiver, new IntentFilter(ACTION_LAST_ENROLL_TIME_CHANGED), + Context.RECEIVER_NOT_EXPORTED); + + client.start(mCallback); + client.onEnrollResult(new Fingerprint("fingerprint", 1 /* fingerId */, 20 /* deviceId */), + 0); + + assertThat(countDownLatch.await(2, TimeUnit.SECONDS)).isTrue(); + final Intent intent = receiver.mIntent; + assertThat(intent).isNotNull(); + assertThat(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)).isEqualTo(0); + assertThat(intent.getIntExtra(AuthenticationStatsCollector.EXTRA_MODALITY, + BiometricsProtoEnums.MODALITY_UNKNOWN)) + .isEqualTo(BiometricsProtoEnums.MODALITY_FINGERPRINT); + } + private void showHideOverlay( Consumer<FingerprintEnrollClient> block) throws RemoteException { final FingerprintEnrollClient client = createClient(); @@ -409,4 +439,19 @@ public class FingerprintEnrollClientTest { .setEnrollReason(ENROLL_SOURCE).build() ); } + + static final class EnrollmentTimeReceiver extends BroadcastReceiver { + final CountDownLatch mLatch; + Intent mIntent; + + EnrollmentTimeReceiver(CountDownLatch latch) { + mLatch = latch; + } + + @Override + public void onReceive(Context context, Intent intent) { + mIntent = intent; + mLatch.countDown(); + } + } } diff --git a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java index 87cd1560509c..992c1183d0c0 100644 --- a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java +++ b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; @@ -62,6 +63,7 @@ import org.mockito.junit.MockitoRule; import java.util.Collections; import java.util.List; +import java.util.concurrent.ScheduledExecutorService; @RunWith(AndroidJUnit4.class) @Presubmit @@ -97,6 +99,7 @@ public class ContextHubEndpointTest { private HubInfoRegistry mHubInfoRegistry; private ContextHubTransactionManager mTransactionManager; private Context mContext; + @Mock private ScheduledExecutorService mMockTimeoutExecutorService; @Mock private IEndpointCommunication mMockEndpointCommunications; @Mock private IContextHubWrapper mMockContextHubWrapper; @Mock private IContextHubEndpointCallback mMockCallback; @@ -120,7 +123,11 @@ public class ContextHubEndpointTest { mMockContextHubWrapper, mClientManager, new NanoAppStateManager()); mEndpointManager = new ContextHubEndpointManager( - mContext, mMockContextHubWrapper, mHubInfoRegistry, mTransactionManager); + mContext, + mMockContextHubWrapper, + mHubInfoRegistry, + mTransactionManager, + mMockTimeoutExecutorService); mEndpointManager.init(); } @@ -248,14 +255,20 @@ public class ContextHubEndpointTest { endpoint.getAssignedHubEndpointInfo().getIdentifier(), targetInfo.getIdentifier(), ENDPOINT_SERVICE_DESCRIPTOR); - verify(mMockCallback) .onSessionOpenRequest( SESSION_ID_FOR_OPEN_REQUEST, targetInfo, ENDPOINT_SERVICE_DESCRIPTOR); // Accept endpoint.openSessionRequestComplete(SESSION_ID_FOR_OPEN_REQUEST); - verify(mMockEndpointCommunications) + + // Even when timeout happens, there should be no effect on this session + ArgumentCaptor<Runnable> runnableArgumentCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mMockTimeoutExecutorService) + .schedule(runnableArgumentCaptor.capture(), anyLong(), any()); + runnableArgumentCaptor.getValue().run(); + + verify(mMockEndpointCommunications, times(1)) .endpointSessionOpenComplete(SESSION_ID_FOR_OPEN_REQUEST); unregisterExampleEndpoint(endpoint); @@ -331,6 +344,87 @@ public class ContextHubEndpointTest { } @Test + public void testEndpointSessionOpenRequest_rejectAfterTimeout() throws RemoteException { + assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE); + IContextHubEndpoint endpoint = registerExampleEndpoint(); + + HubEndpointInfo targetInfo = + new HubEndpointInfo( + TARGET_ENDPOINT_NAME, + TARGET_ENDPOINT_ID, + ENDPOINT_PACKAGE_NAME, + Collections.emptyList()); + mHubInfoRegistry.onEndpointStarted(new HubEndpointInfo[] {targetInfo}); + mEndpointManager.onEndpointSessionOpenRequest( + SESSION_ID_FOR_OPEN_REQUEST, + endpoint.getAssignedHubEndpointInfo().getIdentifier(), + targetInfo.getIdentifier(), + ENDPOINT_SERVICE_DESCRIPTOR); + + // Immediately timeout + ArgumentCaptor<Runnable> runnableArgumentCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mMockTimeoutExecutorService) + .schedule(runnableArgumentCaptor.capture(), anyLong(), any()); + runnableArgumentCaptor.getValue().run(); + + // Client's callback shouldn't matter after timeout + try { + endpoint.openSessionRequestComplete(SESSION_ID_FOR_OPEN_REQUEST); + } catch (IllegalArgumentException ignore) { + // This will throw because the session is no longer valid + } + + // HAL will receive closeEndpointSession with Timeout as reason + verify(mMockEndpointCommunications, times(1)) + .closeEndpointSession(SESSION_ID_FOR_OPEN_REQUEST, Reason.TIMEOUT); + // HAL will not receives open complete notifications + verify(mMockEndpointCommunications, never()) + .endpointSessionOpenComplete(SESSION_ID_FOR_OPEN_REQUEST); + + unregisterExampleEndpoint(endpoint); + } + + @Test + public void testEndpointSessionOpenRequest_duplicatedSessionId_noopWithinTimeout() + throws RemoteException { + assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE); + IContextHubEndpoint endpoint = registerExampleEndpoint(); + + HubEndpointInfo targetInfo = + new HubEndpointInfo( + TARGET_ENDPOINT_NAME, + TARGET_ENDPOINT_ID, + ENDPOINT_PACKAGE_NAME, + Collections.emptyList()); + mHubInfoRegistry.onEndpointStarted(new HubEndpointInfo[] {targetInfo}); + mEndpointManager.onEndpointSessionOpenRequest( + SESSION_ID_FOR_OPEN_REQUEST, + endpoint.getAssignedHubEndpointInfo().getIdentifier(), + targetInfo.getIdentifier(), + ENDPOINT_SERVICE_DESCRIPTOR); + + // Duplicated session open request + mEndpointManager.onEndpointSessionOpenRequest( + SESSION_ID_FOR_OPEN_REQUEST, + endpoint.getAssignedHubEndpointInfo().getIdentifier(), + targetInfo.getIdentifier(), + ENDPOINT_SERVICE_DESCRIPTOR); + + // Finally, endpoint approved the session open request + endpoint.openSessionRequestComplete(SESSION_ID_FOR_OPEN_REQUEST); + + // Client API is only invoked once + verify(mMockCallback, times(1)) + .onSessionOpenRequest( + SESSION_ID_FOR_OPEN_REQUEST, targetInfo, ENDPOINT_SERVICE_DESCRIPTOR); + // HAL still receives two open complete notifications + verify(mMockEndpointCommunications, times(1)) + .endpointSessionOpenComplete(SESSION_ID_FOR_OPEN_REQUEST); + + unregisterExampleEndpoint(endpoint); + } + + @Test public void testMessageTransaction() throws RemoteException { IContextHubEndpoint endpoint = registerExampleEndpoint(); testMessageTransactionInternal(endpoint, /* deliverMessageStatus= */ true); diff --git a/services/tests/servicestests/src/com/android/server/media/LegacyDeviceRouteControllerTest.java b/services/tests/servicestests/src/com/android/server/media/LegacyDeviceRouteControllerTest.java index aed68a5dc7b5..113e039d9a43 100644 --- a/services/tests/servicestests/src/com/android/server/media/LegacyDeviceRouteControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/media/LegacyDeviceRouteControllerTest.java @@ -30,12 +30,17 @@ import android.media.AudioRoutesInfo; import android.media.IAudioRoutesObserver; import android.media.MediaRoute2Info; import android.os.RemoteException; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.text.TextUtils; import com.android.internal.R; +import com.android.media.flags.Flags; import com.android.server.audio.AudioService; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.experimental.runners.Enclosed; import org.junit.runner.RunWith; @@ -48,6 +53,7 @@ import org.mockito.MockitoAnnotations; import java.util.Arrays; import java.util.Collection; +import java.util.List; @RunWith(Enclosed.class) public class LegacyDeviceRouteControllerTest { @@ -70,6 +76,11 @@ public class LegacyDeviceRouteControllerTest { @RunWith(JUnit4.class) public static class DefaultDeviceRouteValueTest { + + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + @Mock private Context mContext; @Mock @@ -136,6 +147,23 @@ public class LegacyDeviceRouteControllerTest { .isTrue(); assertThat(actualMediaRoute.getVolume()).isEqualTo(VOLUME_DEFAULT_VALUE); } + + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_FIX_FOR_EMPTY_SYSTEM_ROUTES_CRASH) + @Test + public void getAvailableRoutes_matchesSelectedRoute() { + when(mResources.getText(R.string.default_audio_route_name)) + .thenReturn(DEFAULT_ROUTE_NAME); + + when(mAudioService.startWatchingRoutes(any())).thenReturn(null); + + LegacyDeviceRouteController deviceRouteController = + new LegacyDeviceRouteController( + mContext, mAudioManager, mAudioService, mOnDeviceRouteChangedListener); + + MediaRoute2Info selectedRoute = deviceRouteController.getSelectedRoute(); + assertThat(deviceRouteController.getAvailableRoutes()) + .isEqualTo(List.of(selectedRoute)); + } } @RunWith(Parameterized.class) diff --git a/services/tests/servicestests/src/com/android/server/security/advancedprotection/AdvancedProtectionServiceTest.java b/services/tests/servicestests/src/com/android/server/security/advancedprotection/AdvancedProtectionServiceTest.java index d6b3fecb487c..ca71ed6a4130 100644 --- a/services/tests/servicestests/src/com/android/server/security/advancedprotection/AdvancedProtectionServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/security/advancedprotection/AdvancedProtectionServiceTest.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.mock; import android.Manifest; import android.annotation.SuppressLint; import android.content.Context; +import android.content.pm.UserInfo; import android.os.RemoteException; import android.os.test.FakePermissionEnforcer; import android.os.test.TestLooper; @@ -36,6 +37,7 @@ import android.security.advancedprotection.IAdvancedProtectionCallback; import androidx.annotation.NonNull; +import com.android.server.pm.UserManagerInternal; import com.android.server.security.advancedprotection.features.AdvancedProtectionHook; import com.android.server.security.advancedprotection.features.AdvancedProtectionProvider; @@ -48,26 +50,37 @@ import java.util.List; import java.util.Map; import java.util.HashMap; import java.util.concurrent.atomic.AtomicBoolean; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; @SuppressLint("VisibleForTests") @RunWith(JUnit4.class) public class AdvancedProtectionServiceTest { - private AdvancedProtectionService mService; private FakePermissionEnforcer mPermissionEnforcer; + private UserManagerInternal mUserManager; private Context mContext; - private AdvancedProtectionService.AdvancedProtectionStore mStore; private TestLooper mLooper; - AdvancedProtectionFeature mTestFeature2g = new AdvancedProtectionFeature( - AdvancedProtectionManager.FEATURE_ID_DISALLOW_CELLULAR_2G); + AdvancedProtectionFeature mTestFeature2g = + new AdvancedProtectionFeature( + AdvancedProtectionManager.FEATURE_ID_DISALLOW_CELLULAR_2G); @Before public void setup() throws Settings.SettingNotFoundException { mContext = mock(Context.class); + mUserManager = mock(UserManagerInternal.class); + mLooper = new TestLooper(); mPermissionEnforcer = new FakePermissionEnforcer(); mPermissionEnforcer.grant(Manifest.permission.MANAGE_ADVANCED_PROTECTION_MODE); mPermissionEnforcer.grant(Manifest.permission.QUERY_ADVANCED_PROTECTION_MODE); + Mockito.when(mUserManager.getUserInfo(ArgumentMatchers.anyInt())) + .thenReturn(new UserInfo(0, "user", UserInfo.FLAG_ADMIN)); + } + + private AdvancedProtectionService createService( + AdvancedProtectionHook hook, AdvancedProtectionProvider provider) { - mStore = new AdvancedProtectionService.AdvancedProtectionStore(mContext) { + AdvancedProtectionService.AdvancedProtectionStore + store = new AdvancedProtectionService.AdvancedProtectionStore(mContext) { private Map<String, Integer> mStoredValues = new HashMap<>(); private boolean mEnabled = false; @@ -92,24 +105,38 @@ public class AdvancedProtectionServiceTest { } }; - mLooper = new TestLooper(); - - mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(), - mPermissionEnforcer, null, null); + return new AdvancedProtectionService( + mContext, + store, + mUserManager, + mLooper.getLooper(), + mPermissionEnforcer, + hook, + provider); } @Test public void testToggleProtection() { - mService.setAdvancedProtectionEnabled(true); - assertTrue(mService.isAdvancedProtectionEnabled()); + AdvancedProtectionService service = createService(null, null); + service.setAdvancedProtectionEnabled(true); + assertTrue(service.isAdvancedProtectionEnabled()); - mService.setAdvancedProtectionEnabled(false); - assertFalse(mService.isAdvancedProtectionEnabled()); + service.setAdvancedProtectionEnabled(false); + assertFalse(service.isAdvancedProtectionEnabled()); } @Test public void testDisableProtection_byDefault() { - assertFalse(mService.isAdvancedProtectionEnabled()); + AdvancedProtectionService service = createService(null, null); + assertFalse(service.isAdvancedProtectionEnabled()); + } + + @Test + public void testSetProtection_nonAdminUser() { + Mockito.when(mUserManager.getUserInfo(ArgumentMatchers.anyInt())) + .thenReturn(new UserInfo(1, "user2", UserInfo.FLAG_FULL)); + AdvancedProtectionService service = createService(null, null); + assertThrows(SecurityException.class, () -> service.setAdvancedProtectionEnabled(true)); } @Test @@ -134,9 +161,8 @@ public class AdvancedProtectionServiceTest { } }; - mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(), - mPermissionEnforcer, hook, null); - mService.setAdvancedProtectionEnabled(true); + AdvancedProtectionService service = createService(hook, null); + service.setAdvancedProtectionEnabled(true); mLooper.dispatchNext(); assertTrue(callbackCaptor.get()); @@ -164,10 +190,8 @@ public class AdvancedProtectionServiceTest { } }; - mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(), - mPermissionEnforcer, hook, null); - - mService.setAdvancedProtectionEnabled(true); + AdvancedProtectionService service = createService(hook, null); + service.setAdvancedProtectionEnabled(true); mLooper.dispatchNext(); assertFalse(callbackCalledCaptor.get()); } @@ -194,14 +218,13 @@ public class AdvancedProtectionServiceTest { } }; - mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(), - mPermissionEnforcer, hook, null); - mService.setAdvancedProtectionEnabled(true); + AdvancedProtectionService service = createService(hook, null); + service.setAdvancedProtectionEnabled(true); mLooper.dispatchNext(); assertTrue(callbackCalledCaptor.get()); callbackCalledCaptor.set(false); - mService.setAdvancedProtectionEnabled(true); + service.setAdvancedProtectionEnabled(true); mLooper.dispatchAll(); assertFalse(callbackCalledCaptor.get()); } @@ -209,21 +232,24 @@ public class AdvancedProtectionServiceTest { @Test public void testRegisterCallback() throws RemoteException { AtomicBoolean callbackCaptor = new AtomicBoolean(false); - IAdvancedProtectionCallback callback = new IAdvancedProtectionCallback.Stub() { - @Override - public void onAdvancedProtectionChanged(boolean enabled) { - callbackCaptor.set(enabled); - } - }; + IAdvancedProtectionCallback callback = + new IAdvancedProtectionCallback.Stub() { + @Override + public void onAdvancedProtectionChanged(boolean enabled) { + callbackCaptor.set(enabled); + } + }; + + AdvancedProtectionService service = createService(null, null); - mService.setAdvancedProtectionEnabled(true); + service.setAdvancedProtectionEnabled(true); mLooper.dispatchAll(); - mService.registerAdvancedProtectionCallback(callback); + service.registerAdvancedProtectionCallback(callback); mLooper.dispatchNext(); assertTrue(callbackCaptor.get()); - mService.setAdvancedProtectionEnabled(false); + service.setAdvancedProtectionEnabled(false); mLooper.dispatchNext(); assertFalse(callbackCaptor.get()); @@ -232,20 +258,23 @@ public class AdvancedProtectionServiceTest { @Test public void testUnregisterCallback() throws RemoteException { AtomicBoolean callbackCalledCaptor = new AtomicBoolean(false); - IAdvancedProtectionCallback callback = new IAdvancedProtectionCallback.Stub() { - @Override - public void onAdvancedProtectionChanged(boolean enabled) { - callbackCalledCaptor.set(true); - } - }; + IAdvancedProtectionCallback callback = + new IAdvancedProtectionCallback.Stub() { + @Override + public void onAdvancedProtectionChanged(boolean enabled) { + callbackCalledCaptor.set(true); + } + }; + + AdvancedProtectionService service = createService(null, null); - mService.setAdvancedProtectionEnabled(true); - mService.registerAdvancedProtectionCallback(callback); + service.setAdvancedProtectionEnabled(true); + service.registerAdvancedProtectionCallback(callback); mLooper.dispatchAll(); callbackCalledCaptor.set(false); - mService.unregisterAdvancedProtectionCallback(callback); - mService.setAdvancedProtectionEnabled(false); + service.unregisterAdvancedProtectionCallback(callback); + service.setAdvancedProtectionEnabled(false); mLooper.dispatchNext(); assertFalse(callbackCalledCaptor.get()); @@ -253,104 +282,107 @@ public class AdvancedProtectionServiceTest { @Test public void testGetFeatures() { - AdvancedProtectionFeature feature1 = new AdvancedProtectionFeature( - AdvancedProtectionManager.FEATURE_ID_DISALLOW_CELLULAR_2G); - AdvancedProtectionFeature feature2 = new AdvancedProtectionFeature( - AdvancedProtectionManager.FEATURE_ID_DISALLOW_INSTALL_UNKNOWN_SOURCES); - AdvancedProtectionHook hook = new AdvancedProtectionHook(mContext, true) { - @NonNull - @Override - public AdvancedProtectionFeature getFeature() { - return feature1; - } + AdvancedProtectionFeature feature1 = + new AdvancedProtectionFeature( + AdvancedProtectionManager.FEATURE_ID_DISALLOW_CELLULAR_2G); + AdvancedProtectionFeature feature2 = + new AdvancedProtectionFeature( + AdvancedProtectionManager.FEATURE_ID_DISALLOW_INSTALL_UNKNOWN_SOURCES); + AdvancedProtectionHook hook = + new AdvancedProtectionHook(mContext, true) { + @NonNull + @Override + public AdvancedProtectionFeature getFeature() { + return feature1; + } - @Override - public boolean isAvailable() { - return true; - } - }; + @Override + public boolean isAvailable() { + return true; + } + }; - AdvancedProtectionProvider provider = new AdvancedProtectionProvider() { - @Override - public List<AdvancedProtectionFeature> getFeatures(Context context) { - return List.of(feature2); - } - }; + AdvancedProtectionProvider provider = + new AdvancedProtectionProvider() { + @Override + public List<AdvancedProtectionFeature> getFeatures(Context context) { + return List.of(feature2); + } + }; - mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(), - mPermissionEnforcer, hook, provider); - List<AdvancedProtectionFeature> features = mService.getAdvancedProtectionFeatures(); + AdvancedProtectionService service = createService(hook, provider); + List<AdvancedProtectionFeature> features = service.getAdvancedProtectionFeatures(); assertThat(features, containsInAnyOrder(feature1, feature2)); } @Test public void testGetFeatures_featureNotAvailable() { - AdvancedProtectionFeature feature1 = new AdvancedProtectionFeature( - AdvancedProtectionManager.FEATURE_ID_DISALLOW_CELLULAR_2G); - AdvancedProtectionFeature feature2 = new AdvancedProtectionFeature( - AdvancedProtectionManager.FEATURE_ID_DISALLOW_INSTALL_UNKNOWN_SOURCES); - AdvancedProtectionHook hook = new AdvancedProtectionHook(mContext, true) { - @NonNull - @Override - public AdvancedProtectionFeature getFeature() { - return feature1; - } + AdvancedProtectionFeature feature1 = + new AdvancedProtectionFeature( + AdvancedProtectionManager.FEATURE_ID_DISALLOW_CELLULAR_2G); + AdvancedProtectionFeature feature2 = + new AdvancedProtectionFeature( + AdvancedProtectionManager.FEATURE_ID_DISALLOW_INSTALL_UNKNOWN_SOURCES); + AdvancedProtectionHook hook = + new AdvancedProtectionHook(mContext, true) { + @NonNull + @Override + public AdvancedProtectionFeature getFeature() { + return feature1; + } - @Override - public boolean isAvailable() { - return false; - } - }; + @Override + public boolean isAvailable() { + return false; + } + }; - AdvancedProtectionProvider provider = new AdvancedProtectionProvider() { - @Override - public List<AdvancedProtectionFeature> getFeatures(Context context) { - return List.of(feature2); - } - }; + AdvancedProtectionProvider provider = + new AdvancedProtectionProvider() { + @Override + public List<AdvancedProtectionFeature> getFeatures(Context context) { + return List.of(feature2); + } + }; - mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(), - mPermissionEnforcer, hook, provider); - List<AdvancedProtectionFeature> features = mService.getAdvancedProtectionFeatures(); + AdvancedProtectionService service = createService(hook, provider); + List<AdvancedProtectionFeature> features = service.getAdvancedProtectionFeatures(); assertThat(features, containsInAnyOrder(feature2)); } - @Test public void testSetProtection_withoutPermission() { + AdvancedProtectionService service = createService(null, null); mPermissionEnforcer.revoke(Manifest.permission.MANAGE_ADVANCED_PROTECTION_MODE); - assertThrows(SecurityException.class, () -> mService.setAdvancedProtectionEnabled(true)); + assertThrows(SecurityException.class, () -> service.setAdvancedProtectionEnabled(true)); } @Test public void testGetProtection_withoutPermission() { + AdvancedProtectionService service = createService(null, null); mPermissionEnforcer.revoke(Manifest.permission.QUERY_ADVANCED_PROTECTION_MODE); - assertThrows(SecurityException.class, () -> mService.isAdvancedProtectionEnabled()); - } - - @Test - public void testUsbDataProtection_withoutPermission() { - mPermissionEnforcer.revoke(Manifest.permission.QUERY_ADVANCED_PROTECTION_MODE); - assertThrows(SecurityException.class, () -> mService.isUsbDataProtectionEnabled()); - } - - @Test - public void testSetUsbDataProtection_withoutPermission() { - mPermissionEnforcer.revoke(Manifest.permission.MANAGE_ADVANCED_PROTECTION_MODE); - assertThrows(SecurityException.class, () -> mService.setUsbDataProtectionEnabled(true)); + assertThrows(SecurityException.class, () -> service.isAdvancedProtectionEnabled()); } @Test public void testRegisterCallback_withoutPermission() { + AdvancedProtectionService service = createService(null, null); mPermissionEnforcer.revoke(Manifest.permission.QUERY_ADVANCED_PROTECTION_MODE); - assertThrows(SecurityException.class, () -> mService.registerAdvancedProtectionCallback( - new IAdvancedProtectionCallback.Default())); + assertThrows( + SecurityException.class, + () -> + service.registerAdvancedProtectionCallback( + new IAdvancedProtectionCallback.Default())); } @Test public void testUnregisterCallback_withoutPermission() { + AdvancedProtectionService service = createService(null, null); mPermissionEnforcer.revoke(Manifest.permission.QUERY_ADVANCED_PROTECTION_MODE); - assertThrows(SecurityException.class, () -> mService.unregisterAdvancedProtectionCallback( - new IAdvancedProtectionCallback.Default())); + assertThrows( + SecurityException.class, + () -> + service.unregisterAdvancedProtectionCallback( + new IAdvancedProtectionCallback.Default())); } } diff --git a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt index c59f0a05c619..02b97442b218 100644 --- a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt +++ b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt @@ -29,9 +29,15 @@ import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager import android.content.pm.UserInfo +import android.content.pm.UserInfo.FLAG_FOR_TESTING +import android.content.pm.UserInfo.FLAG_FULL +import android.content.pm.UserInfo.FLAG_MAIN +import android.content.pm.UserInfo.FLAG_SYSTEM import android.os.Handler import android.os.PersistableBundle import android.os.UserHandle +import android.os.UserHandle.MIN_SECONDARY_USER_ID +import android.os.UserHandle.USER_SYSTEM import android.platform.test.annotations.RequiresFlagsEnabled import android.platform.test.flag.junit.DeviceFlagsValueProvider import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -49,6 +55,7 @@ import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule +import org.mockito.kotlin.any import org.mockito.kotlin.whenever /** @@ -289,6 +296,36 @@ class SupervisionServiceTest { assertThat(service.createConfirmSupervisionCredentialsIntent()).isNull() } + fun shouldAllowBypassingSupervisionRoleQualification_returnsTrue() { + assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse() + assertThat(service.shouldAllowBypassingSupervisionRoleQualification()).isTrue() + + addDefaultAndTestUsers() + assertThat(service.shouldAllowBypassingSupervisionRoleQualification()).isTrue() + } + + @Test + fun shouldAllowBypassingSupervisionRoleQualification_returnsFalse() { + assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse() + assertThat(service.shouldAllowBypassingSupervisionRoleQualification()).isTrue() + + addDefaultAndTestUsers() + assertThat(service.shouldAllowBypassingSupervisionRoleQualification()).isTrue() + + // Enabling supervision on any user will disallow bypassing + service.setSupervisionEnabledForUser(USER_ID, true) + assertThat(service.isSupervisionEnabledForUser(USER_ID)).isTrue() + assertThat(service.shouldAllowBypassingSupervisionRoleQualification()).isFalse() + + // Adding non-default users should also disallow bypassing + addDefaultAndFullUsers() + assertThat(service.shouldAllowBypassingSupervisionRoleQualification()).isFalse() + + // Turning off supervision with non-default users should still disallow bypassing + service.setSupervisionEnabledForUser(USER_ID, false) + assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse() + } + private val systemSupervisionPackage: String get() = context.getResources().getString(R.string.config_systemSupervision) @@ -310,10 +347,31 @@ class SupervisionServiceTest { context.sendBroadcastAsUser(intent, UserHandle.of(userId)) } + private fun addDefaultAndTestUsers() { + val userInfos = userData.map { (userId, flags) -> + UserInfo(userId, "user" + userId, USER_ICON, flags, USER_TYPE) + } + whenever(mockUserManagerInternal.getUsers(any())).thenReturn(userInfos) + } + + private fun addDefaultAndFullUsers() { + val userInfos = userData.map { (userId, flags) -> + UserInfo(userId, "user" + userId, USER_ICON, flags, USER_TYPE) + } + UserInfo(USER_ID, "user" + USER_ID, USER_ICON, FLAG_FULL, USER_TYPE) + whenever(mockUserManagerInternal.getUsers(any())).thenReturn(userInfos) + } + private companion object { const val USER_ID = 100 const val APP_UID = USER_ID * UserHandle.PER_USER_RANGE const val SUPERVISING_USER_ID = 10 + const val USER_ICON = "user_icon" + const val USER_TYPE = "fake_user_type" + val userData: Map<Int, Int> = mapOf( + USER_SYSTEM to FLAG_SYSTEM, + MIN_SECONDARY_USER_ID to FLAG_MAIN, + (MIN_SECONDARY_USER_ID + 1) to (FLAG_FULL or FLAG_FOR_TESTING) + ) } } diff --git a/services/tests/servicestests/src/com/android/server/theming/FieldColorBothTests.java b/services/tests/servicestests/src/com/android/server/theming/FieldColorBothTests.java new file mode 100644 index 000000000000..38cbcf37f88c --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/FieldColorBothTests.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.theming.FieldColorBoth; +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; + +import com.google.common.truth.Truth; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class FieldColorBothTests { + static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321, + "home_wallpaper", ThemeStyle.VIBRANT, true); + private FieldColorBoth mFieldColorBoth; + + @Before + public void setup() { + mFieldColorBoth = new FieldColorBoth("colorBoth", ThemeSettingsUpdater::colorBoth, + ThemeSettings::colorBoth, DEFAULTS); + } + + @Test + public void parse_validColorBoth_returnsTrue() { + Boolean parsedValue = mFieldColorBoth.parse("1"); + assertThat(parsedValue).isTrue(); + } + + @Test + public void parse_validColorBoth_returnsFalse() { + Boolean parsedValue = mFieldColorBoth.parse("0"); + assertThat(parsedValue).isFalse(); + } + + @Test + public void parse_invalidColorBoth_returnsNull() { + Boolean parsedValue = mFieldColorBoth.parse("invalid"); + assertThat(parsedValue).isNull(); + } + + @Test + public void serialize_true_returnsTrueString() { + String serializedValue = mFieldColorBoth.serialize(true); + assertThat(serializedValue).isEqualTo("1"); + } + + @Test + public void serialize_false_returnsFalseString() { + String serializedValue = mFieldColorBoth.serialize(false); + assertThat(serializedValue).isEqualTo("0"); + } + + @Test + public void validate_true_returnsTrue() { + assertThat(mFieldColorBoth.validate(true)).isTrue(); + } + + @Test + public void validate_false_returnsTrue() { + assertThat(mFieldColorBoth.validate(false)).isTrue(); + } + + @Test + public void getFieldType_returnsBooleanClass() { + Truth.assertThat(mFieldColorBoth.getFieldType()).isEqualTo(Boolean.class); + } + + @Test + public void getJsonType_returnsStringClass() { + Truth.assertThat(mFieldColorBoth.getJsonType()).isEqualTo(String.class); + } + + @Test + public void get_returnsDefaultValue() { + Truth.assertThat(mFieldColorBoth.getDefaultValue()).isEqualTo(DEFAULTS.colorBoth()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/FieldColorIndexTests.java b/services/tests/servicestests/src/com/android/server/theming/FieldColorIndexTests.java new file mode 100644 index 000000000000..32df3684a81d --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/FieldColorIndexTests.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.theming.FieldColorIndex; +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class FieldColorIndexTests { + static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321, + "home_wallpaper", ThemeStyle.VIBRANT, true); + + private FieldColorIndex mFieldColorIndex; + + @Before + public void setup() { + mFieldColorIndex = new FieldColorIndex("colorIndex", ThemeSettingsUpdater::colorIndex, + ThemeSettings::colorIndex, DEFAULTS); + } + + @Test + public void parse_validColorIndex_returnsCorrectInteger() { + Integer parsedValue = mFieldColorIndex.parse("10"); + assertThat(parsedValue).isEqualTo(10); + } + + @Test + public void parse_negativeColorIndex_returnsCorrectInteger() { + Integer parsedValue = mFieldColorIndex.parse("-1"); + assertThat(parsedValue).isEqualTo(-1); + } + + @Test + public void parse_invalidColorIndex_returnsNull() { + Integer parsedValue = mFieldColorIndex.parse("invalid"); + assertThat(parsedValue).isNull(); + } + + @Test + public void serialize_validColorIndex_returnsCorrectString() { + String serializedValue = mFieldColorIndex.serialize(15); + assertThat(serializedValue).isEqualTo("15"); + } + + @Test + public void serialize_negativeColorIndex_returnsCorrectString() { + String serializedValue = mFieldColorIndex.serialize(-1); + assertThat(serializedValue).isEqualTo("-1"); + } + + @Test + public void validate_validColorIndex_returnsTrue() { + assertThat(mFieldColorIndex.validate(5)).isTrue(); + } + + @Test + public void validate_negativeColorIndex_returnsTrue() { + assertThat(mFieldColorIndex.validate(-1)).isTrue(); + } + + @Test + public void validate_invalidColorIndex_returnsFalse() { + assertThat(mFieldColorIndex.validate(-2)).isFalse(); + } + + @Test + public void getFieldType_returnsIntegerClass() { + assertThat(mFieldColorIndex.getFieldType()).isEqualTo(Integer.class); + } + + @Test + public void getJsonType_returnsStringClass() { + assertThat(mFieldColorIndex.getJsonType()).isEqualTo(String.class); + } + + @Test + public void get_returnsDefaultValue() { + assertThat(mFieldColorIndex.getDefaultValue()).isEqualTo(DEFAULTS.colorIndex()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/FieldColorSourceTests.java b/services/tests/servicestests/src/com/android/server/theming/FieldColorSourceTests.java new file mode 100644 index 000000000000..06edfa862d9c --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/FieldColorSourceTests.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.theming.FieldColorSource; +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; + +import com.google.common.truth.Truth; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + + +@RunWith(JUnit4.class) +public class FieldColorSourceTests { + static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321, + "home_wallpaper", ThemeStyle.VIBRANT, true); + private FieldColorSource mFieldColorSource; + + @Before + public void setup() { + mFieldColorSource = new FieldColorSource("colorSource", ThemeSettingsUpdater::colorSource, + ThemeSettings::colorSource, DEFAULTS); + } + + @Test + public void parse_validColorSource_returnsSameString() { + String validColorSource = "home_wallpaper"; + String parsedValue = mFieldColorSource.parse(validColorSource); + assertThat(parsedValue).isEqualTo(validColorSource); + } + + @Test + public void serialize_validColorSource_returnsSameString() { + String validColorSource = "lock_wallpaper"; + String serializedValue = mFieldColorSource.serialize(validColorSource); + assertThat(serializedValue).isEqualTo(validColorSource); + } + + @Test + public void validate_preset_returnsTrue() { + assertThat(mFieldColorSource.validate("preset")).isTrue(); + } + + @Test + public void validate_homeWallpaper_returnsTrue() { + assertThat(mFieldColorSource.validate("home_wallpaper")).isTrue(); + } + + @Test + public void validate_lockWallpaper_returnsTrue() { + assertThat(mFieldColorSource.validate("lock_wallpaper")).isTrue(); + } + + @Test + public void validate_invalidColorSource_returnsFalse() { + assertThat(mFieldColorSource.validate("invalid")).isFalse(); + } + + @Test + public void getFieldType_returnsStringClass() { + Truth.assertThat(mFieldColorSource.getFieldType()).isEqualTo(String.class); + } + + @Test + public void getJsonType_returnsStringClass() { + Truth.assertThat(mFieldColorSource.getJsonType()).isEqualTo(String.class); + } + + @Test + public void get_returnsDefaultValue() { + Truth.assertThat(mFieldColorSource.getDefaultValue()).isEqualTo(DEFAULTS.colorSource()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/FieldColorTests.java b/services/tests/servicestests/src/com/android/server/theming/FieldColorTests.java new file mode 100644 index 000000000000..54c4b29a5063 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/FieldColorTests.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.theming.FieldColor; +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; + +import com.google.common.truth.Truth; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class FieldColorTests { + static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321, + "home_wallpaper", ThemeStyle.VIBRANT, true); + + private FieldColor mFieldColor; + + @Before + public void setup() { + // Default to blue + mFieldColor = new FieldColor("accentColor", ThemeSettingsUpdater::accentColor, + ThemeSettings::accentColor, DEFAULTS); + } + + @Test + public void parse_validColor_returnsCorrectColor() { + Integer parsedValue = mFieldColor.parse("FF0000FF"); + assertThat(parsedValue).isEqualTo(0xFF0000FF); + } @Test + public void parse_validColorLowercase_returnsCorrectColor() { + Integer parsedValue = mFieldColor.parse("ff0000ff"); + assertThat(parsedValue).isEqualTo(0xFF0000FF); + } + + @Test + public void parse_validColorNoAlpha_returnsCorrectColor() { + Integer parsedValue = mFieldColor.parse("0000ff"); + assertThat(parsedValue).isEqualTo(0xFF0000FF); + } + + + @Test + public void parse_invalidColor_returnsNull() { + Integer parsedValue = mFieldColor.parse("invalid"); + assertThat(parsedValue).isNull(); + } + + @Test + public void parse_nullColor_returnsNull() { + Integer parsedValue = mFieldColor.parse(null); + assertThat(parsedValue).isNull(); + } + + @Test + public void serialize_validColor_returnsCorrectString() { + String serializedValue = mFieldColor.serialize(0xFFFF0000); // Red + assertThat(serializedValue).isEqualTo("ffff0000"); + } + + @Test + public void serialize_zeroColor_returnsZeroString() { + String serializedValue = mFieldColor.serialize(0); + assertThat(serializedValue).isEqualTo("0"); + } + + @Test + public void validate_validColor_returnsTrue() { + assertThat(mFieldColor.validate(0xFF00FF00)).isTrue(); // Green + } + + @Test + public void getFieldType_returnsIntegerClass() { + Truth.assertThat(mFieldColor.getFieldType()).isEqualTo(Integer.class); + } + + @Test + public void getJsonType_returnsStringClass() { + Truth.assertThat(mFieldColor.getJsonType()).isEqualTo(String.class); + } + + @Test + public void get_returnsDefaultValue() { + Truth.assertThat(mFieldColor.getDefaultValue()).isEqualTo(DEFAULTS.accentColor()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/FieldThemeStyleTests.java b/services/tests/servicestests/src/com/android/server/theming/FieldThemeStyleTests.java new file mode 100644 index 000000000000..09d71292fcf6 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/FieldThemeStyleTests.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.theming.FieldThemeStyle; +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class FieldThemeStyleTests { + static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321, + "home_wallpaper", ThemeStyle.VIBRANT, true); + + private FieldThemeStyle mFieldThemeStyle; + + @Before + public void setup() { + mFieldThemeStyle = new FieldThemeStyle("themeStyle", ThemeSettingsUpdater::themeStyle, + ThemeSettings::themeStyle, DEFAULTS); + } + + @Test + public void parse_validThemeStyle_returnsCorrectStyle() { + Integer parsedValue = mFieldThemeStyle.parse("EXPRESSIVE"); + assertThat(parsedValue).isEqualTo(ThemeStyle.EXPRESSIVE); + } + + @Test + public void parse_invalidThemeStyle_returnsNull() { + Integer parsedValue = mFieldThemeStyle.parse("INVALID"); + assertThat(parsedValue).isNull(); + } + + @Test + public void serialize_validThemeStyle_returnsCorrectString() { + String serializedValue = mFieldThemeStyle.serialize(ThemeStyle.SPRITZ); + assertThat(serializedValue).isEqualTo("SPRITZ"); + } + + @Test + public void validate_validThemeStyle_returnsTrue() { + assertThat(mFieldThemeStyle.validate(ThemeStyle.TONAL_SPOT)).isTrue(); + } + + @Test + public void validate_invalidThemeStyle_returnsFalse() { + assertThat(mFieldThemeStyle.validate(-1)).isFalse(); + } + + @Test + public void getFieldType_returnsIntegerClass() { + assertThat(mFieldThemeStyle.getFieldType()).isEqualTo(Integer.class); + } + + @Test + public void getJsonType_returnsStringClass() { + assertThat(mFieldThemeStyle.getJsonType()).isEqualTo(String.class); + } + + @Test + public void get_returnsDefaultValue() { + assertThat(mFieldThemeStyle.getDefaultValue()).isEqualTo(DEFAULTS.themeStyle()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/TEST_MAPPING b/services/tests/servicestests/src/com/android/server/theming/TEST_MAPPING new file mode 100644 index 000000000000..d8d73444f6ce --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "presubmit": [ + { + "name": "FrameworksServicesTests_theme" + } + ] +}
\ No newline at end of file diff --git a/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsFieldTests.java b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsFieldTests.java new file mode 100644 index 000000000000..0dc267a8059f --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsFieldTests.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsField; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +@RunWith(JUnit4.class) +public class ThemeSettingsFieldTests { + static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321, + "home_wallpaper", ThemeStyle.VIBRANT, true); + private ThemeSettingsUpdater mUpdater; + + @Before + public void setup() { + mUpdater = ThemeSettings.updater(); + } + + @Test + public void testFromJSON_validValue_setsValue() throws Exception { + TestThemeSettingsFieldInteger field = getSampleField(); + + JSONObject json = new JSONObject(); + json.put("testKey", "5"); + + field.fromJSON(json, mUpdater); + + assertThat(mUpdater.getColorIndex()).isEqualTo(5); + } + + @Test + public void testFromJSON_nullValue_setsDefault() throws Exception { + TestThemeSettingsFieldInteger field = getSampleField(); + + JSONObject json = new JSONObject(); + json.put("testKey", + JSONObject.NULL); // Using JSONObject.NULL is how you should indicate null in JSON + + field.fromJSON(json, mUpdater); + + assertThat(mUpdater.getColorIndex()).isEqualTo(DEFAULTS.colorIndex()); + } + + @Test + public void testFromJSON_invalidValue_setsDefault() throws Exception { + TestThemeSettingsFieldInteger field = getSampleField(); + + JSONObject json = new JSONObject(); + json.put("testKey", "abc"); // Invalid value + + field.fromJSON(json, mUpdater); + + assertThat(mUpdater.getColorIndex()).isEqualTo(DEFAULTS.colorIndex()); + } + + @Test + public void testToJSON_validValue_writesValue() throws JSONException { + TestThemeSettingsFieldInteger field = getSampleField(); + ThemeSettings settings = new ThemeSettings(10, 0xFF123456, 0xFF654321, "home_wallpaper", + 0, true); + JSONObject json = new JSONObject(); + + field.toJSON(settings, json); + + assertThat(json.getString("testKey")).isEqualTo("10"); + } + + @Test + public void testDefaultValue_returnsGetDefault() { + TestThemeSettingsFieldInteger field = getSampleField(); + + assertThat(field.getDefaultValue()).isEqualTo(DEFAULTS.colorIndex()); + } + + @Test + public void test_String_validValue_returnsParsedValue() throws JSONException { + TestThemeSettingsFieldInteger field = getSampleField(); + + JSONObject json = new JSONObject(); + json.put("testKey", "123"); + + field.fromJSON(json, mUpdater); + + assertThat(mUpdater.getColorIndex()).isEqualTo(123); + } + + @Test + public void test_String_invalidValue_returnsDefaultValue() throws JSONException { + TestThemeSettingsFieldInteger field = getSampleField(); + + JSONObject json = new JSONObject(); + // values < 0 are invalid + json.put("testKey", "-123"); + field.fromJSON(json, mUpdater); + + assertThat(mUpdater.getColorIndex()).isEqualTo(DEFAULTS.colorIndex()); + } + + private TestThemeSettingsFieldInteger getSampleField() { + return new TestThemeSettingsFieldInteger("testKey", ThemeSettingsUpdater::colorIndex, + ThemeSettings::colorIndex, DEFAULTS); + } + + + // Helper class for testing + private static class TestThemeSettingsFieldInteger extends ThemeSettingsField<Integer, String> { + TestThemeSettingsFieldInteger(String key, BiConsumer<ThemeSettingsUpdater, Integer> setter, + Function<ThemeSettings, Integer> getter, ThemeSettings defaults) { + super(key, setter, getter, defaults); + } + + @Override + public Integer parse(String primitive) { + try { + return Integer.parseInt(primitive); + } catch (NumberFormatException e) { + return null; + } + } + + @Override + public String serialize(Integer value) throws RuntimeException { + return value.toString(); + } + + @Override + public boolean validate(Integer value) { + return value > 0; + } + + @Override + public Class<Integer> getFieldType() { + return Integer.class; + } + + @Override + public Class<String> getJsonType() { + return String.class; + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsManagerTests.java b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsManagerTests.java new file mode 100644 index 000000000000..44f8c73dec84 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsManagerTests.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.theming; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.ContentResolver; +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeStyle; +import android.provider.Settings; +import android.testing.TestableContext; + +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ThemeSettingsManagerTests { + private final int mUserId = 0; + public static final ThemeSettings DEFAULTS = new ThemeSettings( + /* colorIndex= */ 1, + /* systemPalette= */ 0xFF123456, + /* accentColor= */ 0xFF654321, + /* colorSource= */ "home_wallpaper", + /* themeStyle= */ ThemeStyle.VIBRANT, + /* colorBoth= */ true); + + @Rule + public final TestableContext mContext = new TestableContext( + getInstrumentation().getTargetContext(), null); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + private ContentResolver mContentResolver; + + + @Before + public void setup() { + mContentResolver = mContext.getContentResolver(); + } + + @Test + public void loadSettings_emptyJSON_returnsDefault() { + Settings.Secure.putStringForUser(mContentResolver, + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, "{}", mUserId); + + ThemeSettingsManager manager = new ThemeSettingsManager(DEFAULTS); + ThemeSettings settings = manager.loadSettings(mUserId, mContentResolver); + + assertThat(settings.colorIndex()).isEqualTo(DEFAULTS.colorIndex()); + assertThat(settings.systemPalette()).isEqualTo(DEFAULTS.systemPalette()); + assertThat(settings.accentColor()).isEqualTo(DEFAULTS.accentColor()); + assertThat(settings.colorSource()).isEqualTo(DEFAULTS.colorSource()); + assertThat(settings.themeStyle()).isEqualTo(DEFAULTS.themeStyle()); + assertThat(settings.colorBoth()).isEqualTo(DEFAULTS.colorBoth()); + } + + @Test + public void replaceSettings_writesSettingsToProvider() throws Exception { + + ThemeSettingsManager manager = new ThemeSettingsManager(DEFAULTS); + + ThemeSettings newSettings = new ThemeSettings(3, 0xFF112233, 0xFF332211, "preset", + ThemeStyle.MONOCHROMATIC, false); + manager.replaceSettings(mUserId, mContentResolver, newSettings); + + String settingsString = Settings.Secure.getStringForUser(mContentResolver, + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, mUserId); + JSONObject settingsJson = new JSONObject(settingsString); + assertThat(settingsJson.getString("android.theme.customization.color_index")).isEqualTo( + "3"); + assertThat(settingsJson.getString("android.theme.customization.system_palette")) + .isEqualTo("ff112233"); + assertThat(settingsJson.getString("android.theme.customization.accent_color")) + .isEqualTo("ff332211"); + assertThat(settingsJson.getString("android.theme.customization.color_source")) + .isEqualTo("preset"); + assertThat(settingsJson.getString("android.theme.customization.theme_style")) + .isEqualTo("MONOCHROMATIC"); + assertThat(settingsJson.getString("android.theme.customization.color_both")).isEqualTo("0"); + } + + @Test + public void updatesSettings_writesSettingsToProvider() throws Exception { + ThemeSettingsManager manager = new ThemeSettingsManager(DEFAULTS); + + ThemeSettings newSettings = new ThemeSettings(3, 0xFF112233, 0xFF332211, "preset", + ThemeStyle.MONOCHROMATIC, false); + manager.updateSettings(mUserId, mContentResolver, newSettings); + + ThemeSettings loadedSettings = manager.loadSettings(mUserId, mContentResolver); + assertThat(loadedSettings.equals(newSettings)).isTrue(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsTests.java b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsTests.java new file mode 100644 index 000000000000..c417a4b571cb --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsTests.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertNull; + +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; +import android.os.Parcel; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class ThemeSettingsTests { + public static final ThemeSettings DEFAULTS = new ThemeSettings( + /* colorIndex= */ 1, + /* systemPalette= */ 0xFF123456, + /* accentColor= */ 0xFF654321, + /* colorSource= */ "home_wallpaper", + /* themeStyle= */ ThemeStyle.VIBRANT, + /* colorBoth= */ true); + + /** + * Test that the updater correctly sets all fields when they are provided. + */ + @Test + public void testUpdater_allFieldsSet() { + ThemeSettingsUpdater updater = ThemeSettings.updater() + .colorIndex(2) + .systemPalette(0xFFFF0000) + .accentColor(0xFF00FF00) + .colorSource("preset") + .themeStyle(ThemeStyle.MONOCHROMATIC) + .colorBoth(false); + + ThemeSettings settings = updater.toThemeSettings(DEFAULTS); + + assertThat(settings.colorIndex()).isEqualTo(2); + assertThat(settings.systemPalette()).isEqualTo(0xFFFF0000); + assertThat(settings.accentColor()).isEqualTo(0xFF00FF00); + assertThat(settings.colorSource()).isEqualTo("preset"); + assertThat(settings.themeStyle()).isEqualTo(ThemeStyle.MONOCHROMATIC); + assertThat(settings.colorBoth()).isEqualTo(false); + } + + /** + * Test that the updater uses null values when no fields are explicitly set. + */ + @Test + public void testUpdater_noFieldsSet() { + ThemeSettingsUpdater updater = ThemeSettings.updater(); + + assertNull(updater.getColorIndex()); + assertNull(updater.getSystemPalette()); + assertNull(updater.getAccentColor()); + assertNull(updater.getColorSource()); + assertNull(updater.getThemeStyle()); + assertNull(updater.getColorBoth()); + } + + /** + * Test that the ThemeSettings object can be correctly parceled and restored. + */ + @Test + public void testParcel_roundTrip() { + ThemeSettingsUpdater updater = ThemeSettings.updater() + .colorIndex(2) + .systemPalette(0xFFFF0000) + .accentColor(0xFF00FF00) + .colorSource("preset") + .themeStyle(ThemeStyle.MONOCHROMATIC) + .colorBoth(false); + + ThemeSettings settings = updater.toThemeSettings(DEFAULTS); + + Parcel parcel = Parcel.obtain(); + settings.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ThemeSettings fromParcel = ThemeSettings.CREATOR.createFromParcel(parcel); + + assertThat(settings.colorIndex()).isEqualTo(fromParcel.colorIndex()); + assertThat(settings.systemPalette()).isEqualTo(fromParcel.systemPalette()); + assertThat(settings.accentColor()).isEqualTo(fromParcel.accentColor()); + assertThat(settings.colorSource()).isEqualTo(fromParcel.colorSource()); + assertThat(settings.themeStyle()).isEqualTo(fromParcel.themeStyle()); + assertThat(settings.colorBoth()).isEqualTo(fromParcel.colorBoth()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsUpdaterTests.java b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsUpdaterTests.java new file mode 100644 index 000000000000..7ce32da7b713 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsUpdaterTests.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; +import android.os.Parcel; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class ThemeSettingsUpdaterTests { + public static final ThemeSettings DEFAULTS = new ThemeSettings( + /* colorIndex= */ 1, + /* systemPalette= */ 0xFF123456, + /* accentColor= */ 0xFF654321, + /* colorSource= */ "home_wallpaper", + /* themeStyle= */ ThemeStyle.VIBRANT, + /* colorBoth= */ true); + private ThemeSettingsUpdater mUpdater; + + @Before + public void setUp() { + mUpdater = ThemeSettings.updater(); + } + + @Test + public void testSetAndGetColorIndex() { + mUpdater.colorIndex(5); + assertThat(mUpdater.getColorIndex()).isEqualTo(5); + } + + @Test + public void testSetAndGetSystemPalette() { + mUpdater.systemPalette(0xFFABCDEF); + assertThat(mUpdater.getSystemPalette()).isEqualTo(0xFFABCDEF); + } + + @Test + public void testSetAndGetAccentColor() { + mUpdater.accentColor(0xFFFEDCBA); + assertThat(mUpdater.getAccentColor()).isEqualTo(0xFFFEDCBA); + } + + @Test + public void testSetAndGetColorSource() { + mUpdater.colorSource("lock_wallpaper"); + assertThat(mUpdater.getColorSource()).isEqualTo("lock_wallpaper"); + } + + @Test + public void testSetAndGetThemeStyle() { + mUpdater.themeStyle(ThemeStyle.EXPRESSIVE); + assertThat(mUpdater.getThemeStyle()).isEqualTo(ThemeStyle.EXPRESSIVE); + } + + @Test + public void testSetAndGetColorBoth() { + mUpdater.colorBoth(false); + assertThat(mUpdater.getColorBoth()).isFalse(); + } + + + @Test + public void testToThemeSettings_allFieldsSet() { + mUpdater.colorIndex(5) + .systemPalette(0xFFABCDEF) + .accentColor(0xFFFEDCBA) + .colorSource("lock_wallpaper") + .themeStyle(ThemeStyle.EXPRESSIVE) + .colorBoth(false); + ThemeSettings settings = mUpdater.toThemeSettings(DEFAULTS); + + assertThat(settings.colorIndex()).isEqualTo(5); + assertThat(settings.systemPalette()).isEqualTo(0xFFABCDEF); + assertThat(settings.accentColor()).isEqualTo(0xFFFEDCBA); + assertThat(settings.colorSource()).isEqualTo("lock_wallpaper"); + assertThat(settings.themeStyle()).isEqualTo(ThemeStyle.EXPRESSIVE); + assertThat(settings.colorBoth()).isFalse(); + } + + @Test + public void testToThemeSettings_someFieldsNotSet_usesDefaults() { + mUpdater.colorIndex(5) + .systemPalette(0xFFABCDEF); + + ThemeSettings settings = mUpdater.toThemeSettings(DEFAULTS); + + assertThat(settings.colorIndex()).isEqualTo(5); + assertThat(settings.systemPalette()).isEqualTo(0xFFABCDEF); + assertThat(settings.accentColor()).isEqualTo(DEFAULTS.accentColor()); + assertThat(settings.colorSource()).isEqualTo(DEFAULTS.colorSource()); + assertThat(settings.themeStyle()).isEqualTo(DEFAULTS.themeStyle()); + assertThat(settings.colorBoth()).isEqualTo(DEFAULTS.colorBoth()); + } + + @Test + public void testParcel_roundTrip_allFieldsSet() { + mUpdater.colorIndex(5) + .systemPalette(0xFFABCDEF) + .accentColor(0xFFFEDCBA) + .colorSource("lock_wallpaper") + .themeStyle(ThemeStyle.EXPRESSIVE) + .colorBoth(false); + + Parcel parcel = Parcel.obtain(); + mUpdater.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ThemeSettingsUpdater fromParcel = ThemeSettingsUpdater.CREATOR.createFromParcel(parcel); + + assertThat(fromParcel.getColorIndex()).isEqualTo(5); + assertThat(fromParcel.getSystemPalette()).isEqualTo(0xFFABCDEF); + assertThat(fromParcel.getAccentColor()).isEqualTo(0xFFFEDCBA); + assertThat(fromParcel.getColorSource()).isEqualTo("lock_wallpaper"); + assertThat(fromParcel.getThemeStyle()).isEqualTo(ThemeStyle.EXPRESSIVE); + assertThat(fromParcel.getColorBoth()).isFalse(); + } + + @Test + public void testParcel_roundTrip_noFieldsSet() { + Parcel parcel = Parcel.obtain(); + mUpdater.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ThemeSettingsUpdater fromParcel = ThemeSettingsUpdater.CREATOR.createFromParcel(parcel); + + assertThat(fromParcel.getColorIndex()).isNull(); + assertThat(fromParcel.getSystemPalette()).isNull(); + assertThat(fromParcel.getAccentColor()).isNull(); + assertThat(fromParcel.getColorSource()).isNull(); + assertThat(fromParcel.getThemeStyle()).isNull(); + assertThat(fromParcel.getColorBoth()).isNull(); + } +} 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 8c9b9bd03b9f..b9b4a78df0ca 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -24,6 +24,7 @@ import static android.app.ActivityManagerInternal.ServiceNotificationPolicy.NOT_ import static android.app.ActivityManagerInternal.ServiceNotificationPolicy.SHOW_IMMEDIATELY; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.Flags.FLAG_KEYGUARD_PRIVATE_NOTIFICATIONS; +import static android.app.Flags.FLAG_NM_SUMMARIZATION; import static android.app.Flags.FLAG_SORT_SECTION_BY_TIME; import static android.app.Notification.EXTRA_ALLOW_DURING_SETUP; import static android.app.Notification.EXTRA_PICTURE; @@ -105,6 +106,7 @@ import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE; import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; import static android.service.notification.Adjustment.KEY_CONTEXTUAL_ACTIONS; import static android.service.notification.Adjustment.KEY_IMPORTANCE; +import static android.service.notification.Adjustment.KEY_SUMMARIZATION; import static android.service.notification.Adjustment.KEY_TEXT_REPLIES; import static android.service.notification.Adjustment.KEY_TYPE; import static android.service.notification.Adjustment.KEY_USER_SENTIMENT; @@ -17680,6 +17682,58 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags({android.app.Flags.FLAG_API_RICH_ONGOING, + android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION, + android.app.Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI}) + public void testApplyAdjustment_promotedOngoingNotification_doesNotApply() throws Exception { + // promoted ongoing notification which should not have the adjustment applied + Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId()) + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG")) + .setColor(Color.WHITE) + .setColorized(true) + .setOngoing(true) + .setFlag(FLAG_PROMOTED_ONGOING, true) // add manually since we're skipping post + .setFlag(FLAG_CAN_COLORIZE, true) // add manually since we're skipping post + .build(); + + StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 9, null, mUid, 0, + n, UserHandle.getUserHandleForUid(mUid), null, 0); + final NotificationRecord r = new NotificationRecord(mContext, sbn, + mTestNotificationChannel); + + // regular notification record for contrast + final NotificationRecord r2 = generateNotificationRecord(mTestNotificationChannel); + + mService.addNotification(r); + mService.addNotification(r2); + NotificationManagerService.WorkerHandler handler = mock( + NotificationManagerService.WorkerHandler.class); + mService.setHandler(handler); + when(mAssistants.isAdjustmentKeyTypeAllowed(anyInt())).thenReturn(true); + when(mAssistants.isAdjustmentAllowedForPackage(anyString(), anyString())).thenReturn(true); + + Bundle signals = new Bundle(); + signals.putInt(KEY_TYPE, TYPE_NEWS); + Bundle signals2 = new Bundle(signals); // copy for the second adjustment + Adjustment adjustment = new Adjustment( + r.getSbn().getPackageName(), r.getKey(), signals, "", r.getUser().getIdentifier()); + Adjustment a2 = new Adjustment(r2.getSbn().getPackageName(), r2.getKey(), signals2, "", + r2.getUser().getIdentifier()); + when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true); + mBinderService.applyAdjustmentsFromAssistant(null, List.of(adjustment, a2)); + + waitForIdle(); + + r.applyAdjustments(); + r2.applyAdjustments(); + + // promoted ongoing notification does not get bundled; regular one does + assertThat(r.getChannel().getId()).isEqualTo(mTestNotificationChannel.getId()); + assertThat(r2.getChannel().getId()).isEqualTo(NEWS_ID); + } + + @Test @EnableFlags({android.app.Flags.FLAG_API_RICH_ONGOING}) public void testSetCanBePromoted_granted_noui() throws Exception { testSetCanBePromoted_granted(); @@ -18308,9 +18362,11 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // Post some notifications and classify in different bundles final int numNotifications = NotificationChannel.SYSTEM_RESERVED_IDS.size(); final int numNewsNotifications = 1; + List<String> postedNotificationKeys = new ArrayList(); for (int i = 0; i < numNotifications; i++) { NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, i, mUserId); mService.addNotification(r); + postedNotificationKeys.add(r.getKey()); Bundle signals = new Bundle(); final int adjustmentType = i + 1; signals.putInt(Adjustment.KEY_TYPE, adjustmentType); @@ -18330,7 +18386,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { waitForIdle(); //Check that all notifications classified as TYPE_NEWS have been unbundled - for (NotificationRecord record : mService.mNotificationList) { + for (String key : postedNotificationKeys) { + NotificationRecord record= mService.mNotificationsByKey.get(key); // Check that the original channel was restored // for notifications classified as TYPE_NEWS if (record.getBundleType() == TYPE_NEWS) { @@ -18355,7 +18412,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // Check that the bundle channel was restored verify(mRankingHandler, times(numNewsNotifications)).requestSort(); - for (NotificationRecord record : mService.mNotificationList) { + for (String key : postedNotificationKeys) { + NotificationRecord record= mService.mNotificationsByKey.get(key); assertThat(record.getChannel().getId()).isIn(NotificationChannel.SYSTEM_RESERVED_IDS); } } @@ -18425,6 +18483,36 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags({FLAG_NM_SUMMARIZATION}) + public void testDisableBundleAdjustmentByPkg_unsummarizesNotifications() throws Exception { + NotificationManagerService.WorkerHandler handler = mock( + NotificationManagerService.WorkerHandler.class); + mService.setHandler(handler); + when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true); + when(mAssistants.isServiceTokenValidLocked(any())).thenReturn(true); + when(mAssistants.isAdjustmentKeyTypeAllowed(anyInt())).thenReturn(true); + when(mAssistants.isAdjustmentAllowedForPackage(anyString(), anyString())).thenReturn(true); + + NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0, mUserId); + mService.addNotification(r); + Bundle signals = new Bundle(); + signals.putCharSequence(Adjustment.KEY_SUMMARIZATION, "hello"); + Adjustment adjustment = new Adjustment(r.getSbn().getPackageName(), r.getKey(), signals, + "", r.getUser().getIdentifier()); + mBinderService.applyAdjustmentFromAssistant(null, adjustment); + waitForIdle(); + r.applyAdjustments(); + Mockito.clearInvocations(mRankingHandler); + + // Disable summarization for package + mBinderService.setAdjustmentSupportedForPackage(KEY_SUMMARIZATION, mPkg, false); + verify(mRankingHandler).requestSort(); + mService.handleRankingSort(); + + assertThat(mService.mNotificationsByKey.get(r.getKey()).getSummarization()).isNull(); + } + + @Test @EnableFlags({FLAG_NOTIFICATION_CLASSIFICATION, FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION, @@ -18627,6 +18715,36 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags({FLAG_NM_SUMMARIZATION}) + public void testDisableBundleAdjustment_unsummarizesNotifications() throws Exception { + NotificationManagerService.WorkerHandler handler = mock( + NotificationManagerService.WorkerHandler.class); + mService.setHandler(handler); + when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true); + when(mAssistants.isServiceTokenValidLocked(any())).thenReturn(true); + when(mAssistants.isAdjustmentKeyTypeAllowed(anyInt())).thenReturn(true); + when(mAssistants.isAdjustmentAllowedForPackage(anyString(), anyString())).thenReturn(true); + + NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0, mUserId); + mService.addNotification(r); + Bundle signals = new Bundle(); + signals.putCharSequence(Adjustment.KEY_SUMMARIZATION, "hello"); + Adjustment adjustment = new Adjustment(r.getSbn().getPackageName(), r.getKey(), signals, + "", r.getUser().getIdentifier()); + mBinderService.applyAdjustmentFromAssistant(null, adjustment); + waitForIdle(); + r.applyAdjustments(); + Mockito.clearInvocations(mRankingHandler); + + // Disable summarization for package + mBinderService.disallowAssistantAdjustment(KEY_SUMMARIZATION); + verify(mRankingHandler).requestSort(); + mService.handleRankingSort(); + + assertThat(mService.mNotificationsByKey.get(r.getKey()).getSummarization()).isNull(); + } + + @Test @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void clearAll_fromUser_willSendDeleteIntentForCachedSummaries() throws Exception { NotificationRecord n = generateNotificationRecord( diff --git a/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java index 53e82bad818d..8d717bc19e72 100644 --- a/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java @@ -18,16 +18,12 @@ package com.android.server.policy; import static android.view.KeyEvent.KEYCODE_POWER; import static android.view.KeyEvent.KEYCODE_VOLUME_UP; -import static com.android.hardware.input.Flags.FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW; import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_ASSISTANT; import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_GLOBAL_ACTIONS; import static com.android.server.policy.PhoneWindowManager.POWER_MULTI_PRESS_TIMEOUT_MILLIS; import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_DREAM_OR_SLEEP; import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_GO_TO_SLEEP; -import static org.junit.Assert.assertEquals; - -import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.provider.Settings; import android.view.Display; @@ -153,143 +149,4 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { sendKey(KEYCODE_POWER); mPhoneWindowManager.assertNoPowerSleep(); } - - - /** - * Double press of power when the window handles the power key events. The - * system double power gesture launch should not be performed. - */ - @Test - @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public void testPowerDoublePress_windowHasOverridePermissionAndKeysHandled() { - mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); - setDispatchedKeyHandler(keyEvent -> true); - - sendKey(KEYCODE_POWER); - sendKey(KEYCODE_POWER); - - mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished(); - - mPhoneWindowManager.assertNoDoublePowerLaunch(); - } - - /** - * Double press of power when the window doesn't handle the power key events. - * The system default gesture launch should be performed and the app should receive both events. - */ - @Test - @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public void testPowerDoublePress_windowHasOverridePermissionAndKeysUnHandled() { - mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); - setDispatchedKeyHandler(keyEvent -> false); - - sendKey(KEYCODE_POWER); - sendKey(KEYCODE_POWER); - - mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished(); - mPhoneWindowManager.assertDoublePowerLaunch(); - assertEquals(getDownKeysDispatched(), 2); - assertEquals(getUpKeysDispatched(), 2); - } - - /** - * Triple press of power when the window handles the power key double press gesture. - * The system default gesture launch should not be performed, and the app only receives the - * first two presses. - */ - @Test - @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public void testPowerTriplePress_windowHasOverridePermissionAndKeysHandled() { - mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); - setDispatchedKeyHandler(keyEvent -> true); - - sendKey(KEYCODE_POWER); - sendKey(KEYCODE_POWER); - sendKey(KEYCODE_POWER); - - mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished(); - mPhoneWindowManager.assertNoDoublePowerLaunch(); - assertEquals(getDownKeysDispatched(), 2); - assertEquals(getUpKeysDispatched(), 2); - } - - /** - * Tests a single press, followed by a double press when the window can handle the power key. - * The app should receive all 3 events. - */ - @Test - @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public void testPowerTriplePressWithDelay_windowHasOverridePermissionAndKeysHandled() { - mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); - setDispatchedKeyHandler(keyEvent -> true); - - sendKey(KEYCODE_POWER); - mPhoneWindowManager.moveTimeForward(POWER_MULTI_PRESS_TIMEOUT_MILLIS); - sendKey(KEYCODE_POWER); - sendKey(KEYCODE_POWER); - - mPhoneWindowManager.assertNoDoublePowerLaunch(); - assertEquals(getDownKeysDispatched(), 3); - assertEquals(getUpKeysDispatched(), 3); - } - - /** - * Tests single press when window doesn't handle the power key. Phone should go to sleep. - */ - @Test - @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public void testPowerSinglePress_windowHasOverridePermissionAndKeyUnhandledByApp() { - mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); - setDispatchedKeyHandler(keyEvent -> false); - mPhoneWindowManager.overrideShortPressOnPower(SHORT_PRESS_POWER_GO_TO_SLEEP); - - sendKey(KEYCODE_POWER); - - mPhoneWindowManager.assertPowerSleep(); - } - - /** - * Tests single press when the window handles the power key. Phone should go to sleep after a - * delay of {POWER_MULTI_PRESS_TIMEOUT_MILLIS} - */ - @Test - @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public void testPowerSinglePress_windowHasOverridePermissionAndKeyHandledByApp() { - mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); - setDispatchedKeyHandler(keyEvent -> true); - mPhoneWindowManager.overrideDisplayState(Display.STATE_ON); - mPhoneWindowManager.overrideShortPressOnPower(SHORT_PRESS_POWER_GO_TO_SLEEP); - - sendKey(KEYCODE_POWER); - - mPhoneWindowManager.moveTimeForward(POWER_MULTI_PRESS_TIMEOUT_MILLIS); - - mPhoneWindowManager.assertPowerSleep(); - } - - - /** - * Tests 5x press when the window handles the power key. Emergency gesture should still be - * launched. - */ - @Test - @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public void testPowerFiveTimesPress_windowHasOverridePermissionAndKeyHandledByApp() { - mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); - setDispatchedKeyHandler(keyEvent -> true); - mPhoneWindowManager.overrideDisplayState(Display.STATE_ON); - mPhoneWindowManager.overrideShortPressOnPower(SHORT_PRESS_POWER_GO_TO_SLEEP); - - int minEmergencyGestureDurationMillis = mContext.getResources().getInteger( - com.android.internal.R.integer.config_defaultMinEmergencyGestureTapDurationMillis); - int durationMillis = minEmergencyGestureDurationMillis / 4; - for (int i = 0; i < 5; ++i) { - sendKey(KEYCODE_POWER); - mPhoneWindowManager.moveTimeForward(durationMillis); - } - - mPhoneWindowManager.assertEmergencyLaunch(); - assertEquals(getDownKeysDispatched(), 2); - assertEquals(getUpKeysDispatched(), 2); - } } diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index 7059c41898f3..2097d15658a6 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -37,7 +37,6 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; -import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow; import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_ASSISTANT; import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_GLOBAL_ACTIONS; import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_GO_TO_VOICE_ASSIST; @@ -210,8 +209,6 @@ class TestPhoneWindowManager { private int mKeyEventPolicyFlags = FLAG_INTERACTIVE; - private int mProcessPowerKeyDownCount = 0; - private class TestTalkbackShortcutController extends TalkbackShortcutController { TestTalkbackShortcutController(Context context) { super(context); @@ -424,7 +421,7 @@ class TestPhoneWindowManager { doNothing().when(mContext).startActivityAsUser(any(), any()); doNothing().when(mContext).startActivityAsUser(any(), any(), any()); - KeyInterceptionInfo interceptionInfo = new KeyInterceptionInfo(0, 0, null, 0, 0); + KeyInterceptionInfo interceptionInfo = new KeyInterceptionInfo(0, 0, null, 0); doReturn(interceptionInfo) .when(mWindowManagerInternal).getKeyInterceptionInfoFromToken(any()); @@ -442,9 +439,6 @@ class TestPhoneWindowManager { eq(TEST_BROWSER_ROLE_PACKAGE_NAME)); doReturn(mSmsIntent).when(mPackageManager).getLaunchIntentForPackage( eq(TEST_SMS_ROLE_PACKAGE_NAME)); - mProcessPowerKeyDownCount = 0; - captureProcessPowerKeyDownCount(); - Mockito.reset(mContext); } @@ -715,12 +709,6 @@ class TestPhoneWindowManager { .when(mButtonOverridePermissionChecker).canAppOverrideSystemKey(any(), anyInt()); } - void overrideCanWindowOverridePowerKey(boolean granted) { - doReturn(granted) - .when(mButtonOverridePermissionChecker).canWindowOverridePowerKey(any(), anyInt(), - anyInt()); - } - void overrideKeyEventPolicyFlags(int flags) { mKeyEventPolicyFlags = flags; } @@ -800,10 +788,6 @@ class TestPhoneWindowManager { verify(mGestureLauncherService, atMost(4)) .interceptPowerKeyDown(any(), anyBoolean(), valueCaptor.capture()); - if (overridePowerKeyBehaviorInFocusedWindow()) { - assertTrue(mProcessPowerKeyDownCount >= 2 && mProcessPowerKeyDownCount <= 4); - } - List<Boolean> capturedValues = valueCaptor.getAllValues().stream() .map(mutableBoolean -> mutableBoolean.value) .toList(); @@ -832,10 +816,6 @@ class TestPhoneWindowManager { verify(mGestureLauncherService, atLeast(1)) .interceptPowerKeyDown(any(), anyBoolean(), valueCaptor.capture()); - if (overridePowerKeyBehaviorInFocusedWindow()) { - assertEquals(mProcessPowerKeyDownCount, 5); - } - List<Boolean> capturedValues = valueCaptor.getAllValues().stream() .map(mutableBoolean -> mutableBoolean.value) .toList(); @@ -1063,12 +1043,4 @@ class TestPhoneWindowManager { verify(mContext, never()).startActivityAsUser(any(), any(), any()); verify(mContext, never()).startActivityAsUser(any(), any()); } - - private void captureProcessPowerKeyDownCount() { - doAnswer((Answer<Void>) invocation -> { - invocation.callRealMethod(); - mProcessPowerKeyDownCount++; - return null; - }).when(mGestureLauncherService).processPowerKeyDown(any()); - } } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java index 1cb1e3cae413..ec264034871a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java @@ -25,6 +25,7 @@ import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.server.display.feature.flags.Flags.FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT; import static com.android.server.wm.ActivityInterceptorCallback.MAINLINE_FIRST_ORDERED_ID; import static com.android.server.wm.ActivityInterceptorCallback.SYSTEM_FIRST_ORDERED_ID; import static com.android.server.wm.ActivityInterceptorCallback.SYSTEM_LAST_ORDERED_ID; @@ -67,6 +68,7 @@ import android.os.LocaleList; import android.os.PowerManagerInternal; import android.os.RemoteException; import android.os.UserHandle; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.view.Display; import android.view.DisplayInfo; @@ -75,6 +77,8 @@ import android.view.WindowManager; import androidx.test.filters.MediumTest; +import com.android.server.UiThread; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -164,11 +168,13 @@ public class ActivityTaskManagerServiceTests extends WindowTestsBase { verify(mClientLifecycleManager, never()).scheduleTransactionItem(any(), any()); } + @EnableFlags(FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT) @Test public void testDisplayWindowListener() { final ArrayList<Integer> added = new ArrayList<>(); final ArrayList<Integer> changed = new ArrayList<>(); final ArrayList<Integer> removed = new ArrayList<>(); + final ArrayList<Integer> desktopModeEligibleChanged = new ArrayList<>(); IDisplayWindowListener listener = new IDisplayWindowListener.Stub() { @Override public void onDisplayAdded(int displayId) { @@ -194,6 +200,11 @@ public class ActivityTaskManagerServiceTests extends WindowTestsBase { @Override public void onKeepClearAreasChanged(int displayId, List<Rect> restricted, List<Rect> unrestricted) {} + + @Override + public void onDesktopModeEligibleChanged(int displayId) { + desktopModeEligibleChanged.add(displayId); + } }; int[] displayIds = mAtm.mWindowManager.registerDisplayWindowListener(listener); for (int i = 0; i < displayIds.length; i++) { @@ -218,7 +229,25 @@ public class ActivityTaskManagerServiceTests extends WindowTestsBase { assertEquals(1, changed.size()); assertEquals(0, removed.size()); changed.clear(); + + // Check adding decoration + doReturn(true).when(newDisp1).allowContentModeSwitch(); + doReturn(true).when(newDisp1).isSystemDecorationsSupported(); + mAtm.mWindowManager.setShouldShowSystemDecors(newDisp1.mDisplayId, true); + waitHandlerIdle(UiThread.getHandler()); + assertEquals(1, desktopModeEligibleChanged.size()); + assertEquals(newDisp1.mDisplayId, (int) desktopModeEligibleChanged.get(0)); + desktopModeEligibleChanged.clear(); + // Check removing decoration + doReturn(false).when(newDisp1).isSystemDecorationsSupported(); + mAtm.mWindowManager.setShouldShowSystemDecors(newDisp1.mDisplayId, false); + waitHandlerIdle(UiThread.getHandler()); + assertEquals(1, desktopModeEligibleChanged.size()); + assertEquals(newDisp1.mDisplayId, (int) desktopModeEligibleChanged.get(0)); + desktopModeEligibleChanged.clear(); + // Check that removal is reported + changed.clear(); newDisp1.remove(); assertEquals(0, added.size()); assertEquals(0, changed.size()); diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java index f587d6e8c346..678230564b25 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java @@ -21,7 +21,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_MEDIUM_VALUE; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_SMALL_VALUE; @@ -282,6 +282,7 @@ public class DesktopModeLaunchParamsModifierTests extends final DisplayContent dc = spy(createNewDisplay()); final Task existingFreeformTask = new TaskBuilder(mSupervisor).setCreateActivity(true) .setWindowingMode(WINDOWING_MODE_FREEFORM).setPackage(packageName).build(); + existingFreeformTask.topRunningActivity().launchMode = LAUNCH_SINGLE_INSTANCE; existingFreeformTask.setBounds( /* left */ 0, /* top */ 0, @@ -293,8 +294,8 @@ public class DesktopModeLaunchParamsModifierTests extends // so first instance will close. final Task launchingTask = new TaskBuilder(mSupervisor).setPackage(packageName) .setCreateActivity(true).build(); + launchingTask.topRunningActivity().launchMode = LAUNCH_SINGLE_INSTANCE; launchingTask.onDisplayChanged(dc); - launchingTask.intent.addFlags(FLAG_ACTIVITY_NEW_TASK); // New instance should inherit task bounds of old instance. assertEquals(RESULT_DONE, 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 ed00a9e8e74b..b7c325878a78 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -2983,9 +2983,45 @@ public class DisplayContentTests extends WindowTestsBase { assertTrue(dc.mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(dc)); } + @EnableFlags(FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT) + @Test + public void testRemove_displayWithSystemDecorations_emitRemoveSystemDecorations() { + final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); + displayInfo.displayId = DEFAULT_DISPLAY + 1; + displayInfo.flags = (FLAG_ALLOWS_CONTENT_MODE_SWITCH | FLAG_TRUSTED); + final DisplayContent dc = createNewDisplay(displayInfo); + spyOn(dc.mDisplay); + doReturn(true).when(dc.mDisplay).canHostTasks(); + dc.onDisplayInfoChangeApplied(); + final DisplayPolicy displayPolicy = dc.getDisplayPolicy(); + spyOn(displayPolicy); + + dc.remove(); + + verify(displayPolicy).notifyDisplayRemoveSystemDecorations(); + } + + @EnableFlags(FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT) + @Test + public void testRemove_displayWithoutSystemDecorations_dontEmitRemoveSystemDecorations() { + final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); + displayInfo.displayId = DEFAULT_DISPLAY + 1; + displayInfo.flags = (FLAG_ALLOWS_CONTENT_MODE_SWITCH | FLAG_TRUSTED); + final DisplayContent dc = createNewDisplay(displayInfo); + spyOn(dc.mDisplay); + doReturn(false).when(dc.mDisplay).canHostTasks(); + dc.onDisplayInfoChangeApplied(); + final DisplayPolicy displayPolicy = dc.getDisplayPolicy(); + spyOn(displayPolicy); + + dc.remove(); + + verify(displayPolicy, never()).notifyDisplayRemoveSystemDecorations(); + } + @EnableFlags(FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS) @Test - public void testForcedDensityRatioSetForExternalDisplays_persistDensityScaleFlagEnabled() { + public void testForcedDensityRatioSet_persistDensityScaleFlagEnabled() { final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); displayInfo.displayId = DEFAULT_DISPLAY + 1; displayInfo.type = Display.TYPE_EXTERNAL; @@ -3003,19 +3039,20 @@ public class DisplayContentTests extends WindowTestsBase { baseYDpi); final int forcedDensity = 640; - - // Verify that forcing the density is honored and the size doesn't change. - displayContent.setForcedDensity(forcedDensity, 0 /* userId */); - verifySizes(displayContent, baseWidth, baseHeight, forcedDensity); + displayContent.setForcedDensityRatio( + (float) forcedDensity / baseDensity, 0 /* userId */); // Verify that density ratio is set correctly. assertEquals((float) forcedDensity / baseDensity, - displayContent.mExternalDisplayForcedDensityRatio, 0.01); + displayContent.mForcedDisplayDensityRatio, 0.01); + // Verify that density is set correctly. + assertEquals(forcedDensity, + displayContent.mBaseDisplayDensity); } @EnableFlags(FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS) @Test - public void testForcedDensityUpdateForExternalDisplays_persistDensityScaleFlagEnabled() { + public void testForcedDensityUpdateWithRatio_persistDensityScaleFlagEnabled() { final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); displayInfo.displayId = DEFAULT_DISPLAY + 1; displayInfo.type = Display.TYPE_EXTERNAL; @@ -3033,14 +3070,12 @@ public class DisplayContentTests extends WindowTestsBase { baseYDpi); final int forcedDensity = 640; - - // Verify that forcing the density is honored and the size doesn't change. - displayContent.setForcedDensity(forcedDensity, 0 /* userId */); - verifySizes(displayContent, baseWidth, baseHeight, forcedDensity); + displayContent.setForcedDensityRatio( + (float) forcedDensity / baseDensity, 0 /* userId */); // Verify that density ratio is set correctly. - assertEquals((float) 2.0f, - displayContent.mExternalDisplayForcedDensityRatio, 0.001); + assertEquals(2.0f, + displayContent.mForcedDisplayDensityRatio, 0.001); displayContent.mInitialDisplayDensity = 160; diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java index 449ca867b987..9ab20d15acc8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java @@ -280,18 +280,24 @@ public class DisplayWindowSettingsTests extends WindowTestsBase { @EnableFlags(Flags.FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS) @Test public void testSetForcedDensityRatio() { - mDisplayWindowSettings.setForcedDensity(mSecondaryDisplay.getDisplayInfo(), - 300 /* density */, 0 /* userId */); + DisplayInfo info = new DisplayInfo(mDisplayInfo); + info.logicalDensityDpi = 300; + info.type = Display.TYPE_EXTERNAL; + mSecondaryDisplay = createNewDisplay(info); mDisplayWindowSettings.setForcedDensityRatio(mSecondaryDisplay.getDisplayInfo(), 2.0f /* ratio */); mDisplayWindowSettings.applySettingsToDisplayLocked(mSecondaryDisplay); - assertEquals(mSecondaryDisplay.mInitialDisplayDensity * 2.0f, - mSecondaryDisplay.mBaseDisplayDensity, 0.01); + assertEquals((int) (mSecondaryDisplay.mInitialDisplayDensity * 2.0f), + mSecondaryDisplay.mBaseDisplayDensity); + + mWm.clearForcedDisplayDensityForUser(mSecondaryDisplay.getDisplayId(), + 0 /* userId */); - mWm.clearForcedDisplayDensityForUser(mSecondaryDisplay.getDisplayId(), 0 /* userId */); assertEquals(mSecondaryDisplay.mInitialDisplayDensity, mSecondaryDisplay.mBaseDisplayDensity); + assertEquals(mSecondaryDisplay.mForcedDisplayDensityRatio, + 0.0f, 0.001); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java index 2c6884e7a35a..4458b7330a68 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java @@ -81,6 +81,7 @@ import android.window.TaskSnapshot; import androidx.test.filters.MediumTest; import com.android.server.wm.RecentTasks.Callbacks; +import com.android.window.flags.Flags; import org.junit.Before; import org.junit.Rule; @@ -931,6 +932,20 @@ public class RecentTasksTest extends WindowTestsBase { } @Test + public void testVisibleTask_forceExcludedFromRecents() { + final Task forceExcludedFromRecentsTask = mTasks.getFirst(); + forceExcludedFromRecentsTask.setForceExcludedFromRecents(true); + + final boolean visible = mRecentTasks.isVisibleRecentTask(forceExcludedFromRecentsTask); + + if (Flags.excludeTaskFromRecents()) { + assertFalse(visible); + } else { + assertTrue(visible); + } + } + + @Test public void testFreezeTaskListOrder_reorderExistingTask() { // Add some tasks mRecentTasks.add(mTasks.get(0)); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java index b617f0285606..e57f1144e6e9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java @@ -86,6 +86,7 @@ import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; import android.os.IBinder; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.util.DisplayMetrics; import android.util.Xml; @@ -99,6 +100,7 @@ import androidx.test.filters.MediumTest; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import com.android.window.flags.Flags; import org.junit.Assert; import org.junit.Before; @@ -2161,6 +2163,36 @@ public class TaskTests extends WindowTestsBase { } + @Test + public void testIsForceExcludedFromRecents_defaultFalse() { + final Task task = createTask(mDisplayContent); + assertFalse(task.isForceExcludedFromRecents()); + } + + @Test + public void testSetForceExcludedFromRecents() { + final Task task = createTask(mDisplayContent); + + task.setForceExcludedFromRecents(true); + + if (Flags.excludeTaskFromRecents()) { + assertTrue(task.isForceExcludedFromRecents()); + } else { + assertFalse(task.isForceExcludedFromRecents()); + } + } + + @Test + @EnableFlags(Flags.FLAG_EXCLUDE_TASK_FROM_RECENTS) + public void testSetForceExcludedFromRecents_resetsTaskForceExcludedFromRecents() { + final Task task = createTask(mDisplayContent); + task.setForceExcludedFromRecents(true); + + task.setForceExcludedFromRecents(false); + + assertFalse(task.isForceExcludedFromRecents()); + } + private Task getTestTask() { return new TaskBuilder(mSupervisor).setCreateActivity(true).build(); } 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 73102c4478d8..7836ca7d1b4d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -29,7 +29,6 @@ import static android.view.Display.FLAG_OWN_FOCUS; import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.FLAG_SECURE; -import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY; import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE; @@ -49,7 +48,6 @@ 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; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; -import static com.android.hardware.input.Flags.FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW; import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND; import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; @@ -100,6 +98,8 @@ import android.provider.Settings; import android.util.ArraySet; import android.util.MergedConfiguration; import android.view.ContentRecordingSession; +import android.view.Display; +import android.view.DisplayInfo; import android.view.IWindow; import android.view.InputChannel; import android.view.InputDevice; @@ -1154,53 +1154,6 @@ public class WindowManagerServiceTests extends WindowTestsBase { } @Test - @RequiresFlagsEnabled(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public void testUpdateInputChannel_sanitizeWithoutPermission_ThrowsError() { - final Session session = mock(Session.class); - final int callingUid = Process.FIRST_APPLICATION_UID; - final int callingPid = 1234; - final SurfaceControl surfaceControl = mock(SurfaceControl.class); - final IBinder window = new Binder(); - final InputTransferToken inputTransferToken = mock(InputTransferToken.class); - - - final InputChannel inputChannel = new InputChannel(); - - assertThrows(IllegalArgumentException.class, () -> - mWm.grantInputChannel(session, callingUid, callingPid, DEFAULT_DISPLAY, - surfaceControl, window, null /* hostInputToken */, FLAG_NOT_FOCUSABLE, - 0 /* privateFlags */, - INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS, - TYPE_APPLICATION, null /* windowToken */, inputTransferToken, - "TestInputChannel", inputChannel)); - } - - - @Test - @RequiresFlagsEnabled(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public void testUpdateInputChannel_sanitizeWithPermission_doesNotThrowError() { - final Session session = mock(Session.class); - final int callingUid = Process.FIRST_APPLICATION_UID; - final int callingPid = 1234; - final SurfaceControl surfaceControl = mock(SurfaceControl.class); - final IBinder window = new Binder(); - final InputTransferToken inputTransferToken = mock(InputTransferToken.class); - - doReturn(PackageManager.PERMISSION_GRANTED).when(mWm.mContext).checkPermission( - android.Manifest.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW, - callingPid, - callingUid); - - final InputChannel inputChannel = new InputChannel(); - - mWm.grantInputChannel(session, callingUid, callingPid, DEFAULT_DISPLAY, surfaceControl, - window, null /* hostInputToken */, FLAG_NOT_FOCUSABLE, 0 /* privateFlags */, - INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS, - TYPE_APPLICATION, null /* windowToken */, inputTransferToken, "TestInputChannel", - inputChannel); - } - - @Test public void testUpdateInputChannel_allowSpyWindowForInputMonitorPermission() { final Session session = mock(Session.class); final int callingUid = Process.SYSTEM_UID; @@ -1486,6 +1439,46 @@ public class WindowManagerServiceTests extends WindowTestsBase { } @Test + @EnableFlags(Flags.FLAG_FIX_HIDE_OVERLAY_API) + public void testUpdateOverlayWindows_multipleWindowsFromSameUid_idempotent() { + // Deny INTERNAL_SYSTEM_WINDOW permission for WindowSession so that the saw isn't allowed to + // show despite hideNonSystemOverlayWindows. + doReturn(PackageManager.PERMISSION_DENIED).when(mWm.mContext).checkPermission( + eq(android.Manifest.permission.INTERNAL_SYSTEM_WINDOW), anyInt(), anyInt()); + + WindowState saw = + newWindowBuilder("saw", TYPE_APPLICATION_OVERLAY).setOwnerId(10123).build(); + saw.mWinAnimator.mDrawState = WindowStateAnimator.HAS_DRAWN; + saw.mWinAnimator.mSurfaceControl = mock(SurfaceControl.class); + assertThat(saw.mSession.mCanAddInternalSystemWindow).isFalse(); + + WindowState app1 = newWindowBuilder("app1", TYPE_APPLICATION).setOwnerId(10456).build(); + spyOn(app1); + doReturn(true).when(app1).hideNonSystemOverlayWindowsWhenVisible(); + + WindowState app2 = newWindowBuilder("app2", TYPE_APPLICATION).setOwnerId(10456).build(); + spyOn(app2); + doReturn(true).when(app2).hideNonSystemOverlayWindowsWhenVisible(); + + makeWindowVisible(saw, app1, app2); + assertThat(saw.isVisibleByPolicy()).isTrue(); + + // Two hideNonSystemOverlayWindows windows: SAW is hidden. + mWm.updateNonSystemOverlayWindowsVisibilityIfNeeded(app1, true); + mWm.updateNonSystemOverlayWindowsVisibilityIfNeeded(app2, true); + assertThat(saw.isVisibleByPolicy()).isFalse(); + + // Marking the same window hidden twice: SAW is still hidden. + mWm.updateNonSystemOverlayWindowsVisibilityIfNeeded(app1, false); + mWm.updateNonSystemOverlayWindowsVisibilityIfNeeded(app1, false); + assertThat(saw.isVisibleByPolicy()).isFalse(); + + // Marking the remaining window hidden: SAW can be shown again. + mWm.updateNonSystemOverlayWindowsVisibilityIfNeeded(app2, false); + assertThat(saw.isVisibleByPolicy()).isTrue(); + } + + @Test @EnableFlags(Flags.FLAG_REPARENT_WINDOW_TOKEN_API) public void reparentWindowContextToDisplayArea_newDisplay_reparented() { final WindowToken windowToken = createTestClientWindowToken(TYPE_NOTIFICATION_SHADE, @@ -1603,6 +1596,60 @@ public class WindowManagerServiceTests extends WindowTestsBase { }); } + @Test + @EnableFlags(Flags.FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS) + public void setForcedDisplayDensityRatio_forExternalDisplay_setsRatio() { + final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); + displayInfo.displayId = DEFAULT_DISPLAY + 1; + displayInfo.type = Display.TYPE_EXTERNAL; + displayInfo.logicalDensityDpi = 100; + mDisplayContent = createNewDisplay(displayInfo); + final int currentUserId = ActivityManager.getCurrentUser(); + final float forcedDensityRatio = 2f; + + mWm.setForcedDisplayDensityRatio(displayInfo.displayId, forcedDensityRatio, + currentUserId); + + verify(mDisplayContent).setForcedDensityRatio(forcedDensityRatio, + currentUserId); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS) + public void setForcedDisplayDensityRatio_forInternalDisplay_setsRatio() { + final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); + displayInfo.displayId = DEFAULT_DISPLAY + 1; + displayInfo.type = Display.TYPE_INTERNAL; + mDisplayContent = createNewDisplay(displayInfo); + final int currentUserId = ActivityManager.getCurrentUser(); + final float forcedDensityRatio = 2f; + + mWm.setForcedDisplayDensityRatio(displayInfo.displayId, forcedDensityRatio, + currentUserId); + + verify(mDisplayContent).setForcedDensityRatio(forcedDensityRatio, + currentUserId); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS) + public void clearForcedDisplayDensityRatio_clearsRatioAndDensity() { + final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); + displayInfo.displayId = DEFAULT_DISPLAY + 1; + displayInfo.type = Display.TYPE_INTERNAL; + mDisplayContent = createNewDisplay(displayInfo); + final int currentUserId = ActivityManager.getCurrentUser(); + + mWm.clearForcedDisplayDensityForUser(displayInfo.displayId, currentUserId); + + verify(mDisplayContent).setForcedDensityRatio(0.0f, + currentUserId); + + assertEquals(mDisplayContent.mBaseDisplayDensity, + mDisplayContent.getInitialDisplayDensity()); + assertEquals(mDisplayContent.mForcedDisplayDensityRatio, 0.0f, 0.001); + } + /** * Simulates IPC transfer by writing the setting to a parcel and reading it back. * diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java index 5624677779a2..fa77e42611e1 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -562,26 +562,6 @@ public class WindowStateTests extends WindowTestsBase { } @Test - public void testDeferredRemovalByAnimating() { - final WindowState appWindow = newWindowBuilder("appWindow", TYPE_APPLICATION).build(); - makeWindowVisible(appWindow); - spyOn(appWindow.mWinAnimator); - doReturn(true).when(appWindow.mWinAnimator).getShown(); - final AnimationAdapter animation = mock(AnimationAdapter.class); - final ActivityRecord activity = appWindow.mActivityRecord; - activity.startAnimation(appWindow.getPendingTransaction(), - animation, false /* hidden */, SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION); - - appWindow.removeIfPossible(); - assertTrue(appWindow.mAnimatingExit); - assertFalse(appWindow.mRemoved); - - activity.cancelAnimation(); - assertFalse(appWindow.mAnimatingExit); - assertTrue(appWindow.mRemoved); - } - - @Test public void testOnExitAnimationDone() { final WindowState parent = newWindowBuilder("parent", TYPE_APPLICATION).build(); final WindowState child = newWindowBuilder("child", TYPE_APPLICATION_PANEL).setParent( diff --git a/services/usage/java/com/android/server/usage/StorageStatsService.java b/services/usage/java/com/android/server/usage/StorageStatsService.java index e9da53a8a899..5097231b468f 100644 --- a/services/usage/java/com/android/server/usage/StorageStatsService.java +++ b/services/usage/java/com/android/server/usage/StorageStatsService.java @@ -352,7 +352,53 @@ public class StorageStatsService extends IStorageStatsManager.Stub { } @Override - public StorageStats queryStatsForPackage(String volumeUuid, String packageName, int userId, + public StorageStats queryArtManagedStats(String packageName, int userId, int uid) { + if (userId != UserHandle.getCallingUserId()) { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.INTERACT_ACROSS_USERS, TAG); + } + + ApplicationInfo appInfo; + if (!TextUtils.isEmpty(packageName)) { + try { + appInfo = mPackage.getApplicationInfoAsUser(packageName, + PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); + } catch (NameNotFoundException e) { + throw new ParcelableException(e); + } + uid = appInfo.uid; + if (defeatNullable(mPackage.getPackagesForUid(appInfo.uid)).length > 1) { + // Multiple packages, skip + return translate(new PackageStats(TAG)); + } + } + + int callingUid = Binder.getCallingUid(); + String callingPackage = mPackage.getNameForUid(callingUid); + if (Binder.getCallingUid() != uid) { + enforceStatsPermission(Binder.getCallingUid(), callingPackage); + } + + final String[] packageNames = defeatNullable(mPackage.getPackagesForUid(uid)); + final PackageStats stats = new PackageStats(TAG); + for (int i = 0; i < packageNames.length; i++) { + try { + appInfo = mPackage.getApplicationInfoAsUser(packageNames[i], + PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); + if (appInfo.isSystemApp() && !appInfo.isUpdatedSystemApp()) { + // We don't count code baked into system image + } else { + computeAppArtStats(stats, packageNames[i]); + } + } catch (NameNotFoundException e) { + throw new ParcelableException(e); + } + } + return translate(stats); + } + + @Override + public StorageStats queryStatsForPackage(String volumeUuid, String packageName, int userId, String callingPackage) { if (userId != UserHandle.getCallingUserId()) { mContext.enforceCallingOrSelfPermission( @@ -378,9 +424,10 @@ public class StorageStatsService extends IStorageStatsManager.Stub { callerHasStatsPermission = true; } + StorageStats storageStats; if (defeatNullable(mPackage.getPackagesForUid(appInfo.uid)).length == 1) { // Only one package inside UID means we can fast-path - return queryStatsForUid(volumeUuid, appInfo.uid, callingPackage); + storageStats = queryStatsForUid(volumeUuid, appInfo.uid, callingPackage); } else { // Multiple packages means we need to go manual final int appId = UserHandle.getAppId(appInfo.uid); @@ -411,12 +458,16 @@ public class StorageStatsService extends IStorageStatsManager.Stub { packageName, userHandle, callerHasStatsPermission); }, "queryStatsForPackage"); } - return translate(stats); + storageStats = translate(stats); } + storageStats.packageName = packageName; + storageStats.userHandle = userId; + return storageStats; } @Override - public StorageStats queryStatsForUid(String volumeUuid, int uid, String callingPackage) { + public StorageStats queryStatsForUid(String volumeUuid, int uid, + String callingPackage) { final int userId = UserHandle.getUserId(uid); final int appId = UserHandle.getAppId(uid); @@ -481,7 +532,10 @@ public class StorageStatsService extends IStorageStatsManager.Stub { storageStatsAugmenter.augmentStatsForUid(stats, uid, callerHasStatsPermission); }, "queryStatsForUid"); } - return translate(stats); + StorageStats storageStats = translate(stats); + storageStats.userHandle = userId; + storageStats.uid = uid; + return storageStats; } @Override @@ -592,8 +646,9 @@ public class StorageStatsService extends IStorageStatsManager.Stub { } } - private static StorageStats translate(PackageStats stats) { + private StorageStats translate(PackageStats stats) { final StorageStats res = new StorageStats(); + res.userHandle = stats.userHandle; res.codeBytes = stats.codeSize + stats.externalCodeSize; res.dataBytes = stats.dataSize + stats.externalDataSize; res.cacheBytes = stats.cacheSize + stats.externalCacheSize; @@ -967,6 +1022,12 @@ public class StorageStatsService extends IStorageStatsManager.Stub { stats.dmSize += getFileBytesInDir(srcDir, ".dm"); stats.libSize += getDirBytes(new File(sourceDirName + "/lib/")); + if (!Flags.getAppArtManagedBytes()) { + computeAppArtStats(stats, packageName); + } + } + + private void computeAppArtStats(PackageStats stats, String packageName) { // Get dexopt, current profle and reference profile sizes. ArtManagedFileStats artManagedFileStats; try (var snapshot = getPackageManagerLocal().withFilteredSnapshot()) { diff --git a/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig b/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig index dfbd74c1f3e1..5628c75cf8ed 100644 --- a/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig +++ b/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig @@ -28,3 +28,10 @@ flag { description: "This flag maximizes the usb audio volume when it is connected" bug: "245041322" } + +flag { + name: "wait_for_alsa_scan_results_if_has_audio_interface" + namespace: "usb" + description: "This flag waits for alsa scan results for audio device" + bug: "378826805" +} diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java index ebe00782319a..68216b2dbd8a 100644 --- a/telecomm/java/android/telecom/Connection.java +++ b/telecomm/java/android/telecom/Connection.java @@ -912,6 +912,16 @@ public abstract class Connection extends Conferenceable { public static final String EVENT_CALL_HOLD_FAILED = "android.telecom.event.CALL_HOLD_FAILED"; /** + * Connection event used to inform Telecom when a resume operation on a call has failed. + * <p> + * Sent via {@link #sendConnectionEvent(String, Bundle)}. The {@link Bundle} parameter is + * expected to be null when this connection event is used. + */ + @FlaggedApi(Flags.FLAG_CALL_SEQUENCING_CALL_RESUME_FAILED) + public static final String EVENT_CALL_RESUME_FAILED = + "android.telecom.event.CALL_RESUME_FAILED"; + + /** * Connection event used to inform Telecom when a switch operation on a call has failed. * <p> * Sent via {@link #sendConnectionEvent(String, Bundle)}. The {@link Bundle} parameter is diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index d7f80a94081a..2095ee83b77d 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -3135,6 +3135,16 @@ interface ITelephony { boolean setSatelliteIgnoreCellularServiceState(in boolean enabled); /** + * This API can be used by only CTS to control the feature + * {@code config_support_disable_satellite_while_enable_in_progress}. + * + * @param reset Whether to reset the override. + * @param supported Whether to support the feature. + * @return {@code true} if the value is set successfully, {@code false} otherwise. + */ + boolean setSupportDisableSatelliteWhileEnableInProgress(boolean reset, boolean supported); + + /** * This API can be used by only CTS to update satellite pointing UI app package and class names. * * @param packageName The package name of the satellite pointing UI app. diff --git a/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java b/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java index 229c7bfb53e9..9c176cfe45e5 100644 --- a/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java +++ b/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java @@ -60,6 +60,7 @@ import java.util.HashMap; @RunWith(AndroidJUnit4.class) public class IntegrationTests { public static final int WAIT_FOR_TIMEOUT_MS = 5000; + public static final int WAIT_FOR_PENDING_JANKSTATS_MS = 1000; @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); @@ -116,18 +117,8 @@ public class IntegrationTests { editText.reportAppJankStats(JankUtils.getAppJankStats()); - // reportAppJankStats performs the work on a background thread, check periodically to see - // if the work is complete. - for (int i = 0; i < 10; i++) { - try { - Thread.sleep(100); - if (jankTracker.getPendingJankStats().size() > 0) { - break; - } - } catch (InterruptedException exception) { - //do nothing and continue - } - } + // wait until pending results are available. + JankUtils.waitForResults(jankTracker, WAIT_FOR_PENDING_JANKSTATS_MS); pendingStats = jankTracker.getPendingJankStats(); @@ -222,4 +213,36 @@ public class IntegrationTests { assertTrue(jankTracker.shouldTrack()); } + + /* + When JankTracker is first instantiated it gets passed the apps UID the same UID should be + passed when reporting AppJankStats. To make sure frames and metrics are all associated with + the same app these UIDs need to match. This test confirms that mismatched IDs are not + counted. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void reportJankStats_statNotMerged_onMisMatchedAppIds() { + Activity jankTrackerActivity = mJankTrackerActivityRule.launchActivity(null); + mDevice.wait(Until.findObject( + By.text(jankTrackerActivity.getString(R.string.continue_test))), + WAIT_FOR_TIMEOUT_MS); + + EditText editText = jankTrackerActivity.findViewById(R.id.edit_text); + JankTracker jankTracker = editText.getJankTracker(); + + HashMap<String, JankDataProcessor.PendingJankStat> pendingStats = + jankTracker.getPendingJankStats(); + assertEquals(0, pendingStats.size()); + + int mismatchedAppUID = 25; + editText.reportAppJankStats(JankUtils.getAppJankStats(mismatchedAppUID)); + + // wait until pending results should be available. + JankUtils.waitForResults(jankTracker, WAIT_FOR_PENDING_JANKSTATS_MS); + + pendingStats = jankTracker.getPendingJankStats(); + + assertEquals(0, pendingStats.size()); + } } diff --git a/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java b/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java index 9640a84eb9ca..302cad11bbb9 100644 --- a/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java +++ b/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java @@ -17,17 +17,20 @@ package android.app.jank.tests; import android.app.jank.AppJankStats; +import android.app.jank.JankTracker; import android.app.jank.RelativeFrameTimeHistogram; +import android.os.Process; + public class JankUtils { - private static final int APP_ID = 25; + private static final int APP_ID = Process.myUid(); /** * Returns a mock AppJankStats object to be used in tests. */ - public static AppJankStats getAppJankStats() { + public static AppJankStats getAppJankStats(int appUID) { AppJankStats jankStats = new AppJankStats( - /*App Uid*/APP_ID, + /*App Uid*/appUID, /*Widget Id*/"test widget id", /*navigationComponent*/null, /*Widget Category*/AppJankStats.WIDGET_CATEGORY_SCROLL, @@ -39,6 +42,10 @@ public class JankUtils { return jankStats; } + public static AppJankStats getAppJankStats() { + return getAppJankStats(APP_ID); + } + /** * Returns a mock histogram to be used with an AppJankStats object. */ @@ -50,4 +57,26 @@ public class JankUtils { overrunHistogram.addRelativeFrameTimeMillis(25); return overrunHistogram; } + + /** + * When JankStats are reported they are processed on a background thread. This method checks + * every 100 ms up to the maxWaitTime to see if the pending stat count is greater than zero. + * If the pending stat count is greater than zero it will return or keep trying until + * maxWaitTime has elapsed. + */ + public static void waitForResults(JankTracker jankTracker, int maxWaitTimeMs) { + int currentWaitTimeMs = 0; + int threadSleepTimeMs = 100; + while (currentWaitTimeMs < maxWaitTimeMs) { + try { + Thread.sleep(threadSleepTimeMs); + if (!jankTracker.getPendingJankStats().isEmpty()) { + return; + } + currentWaitTimeMs += threadSleepTimeMs; + } catch (InterruptedException exception) { + // do nothing and continue. + } + } + } } diff --git a/tests/Input/src/android/hardware/input/InputDeviceBatteryListenerTest.kt b/tests/Input/src/android/hardware/input/InputDeviceBatteryListenerTest.kt index a1e165551b5b..c58287760d0e 100644 --- a/tests/Input/src/android/hardware/input/InputDeviceBatteryListenerTest.kt +++ b/tests/Input/src/android/hardware/input/InputDeviceBatteryListenerTest.kt @@ -44,14 +44,12 @@ import org.mockito.junit.MockitoJUnitRunner /** * Tests for [InputManager.InputDeviceBatteryListener]. * - * Build/Install/Run: - * atest InputTests:InputDeviceBatteryListenerTest + * Build/Install/Run: atest InputTests:InputDeviceBatteryListenerTest */ @Presubmit @RunWith(MockitoJUnitRunner::class) class InputDeviceBatteryListenerTest { - @get:Rule - val rule = MockitoJUnit.rule()!! + @get:Rule val rule = MockitoJUnit.rule()!! private lateinit var testLooper: TestLooper private var registeredListener: IInputDeviceBatteryListener? = null @@ -60,8 +58,7 @@ class InputDeviceBatteryListenerTest { private lateinit var context: Context private lateinit var inputManager: InputManager - @get:Rule - val inputManagerRule = MockInputManagerRule() + @get:Rule val inputManagerRule = MockInputManagerRule() @Before fun setUp() { @@ -71,41 +68,48 @@ class InputDeviceBatteryListenerTest { registeredListener = null monitoredDevices.clear() inputManager = InputManager(context) - `when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) - .thenReturn(inputManager) + `when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))).thenReturn(inputManager) // Handle battery listener registration. doAnswer { - val deviceId = it.getArgument(0) as Int - val listener = it.getArgument(1) as IInputDeviceBatteryListener - if (registeredListener != null && - registeredListener!!.asBinder() != listener.asBinder()) { - // There can only be one registered battery listener per process. - fail("Trying to register a new listener when one already exists") + val deviceId = it.getArgument(0) as Int + val listener = it.getArgument(1) as IInputDeviceBatteryListener + if ( + registeredListener != null && + registeredListener!!.asBinder() != listener.asBinder() + ) { + // There can only be one registered battery listener per process. + fail("Trying to register a new listener when one already exists") + } + if (monitoredDevices.contains(deviceId)) { + fail("Trying to start monitoring a device that was already being monitored") + } + monitoredDevices.add(deviceId) + registeredListener = listener + null } - if (monitoredDevices.contains(deviceId)) { - fail("Trying to start monitoring a device that was already being monitored") - } - monitoredDevices.add(deviceId) - registeredListener = listener - null - }.`when`(inputManagerRule.mock).registerBatteryListener(anyInt(), any()) + .`when`(inputManagerRule.mock) + .registerBatteryListener(anyInt(), any()) // Handle battery listener being unregistered. doAnswer { - val deviceId = it.getArgument(0) as Int - val listener = it.getArgument(1) as IInputDeviceBatteryListener - if (registeredListener == null || - registeredListener!!.asBinder() != listener.asBinder()) { - fail("Trying to unregister a listener that is not registered") - } - if (!monitoredDevices.remove(deviceId)) { - fail("Trying to stop monitoring a device that is not being monitored") + val deviceId = it.getArgument(0) as Int + val listener = it.getArgument(1) as IInputDeviceBatteryListener + if ( + registeredListener == null || + registeredListener!!.asBinder() != listener.asBinder() + ) { + fail("Trying to unregister a listener that is not registered") + } + if (!monitoredDevices.remove(deviceId)) { + fail("Trying to stop monitoring a device that is not being monitored") + } + if (monitoredDevices.isEmpty()) { + registeredListener = null + } } - if (monitoredDevices.isEmpty()) { - registeredListener = null - } - }.`when`(inputManagerRule.mock).unregisterBatteryListener(anyInt(), any()) + .`when`(inputManagerRule.mock) + .unregisterBatteryListener(anyInt(), any()) } private fun notifyBatteryStateChanged( @@ -113,15 +117,17 @@ class InputDeviceBatteryListenerTest { isPresent: Boolean = true, status: Int = BatteryState.STATUS_FULL, capacity: Float = 1.0f, - eventTime: Long = 12345L + eventTime: Long = 12345L, ) { - registeredListener!!.onBatteryStateChanged(IInputDeviceBatteryState().apply { - this.deviceId = deviceId - this.updateTime = eventTime - this.isPresent = isPresent - this.status = status - this.capacity = capacity - }) + registeredListener!!.onBatteryStateChanged( + IInputDeviceBatteryState().apply { + this.deviceId = deviceId + this.updateTime = eventTime + this.isPresent = isPresent + this.status = status + this.capacity = capacity + } + ) } @Test @@ -130,7 +136,9 @@ class InputDeviceBatteryListenerTest { // Add a battery listener to monitor battery changes. inputManager.addInputDeviceBatteryListener(1 /*deviceId*/, executor) { - deviceId: Int, eventTime: Long, batteryState: BatteryState -> + deviceId: Int, + eventTime: Long, + batteryState: BatteryState -> callbackCount++ assertEquals(1, deviceId) assertEquals(true, batteryState.isPresent) @@ -149,8 +157,13 @@ class InputDeviceBatteryListenerTest { assertEquals(0, callbackCount) // Notifying battery change for the registered device will notify the listener. - notifyBatteryStateChanged(1 /*deviceId*/, true /*isPresent*/, - BatteryState.STATUS_DISCHARGING, 0.5f /*capacity*/, 8675309L /*eventTime*/) + notifyBatteryStateChanged( + 1 /*deviceId*/, + true /*isPresent*/, + BatteryState.STATUS_DISCHARGING, + 0.5f /*capacity*/, + 8675309L, /*eventTime*/ + ) testLooper.dispatchNext() assertEquals(1, callbackCount) } diff --git a/tests/Input/src/android/hardware/input/InputManagerTest.kt b/tests/Input/src/android/hardware/input/InputManagerTest.kt index 4c6bb849155c..e6bec077d9d4 100644 --- a/tests/Input/src/android/hardware/input/InputManagerTest.kt +++ b/tests/Input/src/android/hardware/input/InputManagerTest.kt @@ -38,8 +38,7 @@ import org.mockito.junit.MockitoJUnitRunner /** * Tests for [InputManager]. * - * Build/Install/Run: - * atest InputTests:InputManagerTest + * Build/Install/Run: atest InputTests:InputManagerTest */ @Presubmit @RunWith(MockitoJUnitRunner::class) @@ -51,8 +50,7 @@ class InputManagerTest { const val THIRD_DEVICE_ID = 99 } - @get:Rule - val inputManagerRule = MockInputManagerRule() + @get:Rule val inputManagerRule = MockInputManagerRule() private lateinit var devicesChangedListener: IInputDevicesChangedListener private val deviceGenerationMap = mutableMapOf<Int /*deviceId*/, Int /*generation*/>() @@ -64,9 +62,7 @@ class InputManagerTest { context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) inputManager = InputManager(context) `when`(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager) - `when`(inputManagerRule.mock.inputDeviceIds).then { - deviceGenerationMap.keys.toIntArray() - } + `when`(inputManagerRule.mock.inputDeviceIds).then { deviceGenerationMap.keys.toIntArray() } } private fun notifyDeviceChanged( @@ -74,8 +70,9 @@ class InputManagerTest { associatedDisplayId: Int, usiVersion: HostUsiVersion?, ) { - val generation = deviceGenerationMap[deviceId]?.plus(1) - ?: throw IllegalArgumentException("Device $deviceId was never added!") + val generation = + deviceGenerationMap[deviceId]?.plus(1) + ?: throw IllegalArgumentException("Device $deviceId was never added!") deviceGenerationMap[deviceId] = generation `when`(inputManagerRule.mock.getInputDevice(deviceId)) diff --git a/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt index c62bd0b72584..7f7d4590c322 100644 --- a/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt +++ b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt @@ -23,6 +23,11 @@ import android.platform.test.flag.junit.SetFlagsRule import android.view.KeyEvent import androidx.test.core.app.ApplicationProvider import com.android.test.input.MockInputManagerRule +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.fail +import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Rule import org.junit.Test @@ -31,17 +36,11 @@ 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.fail -import org.junit.Assert.assertThrows /** * Tests for [InputManager.KeyGestureEventHandler]. * - * Build/Install/Run: - * atest InputTests:KeyGestureEventHandlerTest + * Build/Install/Run: atest InputTests:KeyGestureEventHandlerTest */ @Presubmit @RunWith(MockitoJUnitRunner::class) @@ -49,24 +48,24 @@ class KeyGestureEventHandlerTest { companion object { const val DEVICE_ID = 1 - val HOME_GESTURE_EVENT = KeyGestureEvent.Builder() - .setDeviceId(DEVICE_ID) - .setKeycodes(intArrayOf(KeyEvent.KEYCODE_H)) - .setModifierState(KeyEvent.META_META_ON or KeyEvent.META_META_LEFT_ON) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) - .build() - val BACK_GESTURE_EVENT = KeyGestureEvent.Builder() - .setDeviceId(DEVICE_ID) - .setKeycodes(intArrayOf(KeyEvent.KEYCODE_DEL)) - .setModifierState(KeyEvent.META_META_ON or KeyEvent.META_META_LEFT_ON) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) - .build() + val HOME_GESTURE_EVENT = + KeyGestureEvent.Builder() + .setDeviceId(DEVICE_ID) + .setKeycodes(intArrayOf(KeyEvent.KEYCODE_H)) + .setModifierState(KeyEvent.META_META_ON or KeyEvent.META_META_LEFT_ON) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() + val BACK_GESTURE_EVENT = + KeyGestureEvent.Builder() + .setDeviceId(DEVICE_ID) + .setKeycodes(intArrayOf(KeyEvent.KEYCODE_DEL)) + .setModifierState(KeyEvent.META_META_ON or KeyEvent.META_META_LEFT_ON) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) + .build() } - @get:Rule - val rule = SetFlagsRule() - @get:Rule - val inputManagerRule = MockInputManagerRule() + @get:Rule val rule = SetFlagsRule() + @get:Rule val inputManagerRule = MockInputManagerRule() private var registeredListener: IKeyGestureHandler? = null private lateinit var context: Context @@ -76,31 +75,38 @@ class KeyGestureEventHandlerTest { fun setUp() { context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) inputManager = InputManager(context) - `when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) - .thenReturn(inputManager) + `when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))).thenReturn(inputManager) // Handle key gesture handler registration. doAnswer { - val listener = it.getArgument(1) as IKeyGestureHandler - if (registeredListener != null && - registeredListener!!.asBinder() != listener.asBinder()) { - // There can only be one registered key gesture handler per process. - fail("Trying to register a new listener when one already exists") + val listener = it.getArgument(1) as IKeyGestureHandler + if ( + registeredListener != null && + registeredListener!!.asBinder() != listener.asBinder() + ) { + // There can only be one registered key gesture handler per process. + fail("Trying to register a new listener when one already exists") + } + registeredListener = listener + null } - registeredListener = listener - null - }.`when`(inputManagerRule.mock).registerKeyGestureHandler(Mockito.any(), Mockito.any()) + .`when`(inputManagerRule.mock) + .registerKeyGestureHandler(Mockito.any(), Mockito.any()) // Handle key gesture handler being unregistered. doAnswer { - val listener = it.getArgument(0) as IKeyGestureHandler - if (registeredListener == null || - registeredListener!!.asBinder() != listener.asBinder()) { - fail("Trying to unregister a listener that is not registered") + val listener = it.getArgument(0) as IKeyGestureHandler + if ( + registeredListener == null || + registeredListener!!.asBinder() != listener.asBinder() + ) { + fail("Trying to unregister a listener that is not registered") + } + registeredListener = null + null } - registeredListener = null - null - }.`when`(inputManagerRule.mock).unregisterKeyGestureHandler(Mockito.any()) + .`when`(inputManagerRule.mock) + .unregisterKeyGestureHandler(Mockito.any()) } private fun handleKeyGestureEvent(event: KeyGestureEvent) { @@ -143,7 +149,7 @@ class KeyGestureEventHandlerTest { // Adding the handler should register the callback with InputManagerService. inputManager.registerKeyGestureEventHandler( listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME), - callback1 + callback1, ) assertNotNull(registeredListener) @@ -151,7 +157,7 @@ class KeyGestureEventHandlerTest { val currListener = registeredListener inputManager.registerKeyGestureEventHandler( listOf(KeyGestureEvent.KEY_GESTURE_TYPE_BACK), - callback2 + callback2, ) assertEquals(currListener, registeredListener) } @@ -164,11 +170,11 @@ class KeyGestureEventHandlerTest { inputManager.registerKeyGestureEventHandler( listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME), - callback1 + callback1, ) inputManager.registerKeyGestureEventHandler( listOf(KeyGestureEvent.KEY_GESTURE_TYPE_BACK), - callback2 + callback2, ) // Only removing all handlers should remove the internal callback @@ -184,24 +190,26 @@ class KeyGestureEventHandlerTest { var callbackCount1 = 0 var callbackCount2 = 0 // Handler 1 captures all home gestures - val callback1 = InputManager.KeyGestureEventHandler { event, _ -> - callbackCount1++ - assertEquals(KeyGestureEvent.KEY_GESTURE_TYPE_HOME, event.keyGestureType) - } + val callback1 = + InputManager.KeyGestureEventHandler { event, _ -> + callbackCount1++ + assertEquals(KeyGestureEvent.KEY_GESTURE_TYPE_HOME, event.keyGestureType) + } // Handler 2 captures all back gestures - val callback2 = InputManager.KeyGestureEventHandler { event, _ -> - callbackCount2++ - assertEquals(KeyGestureEvent.KEY_GESTURE_TYPE_BACK, event.keyGestureType) - } + val callback2 = + InputManager.KeyGestureEventHandler { event, _ -> + callbackCount2++ + assertEquals(KeyGestureEvent.KEY_GESTURE_TYPE_BACK, event.keyGestureType) + } // Add both key gesture event handlers inputManager.registerKeyGestureEventHandler( listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME), - callback1 + callback1, ) inputManager.registerKeyGestureEventHandler( listOf(KeyGestureEvent.KEY_GESTURE_TYPE_BACK), - callback2 + callback2, ) // Request handling for home key gesture event, should notify only callback1 @@ -228,12 +236,13 @@ class KeyGestureEventHandlerTest { inputManager.registerKeyGestureEventHandler( listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME), - handler + handler, ) assertThrows(IllegalArgumentException::class.java) { inputManager.registerKeyGestureEventHandler( - listOf(KeyGestureEvent.KEY_GESTURE_TYPE_BACK), handler + listOf(KeyGestureEvent.KEY_GESTURE_TYPE_BACK), + handler, ) } } @@ -245,12 +254,13 @@ class KeyGestureEventHandlerTest { inputManager.registerKeyGestureEventHandler( listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME), - handler1 + handler1, ) assertThrows(IllegalArgumentException::class.java) { inputManager.registerKeyGestureEventHandler( - listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME), handler2 + listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME), + handler2, ) } } diff --git a/tests/Input/src/android/hardware/input/KeyGestureEventListenerTest.kt b/tests/Input/src/android/hardware/input/KeyGestureEventListenerTest.kt index cf0bfcc4f6df..bf3a9c389c7e 100644 --- a/tests/Input/src/android/hardware/input/KeyGestureEventListenerTest.kt +++ b/tests/Input/src/android/hardware/input/KeyGestureEventListenerTest.kt @@ -43,8 +43,7 @@ import org.mockito.junit.MockitoJUnitRunner /** * Tests for [InputManager.KeyGestureEventListener]. * - * Build/Install/Run: - * atest InputTests:KeyGestureEventListenerTest + * Build/Install/Run: atest InputTests:KeyGestureEventListenerTest */ @Presubmit @RunWith(MockitoJUnitRunner::class) @@ -52,18 +51,17 @@ class KeyGestureEventListenerTest { companion object { const val DEVICE_ID = 1 - val HOME_GESTURE_EVENT = KeyGestureEvent.Builder() - .setDeviceId(DEVICE_ID) - .setKeycodes(intArrayOf(KeyEvent.KEYCODE_H)) - .setModifierState(KeyEvent.META_META_ON or KeyEvent.META_META_LEFT_ON) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) - .build() + val HOME_GESTURE_EVENT = + KeyGestureEvent.Builder() + .setDeviceId(DEVICE_ID) + .setKeycodes(intArrayOf(KeyEvent.KEYCODE_H)) + .setModifierState(KeyEvent.META_META_ON or KeyEvent.META_META_LEFT_ON) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() } - @get:Rule - val rule = SetFlagsRule() - @get:Rule - val inputManagerRule = MockInputManagerRule() + @get:Rule val rule = SetFlagsRule() + @get:Rule val inputManagerRule = MockInputManagerRule() private val testLooper = TestLooper() private val executor = HandlerExecutor(Handler(testLooper.looper)) @@ -75,31 +73,38 @@ class KeyGestureEventListenerTest { fun setUp() { context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) inputManager = InputManager(context) - `when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) - .thenReturn(inputManager) + `when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))).thenReturn(inputManager) // Handle key gesture event listener registration. doAnswer { - val listener = it.getArgument(0) as IKeyGestureEventListener - if (registeredListener != null && - registeredListener!!.asBinder() != listener.asBinder()) { - // There can only be one registered key gesture event listener per process. - fail("Trying to register a new listener when one already exists") + val listener = it.getArgument(0) as IKeyGestureEventListener + if ( + registeredListener != null && + registeredListener!!.asBinder() != listener.asBinder() + ) { + // There can only be one registered key gesture event listener per process. + fail("Trying to register a new listener when one already exists") + } + registeredListener = listener + null } - registeredListener = listener - null - }.`when`(inputManagerRule.mock).registerKeyGestureEventListener(any()) + .`when`(inputManagerRule.mock) + .registerKeyGestureEventListener(any()) // Handle key gesture event listener being unregistered. doAnswer { - val listener = it.getArgument(0) as IKeyGestureEventListener - if (registeredListener == null || - registeredListener!!.asBinder() != listener.asBinder()) { - fail("Trying to unregister a listener that is not registered") + val listener = it.getArgument(0) as IKeyGestureEventListener + if ( + registeredListener == null || + registeredListener!!.asBinder() != listener.asBinder() + ) { + fail("Trying to unregister a listener that is not registered") + } + registeredListener = null + null } - registeredListener = null - null - }.`when`(inputManagerRule.mock).unregisterKeyGestureEventListener(any()) + .`when`(inputManagerRule.mock) + .unregisterKeyGestureEventListener(any()) } private fun notifyKeyGestureEvent(event: KeyGestureEvent) { @@ -119,8 +124,7 @@ class KeyGestureEventListenerTest { var callbackCount = 0 // Add a key gesture event listener - inputManager.registerKeyGestureEventListener(executor) { - event: KeyGestureEvent -> + inputManager.registerKeyGestureEventListener(executor) { event: KeyGestureEvent -> assertEquals(HOME_GESTURE_EVENT, event) callbackCount++ } diff --git a/tests/Input/src/android/hardware/input/KeyboardBacklightListenerTest.kt b/tests/Input/src/android/hardware/input/KeyboardBacklightListenerTest.kt index d25dee1d402c..9e419439fba4 100644 --- a/tests/Input/src/android/hardware/input/KeyboardBacklightListenerTest.kt +++ b/tests/Input/src/android/hardware/input/KeyboardBacklightListenerTest.kt @@ -42,15 +42,13 @@ import org.mockito.junit.MockitoJUnitRunner /** * Tests for [InputManager.KeyboardBacklightListener]. * - * Build/Install/Run: - * atest InputTests:KeyboardBacklightListenerTest + * Build/Install/Run: atest InputTests:KeyboardBacklightListenerTest */ @Presubmit @RunWith(MockitoJUnitRunner::class) class KeyboardBacklightListenerTest { - @get:Rule - val inputManagerRule = MockInputManagerRule() + @get:Rule val inputManagerRule = MockInputManagerRule() private lateinit var testLooper: TestLooper private var registeredListener: IKeyboardBacklightListener? = null @@ -65,43 +63,54 @@ class KeyboardBacklightListenerTest { executor = HandlerExecutor(Handler(testLooper.looper)) registeredListener = null inputManager = InputManager(context) - `when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) - .thenReturn(inputManager) + `when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))).thenReturn(inputManager) // Handle keyboard backlight listener registration. doAnswer { - val listener = it.getArgument(0) as IKeyboardBacklightListener - if (registeredListener != null && - registeredListener!!.asBinder() != listener.asBinder()) { - // There can only be one registered keyboard backlight listener per process. - fail("Trying to register a new listener when one already exists") + val listener = it.getArgument(0) as IKeyboardBacklightListener + if ( + registeredListener != null && + registeredListener!!.asBinder() != listener.asBinder() + ) { + // There can only be one registered keyboard backlight listener per process. + fail("Trying to register a new listener when one already exists") + } + registeredListener = listener + null } - registeredListener = listener - null - }.`when`(inputManagerRule.mock).registerKeyboardBacklightListener(any()) + .`when`(inputManagerRule.mock) + .registerKeyboardBacklightListener(any()) // Handle keyboard backlight listener being unregistered. doAnswer { - val listener = it.getArgument(0) as IKeyboardBacklightListener - if (registeredListener == null || - registeredListener!!.asBinder() != listener.asBinder()) { - fail("Trying to unregister a listener that is not registered") + val listener = it.getArgument(0) as IKeyboardBacklightListener + if ( + registeredListener == null || + registeredListener!!.asBinder() != listener.asBinder() + ) { + fail("Trying to unregister a listener that is not registered") + } + registeredListener = null + null } - registeredListener = null - null - }.`when`(inputManagerRule.mock).unregisterKeyboardBacklightListener(any()) + .`when`(inputManagerRule.mock) + .unregisterKeyboardBacklightListener(any()) } private fun notifyKeyboardBacklightChanged( deviceId: Int, brightnessLevel: Int, maxBrightnessLevel: Int = 10, - isTriggeredByKeyPress: Boolean = true + isTriggeredByKeyPress: Boolean = true, ) { - registeredListener!!.onBrightnessChanged(deviceId, IKeyboardBacklightState().apply { - this.brightnessLevel = brightnessLevel - this.maxBrightnessLevel = maxBrightnessLevel - }, isTriggeredByKeyPress) + registeredListener!!.onBrightnessChanged( + deviceId, + IKeyboardBacklightState().apply { + this.brightnessLevel = brightnessLevel + this.maxBrightnessLevel = maxBrightnessLevel + }, + isTriggeredByKeyPress, + ) } @Test @@ -110,9 +119,9 @@ class KeyboardBacklightListenerTest { // Add a keyboard backlight listener inputManager.registerKeyboardBacklightListener(executor) { - deviceId: Int, - keyboardBacklightState: KeyboardBacklightState, - isTriggeredByKeyPress: Boolean -> + deviceId: Int, + keyboardBacklightState: KeyboardBacklightState, + isTriggeredByKeyPress: Boolean -> callbackCount++ assertEquals(1, deviceId) assertEquals(2, keyboardBacklightState.brightnessLevel) diff --git a/tests/Input/src/android/hardware/input/KeyboardLayoutPreviewTests.kt b/tests/Input/src/android/hardware/input/KeyboardLayoutPreviewTests.kt index bcff2fcfca93..a59cbaf5fd55 100644 --- a/tests/Input/src/android/hardware/input/KeyboardLayoutPreviewTests.kt +++ b/tests/Input/src/android/hardware/input/KeyboardLayoutPreviewTests.kt @@ -28,8 +28,7 @@ import org.mockito.junit.MockitoJUnitRunner /** * Tests for Keyboard layout preview * - * Build/Install/Run: - * atest InputTests:KeyboardLayoutPreviewTests + * Build/Install/Run: atest InputTests:KeyboardLayoutPreviewTests */ @Presubmit @RunWith(MockitoJUnitRunner::class) @@ -52,4 +51,4 @@ class KeyboardLayoutPreviewTests { assertEquals(WIDTH, drawable.intrinsicWidth) assertEquals(HEIGHT, drawable.intrinsicHeight) } -}
\ No newline at end of file +} diff --git a/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt b/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt index cc58bbc38e2d..620cb015911e 100644 --- a/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt +++ b/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt @@ -43,15 +43,13 @@ import org.mockito.junit.MockitoJUnitRunner /** * Tests for [InputManager.StickyModifierStateListener]. * - * Build/Install/Run: - * atest InputTests:StickyModifierStateListenerTest + * Build/Install/Run: atest InputTests:StickyModifierStateListenerTest */ @Presubmit @RunWith(MockitoJUnitRunner::class) class StickyModifierStateListenerTest { - @get:Rule - val inputManagerRule = MockInputManagerRule() + @get:Rule val inputManagerRule = MockInputManagerRule() private val testLooper = TestLooper() private val executor = HandlerExecutor(Handler(testLooper.looper)) @@ -63,31 +61,38 @@ class StickyModifierStateListenerTest { fun setUp() { context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) inputManager = InputManager(context) - `when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) - .thenReturn(inputManager) + `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") + 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 } - registeredListener = listener - null - }.`when`(inputManagerRule.mock).registerStickyModifierStateListener(any()) + .`when`(inputManagerRule.mock) + .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") + 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 } - registeredListener = null - null - }.`when`(inputManagerRule.mock).unregisterStickyModifierStateListener(any()) + .`when`(inputManagerRule.mock) + .unregisterStickyModifierStateListener(any()) } private fun notifyStickyModifierStateChanged(modifierState: Int, lockedModifierState: Int) { @@ -99,9 +104,7 @@ class StickyModifierStateListenerTest { var callbackCount = 0 // Add a sticky modifier state listener - inputManager.registerStickyModifierStateListener(executor) { - callbackCount++ - } + inputManager.registerStickyModifierStateListener(executor) { callbackCount++ } // Notifying sticky modifier state change will notify the listener. notifyStickyModifierStateChanged(0, 0) @@ -112,8 +115,7 @@ class StickyModifierStateListenerTest { @Test fun testListenerHasCorrectModifierStateNotified() { // Add a sticky modifier state listener - inputManager.registerStickyModifierStateListener(executor) { - state: StickyModifierState -> + inputManager.registerStickyModifierStateListener(executor) { state: StickyModifierState -> assertTrue(state.isAltModifierOn) assertTrue(state.isAltModifierLocked) assertTrue(state.isShiftModifierOn) @@ -128,9 +130,11 @@ class StickyModifierStateListenerTest { // 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 + 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() } diff --git a/tests/Input/src/com/android/server/input/AmbientKeyboardBacklightControllerTests.kt b/tests/Input/src/com/android/server/input/AmbientKeyboardBacklightControllerTests.kt index ad481dff810c..075a3ab8cc73 100644 --- a/tests/Input/src/com/android/server/input/AmbientKeyboardBacklightControllerTests.kt +++ b/tests/Input/src/com/android/server/input/AmbientKeyboardBacklightControllerTests.kt @@ -54,8 +54,7 @@ import org.mockito.junit.MockitoJUnit /** * Tests for {@link AmbientKeyboardBacklightController}. * - * Build/Install/Run: - * atest InputTests:AmbientKeyboardBacklightControllerTests + * Build/Install/Run: atest InputTests:AmbientKeyboardBacklightControllerTests */ @Presubmit class AmbientKeyboardBacklightControllerTests { @@ -66,24 +65,19 @@ class AmbientKeyboardBacklightControllerTests { const val SENSOR_TYPE = "test_sensor_type" } - @get:Rule - val rule = MockitoJUnit.rule()!! + @get:Rule val rule = MockitoJUnit.rule()!! private lateinit var context: Context private lateinit var testLooper: TestLooper private lateinit var ambientController: AmbientKeyboardBacklightController - @Mock - private lateinit var resources: Resources + @Mock private lateinit var resources: Resources - @Mock - private lateinit var lightSensorInfo: InputSensorInfo + @Mock private lateinit var lightSensorInfo: InputSensorInfo - @Mock - private lateinit var sensorManager: SensorManager + @Mock private lateinit var sensorManager: SensorManager - @Mock - private lateinit var displayManagerInternal: DisplayManagerInternal + @Mock private lateinit var displayManagerInternal: DisplayManagerInternal private lateinit var lightSensor: Sensor private var currentDisplayInfo = DisplayInfo() @@ -114,26 +108,26 @@ class AmbientKeyboardBacklightControllerTests { `when`(resources.getIntArray(R.array.config_autoKeyboardBacklightIncreaseLuxThreshold)) .thenReturn(increaseThresholds) `when`( - resources.getValue( - eq(R.dimen.config_autoKeyboardBrightnessSmoothingConstant), - any(TypedValue::class.java), - anyBoolean() + resources.getValue( + eq(R.dimen.config_autoKeyboardBrightnessSmoothingConstant), + any(TypedValue::class.java), + anyBoolean(), + ) ) - ).then { - val args = it.arguments - val outValue = args[1] as TypedValue - outValue.data = java.lang.Float.floatToRawIntBits(1.0f) - Unit - } + .then { + val args = it.arguments + val outValue = args[1] as TypedValue + outValue.data = java.lang.Float.floatToRawIntBits(1.0f) + Unit + } } private fun setupSensor() { LocalServices.removeServiceForTest(DisplayManagerInternal::class.java) LocalServices.addService(DisplayManagerInternal::class.java, displayManagerInternal) currentDisplayInfo.uniqueId = DEFAULT_DISPLAY_UNIQUE_ID - `when`(displayManagerInternal.getDisplayInfo(Display.DEFAULT_DISPLAY)).thenReturn( - currentDisplayInfo - ) + `when`(displayManagerInternal.getDisplayInfo(Display.DEFAULT_DISPLAY)) + .thenReturn(currentDisplayInfo) val sensorData = DisplayManagerInternal.AmbientLightSensorData(SENSOR_NAME, SENSOR_TYPE) `when`(displayManagerInternal.getAmbientLightSensorData(Display.DEFAULT_DISPLAY)) .thenReturn(sensorData) @@ -144,26 +138,28 @@ class AmbientKeyboardBacklightControllerTests { `when`(context.getSystemService(eq(Context.SENSOR_SERVICE))).thenReturn(sensorManager) `when`(sensorManager.getSensorList(anyInt())).thenReturn(listOf(lightSensor)) `when`( - sensorManager.registerListener( - any(), - eq(lightSensor), - anyInt(), - any(Handler::class.java) + sensorManager.registerListener( + any(), + eq(lightSensor), + anyInt(), + any(Handler::class.java), + ) ) - ).then { - listenerRegistered = true - listenerRegistrationCount++ - true - } + .then { + listenerRegistered = true + listenerRegistrationCount++ + true + } `when`( - sensorManager.unregisterListener( - any(SensorEventListener::class.java), - eq(lightSensor) + sensorManager.unregisterListener( + any(SensorEventListener::class.java), + eq(lightSensor), + ) ) - ).then { - listenerRegistered = false - Unit - } + .then { + listenerRegistered = false + Unit + } } private fun setupSensorWithInitialLux(luxValue: Float) { @@ -181,7 +177,7 @@ class AmbientKeyboardBacklightControllerTests { assertEquals( "Should receive immediate callback for first lux change", 100, - lastBrightnessCallback + lastBrightnessCallback, ) } @@ -190,15 +186,13 @@ class AmbientKeyboardBacklightControllerTests { setupSensorWithInitialLux(500F) // Current state: Step 1 [value = 100, increaseThreshold = 1000, decreaseThreshold = -1] - repeat(HYSTERESIS_THRESHOLD) { - sendAmbientLuxValue(1500F) - } + repeat(HYSTERESIS_THRESHOLD) { sendAmbientLuxValue(1500F) } testLooper.dispatchAll() assertEquals( "Should receive brightness change callback for increasing lux change", 200, - lastBrightnessCallback + lastBrightnessCallback, ) } @@ -207,39 +201,31 @@ class AmbientKeyboardBacklightControllerTests { setupSensorWithInitialLux(1500F) // Current state: Step 2 [value = 200, increaseThreshold = 2000, decreaseThreshold = 900] - repeat(HYSTERESIS_THRESHOLD) { - sendAmbientLuxValue(500F) - } + repeat(HYSTERESIS_THRESHOLD) { sendAmbientLuxValue(500F) } testLooper.dispatchAll() assertEquals( "Should receive brightness change callback for decreasing lux change", 100, - lastBrightnessCallback + lastBrightnessCallback, ) } @Test fun testRegisterAmbientListener_throwsExceptionOnRegisteringDuplicate() { - val ambientListener = - AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener { } + val ambientListener = AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener {} ambientController.registerAmbientBacklightListener(ambientListener) assertThrows(IllegalStateException::class.java) { - ambientController.registerAmbientBacklightListener( - ambientListener - ) + ambientController.registerAmbientBacklightListener(ambientListener) } } @Test fun testUnregisterAmbientListener_throwsExceptionOnUnregisteringNonExistent() { - val ambientListener = - AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener { } + val ambientListener = AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener {} assertThrows(IllegalStateException::class.java) { - ambientController.unregisterAmbientBacklightListener( - ambientListener - ) + ambientController.unregisterAmbientBacklightListener(ambientListener) } } @@ -248,25 +234,23 @@ class AmbientKeyboardBacklightControllerTests { assertEquals( "Should not have a sensor listener registered at init", 0, - listenerRegistrationCount + listenerRegistrationCount, ) assertFalse("Should not have a sensor listener registered at init", listenerRegistered) val ambientListener1 = - AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener { } + AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener {} ambientController.registerAmbientBacklightListener(ambientListener1) - assertEquals( - "Should register a new sensor listener", 1, listenerRegistrationCount - ) + assertEquals("Should register a new sensor listener", 1, listenerRegistrationCount) assertTrue("Should have sensor listener registered", listenerRegistered) val ambientListener2 = - AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener { } + AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener {} ambientController.registerAmbientBacklightListener(ambientListener2) assertEquals( "Should not register a new sensor listener when adding a second ambient listener", 1, - listenerRegistrationCount + listenerRegistrationCount, ) assertTrue("Should have sensor listener registered", listenerRegistered) @@ -276,7 +260,7 @@ class AmbientKeyboardBacklightControllerTests { ambientController.unregisterAmbientBacklightListener(ambientListener2) assertFalse( "Should not have sensor listener registered if there are no ambient listeners", - listenerRegistered + listenerRegistered, ) } @@ -291,7 +275,7 @@ class AmbientKeyboardBacklightControllerTests { assertEquals( "Should not re-register listener on display change if unique is same", count, - listenerRegistrationCount + listenerRegistrationCount, ) } @@ -307,7 +291,7 @@ class AmbientKeyboardBacklightControllerTests { assertEquals( "Should re-register listener on display change if unique id changed", count + 1, - listenerRegistrationCount + listenerRegistrationCount, ) } @@ -318,15 +302,13 @@ class AmbientKeyboardBacklightControllerTests { // Previous state: Step 1 [value = 100, increaseThreshold = 1000, decreaseThreshold = -1] // Current state: Step 2 [value = 200, increaseThreshold = 2000, decreaseThreshold = 900] lastBrightnessCallback = -1 - repeat(HYSTERESIS_THRESHOLD) { - sendAmbientLuxValue(999F) - } + repeat(HYSTERESIS_THRESHOLD) { sendAmbientLuxValue(999F) } testLooper.dispatchAll() assertEquals( "Should not receive any callback for brightness change", -1, - lastBrightnessCallback + lastBrightnessCallback, ) } @@ -337,15 +319,13 @@ class AmbientKeyboardBacklightControllerTests { // Previous state: Step 1 [value = 100, increaseThreshold = 1000, decreaseThreshold = -1] // Current state: Step 2 [value = 200, increaseThreshold = 2000, decreaseThreshold = 900] lastBrightnessCallback = -1 - repeat(HYSTERESIS_THRESHOLD - 1) { - sendAmbientLuxValue(2001F) - } + repeat(HYSTERESIS_THRESHOLD - 1) { sendAmbientLuxValue(2001F) } testLooper.dispatchAll() assertEquals( "Should not receive any callback for brightness change", -1, - lastBrightnessCallback + lastBrightnessCallback, ) } diff --git a/tests/Input/src/com/android/server/input/BatteryControllerTests.kt b/tests/Input/src/com/android/server/input/BatteryControllerTests.kt index 890c346ea015..bcb740652dde 100644 --- a/tests/Input/src/com/android/server/input/BatteryControllerTests.kt +++ b/tests/Input/src/com/android/server/input/BatteryControllerTests.kt @@ -92,7 +92,7 @@ private fun createInputDevice( private fun <T, U> memberMatcher( member: String, memberProvider: (T) -> U, - match: Matcher<U> + match: Matcher<U>, ): TypeSafeMatcher<T> = object : TypeSafeMatcher<T>() { @@ -115,12 +115,13 @@ private fun matchesState( isPresent: Boolean = true, status: Int? = null, capacity: Float? = null, - eventTime: Long? = null + eventTime: Long? = null, ): Matcher<IInputDeviceBatteryState> { - val batteryStateMatchers = mutableListOf<Matcher<IInputDeviceBatteryState>>( - memberMatcher("deviceId", { it.deviceId }, equalTo(deviceId)), - memberMatcher("isPresent", { it.isPresent }, equalTo(isPresent)) - ) + val batteryStateMatchers = + mutableListOf<Matcher<IInputDeviceBatteryState>>( + memberMatcher("deviceId", { it.deviceId }, equalTo(deviceId)), + memberMatcher("isPresent", { it.isPresent }, equalTo(isPresent)), + ) if (eventTime != null) { batteryStateMatchers.add(memberMatcher("updateTime", { it.updateTime }, equalTo(eventTime))) } @@ -143,14 +144,14 @@ private fun IInputDeviceBatteryListener.verifyNotified( isPresent: Boolean = true, status: Int? = null, capacity: Float? = null, - eventTime: Long? = null + eventTime: Long? = null, ) { verifyNotified(matchesState(deviceId, isPresent, status, capacity, eventTime), mode) } private fun IInputDeviceBatteryListener.verifyNotified( matcher: Matcher<IInputDeviceBatteryState>, - mode: VerificationMode = times(1) + mode: VerificationMode = times(1), ) { verify(this, mode).onBatteryStateChanged(MockitoHamcrest.argThat(matcher)) } @@ -165,8 +166,7 @@ private fun createMockListener(): IInputDeviceBatteryListener { /** * Tests for {@link InputDeviceBatteryController}. * - * Build/Install/Run: - * atest InputTests:InputDeviceBatteryControllerTests + * Build/Install/Run: atest InputTests:InputDeviceBatteryControllerTests */ @Presubmit class BatteryControllerTests { @@ -181,19 +181,13 @@ class BatteryControllerTests { const val TIMESTAMP = 123456789L } - @get:Rule - val rule = MockitoJUnit.rule()!! - @get:Rule - val context = TestableContext(ApplicationProvider.getApplicationContext()) - @get:Rule - val inputManagerRule = MockInputManagerRule() + @get:Rule val rule = MockitoJUnit.rule()!! + @get:Rule val context = TestableContext(ApplicationProvider.getApplicationContext()) + @get:Rule val inputManagerRule = MockInputManagerRule() - @Mock - private lateinit var native: NativeInputManagerService - @Mock - private lateinit var uEventManager: UEventManager - @Mock - private lateinit var bluetoothBatteryManager: BluetoothBatteryManager + @Mock private lateinit var native: NativeInputManagerService + @Mock private lateinit var uEventManager: UEventManager + @Mock private lateinit var bluetoothBatteryManager: BluetoothBatteryManager private lateinit var batteryController: BatteryController private lateinit var testLooper: TestLooper @@ -206,14 +200,18 @@ class BatteryControllerTests { testLooper = TestLooper() val inputManager = InputManager(context) context.addMockSystemService(InputManager::class.java, inputManager) - `when`(inputManagerRule.mock.inputDeviceIds).then { - deviceGenerationMap.keys.toIntArray() - } + `when`(inputManagerRule.mock.inputDeviceIds).then { deviceGenerationMap.keys.toIntArray() } addInputDevice(DEVICE_ID) addInputDevice(SECOND_DEVICE_ID) - batteryController = BatteryController(context, native, testLooper.looper, uEventManager, - bluetoothBatteryManager) + batteryController = + BatteryController( + context, + native, + testLooper.looper, + uEventManager, + bluetoothBatteryManager, + ) batteryController.systemRunning() val listenerCaptor = ArgumentCaptor.forClass(IInputDevicesChangedListener::class.java) verify(inputManagerRule.mock).registerInputDevicesChangedListener(listenerCaptor.capture()) @@ -222,12 +220,13 @@ class BatteryControllerTests { } private fun notifyDeviceChanged( - deviceId: Int, + deviceId: Int, hasBattery: Boolean = true, - supportsUsi: Boolean = false + supportsUsi: Boolean = false, ) { - val generation = deviceGenerationMap[deviceId]?.plus(1) - ?: throw IllegalArgumentException("Device $deviceId was never added!") + val generation = + deviceGenerationMap[deviceId]?.plus(1) + ?: throw IllegalArgumentException("Device $deviceId was never added!") deviceGenerationMap[deviceId] = generation `when`(inputManagerRule.mock.getInputDevice(deviceId)) @@ -239,7 +238,7 @@ class BatteryControllerTests { } private fun addInputDevice( - deviceId: Int, + deviceId: Int, hasBattery: Boolean = true, supportsUsi: Boolean = false, ) { @@ -248,8 +247,10 @@ class BatteryControllerTests { } private fun createBluetoothDevice(address: String): BluetoothDevice { - return context.getSystemService(BluetoothManager::class.java)!! - .adapter.getRemoteDevice(address) + return context + .getSystemService(BluetoothManager::class.java)!! + .adapter + .getRemoteDevice(address) } @Test @@ -279,8 +280,7 @@ class BatteryControllerTests { try { batteryController.registerBatteryListener(DEVICE_ID, listener2, PID) fail("Expected security exception when registering more than one listener per process") - } catch (ignored: SecurityException) { - } + } catch (ignored: SecurityException) {} } @Test @@ -323,15 +323,18 @@ class BatteryControllerTests { batteryController.registerBatteryListener(DEVICE_ID, listener, PID) // The device paths for UEvent notifications do not include the "/sys" prefix, so verify // that the added listener is configured to match the path without that prefix. - verify(uEventManager) - .addListener(uEventListener.capture(), eq("DEVPATH=/dev/test/device1")) + verify(uEventManager).addListener(uEventListener.capture(), eq("DEVPATH=/dev/test/device1")) listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.78f) // If the battery state has changed when an UEvent is sent, the listeners are notified. `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(80) uEventListener.value!!.onBatteryUEvent(TIMESTAMP) - listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.80f, - eventTime = TIMESTAMP) + listener.verifyNotified( + DEVICE_ID, + status = STATUS_CHARGING, + capacity = 0.80f, + eventTime = TIMESTAMP, + ) // If the battery state has not changed when an UEvent is sent, the listeners are not // notified. @@ -341,8 +344,11 @@ class BatteryControllerTests { batteryController.unregisterBatteryListener(DEVICE_ID, listener, PID) verify(uEventManager).removeListener(uEventListener.capture()) - assertEquals("The same observer must be registered and unregistered", - uEventListener.allValues[0], uEventListener.allValues[1]) + assertEquals( + "The same observer must be registered and unregistered", + uEventListener.allValues[0], + uEventListener.allValues[1], + ) } @Test @@ -366,8 +372,12 @@ class BatteryControllerTests { // If the battery becomes present again, the listener is notified. notifyDeviceChanged(DEVICE_ID, hasBattery = true) testLooper.dispatchNext() - listener.verifyNotified(DEVICE_ID, mode = times(2), status = STATUS_CHARGING, - capacity = 0.78f) + listener.verifyNotified( + DEVICE_ID, + mode = times(2), + status = STATUS_CHARGING, + capacity = 0.78f, + ) // Ensure that a new UEventListener was added. verify(uEventManager, times(2)) .addListener(uEventListener.capture(), eq("DEVPATH=/test/device1")) @@ -437,8 +447,11 @@ class BatteryControllerTests { `when`(native.getBatteryStatus(DEVICE_ID)).thenReturn(STATUS_CHARGING) `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(78) val batteryState = batteryController.getBatteryState(DEVICE_ID) - assertThat("battery state matches", batteryState, - matchesState(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.78f)) + assertThat( + "battery state matches", + batteryState, + matchesState(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.78f), + ) } @Test @@ -453,8 +466,11 @@ class BatteryControllerTests { // change in the battery state, the listener is also notified. `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(80) val batteryState = batteryController.getBatteryState(DEVICE_ID) - assertThat("battery matches state", batteryState, - matchesState(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.80f)) + assertThat( + "battery matches state", + batteryState, + matchesState(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.80f), + ) listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.80f) } @@ -466,8 +482,7 @@ class BatteryControllerTests { // Even though there is no listener added for this device, it is being monitored. val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) - verify(uEventManager) - .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) + verify(uEventManager).addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) // Add and remove a listener for the device. val listener = createMockListener() @@ -507,8 +522,7 @@ class BatteryControllerTests { addInputDevice(USI_DEVICE_ID, supportsUsi = true) testLooper.dispatchNext() val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) - verify(uEventManager) - .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) + verify(uEventManager).addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) // A USI device's battery state is not valid until the first UEvent notification. // Add a listener, and ensure it is notified that the battery state is not present. @@ -517,34 +531,49 @@ class BatteryControllerTests { listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID)) // Ensure that querying for battery state also returns the same invalid result. - assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), - isInvalidBatteryState(USI_DEVICE_ID)) + assertThat( + "battery state matches", + batteryController.getBatteryState(USI_DEVICE_ID), + isInvalidBatteryState(USI_DEVICE_ID), + ) // There is a UEvent signaling a battery change. The battery state is now valid. uEventListener.value!!.onBatteryUEvent(TIMESTAMP) listener.verifyNotified(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f) - assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), - matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f)) + assertThat( + "battery state matches", + batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f), + ) // There is another UEvent notification. The battery state is now updated. `when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(64) uEventListener.value!!.onBatteryUEvent(TIMESTAMP + 1) listener.verifyNotified(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f) - assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), - matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f)) + assertThat( + "battery state matches", + batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f), + ) // The battery state is still valid after a millisecond. testLooper.moveTimeForward(1) testLooper.dispatchAll() - assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), - matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f)) + assertThat( + "battery state matches", + batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f), + ) // The battery is no longer present after the timeout expires. testLooper.moveTimeForward(USI_BATTERY_VALIDITY_DURATION_MILLIS - 1) testLooper.dispatchNext() listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID), times(2)) - assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), - isInvalidBatteryState(USI_DEVICE_ID)) + assertThat( + "battery state matches", + batteryController.getBatteryState(USI_DEVICE_ID), + isInvalidBatteryState(USI_DEVICE_ID), + ) } @Test @@ -556,16 +585,18 @@ class BatteryControllerTests { addInputDevice(USI_DEVICE_ID, supportsUsi = true) testLooper.dispatchNext() val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) - verify(uEventManager) - .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) + verify(uEventManager).addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) // There is a UEvent signaling a battery change. The battery state is now valid. uEventListener.value!!.onBatteryUEvent(TIMESTAMP) val listener = createMockListener() batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID) listener.verifyNotified(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f) - assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), - matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f)) + assertThat( + "battery state matches", + batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f), + ) // Stylus presence is detected before the validity timeout expires. testLooper.moveTimeForward(100) @@ -575,15 +606,21 @@ class BatteryControllerTests { // Ensure that timeout was extended, and the battery state is now valid for longer. testLooper.moveTimeForward(USI_BATTERY_VALIDITY_DURATION_MILLIS - 100) testLooper.dispatchAll() - assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), - matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f)) + assertThat( + "battery state matches", + batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f), + ) // Ensure the validity period expires after the expected amount of time. testLooper.moveTimeForward(100) testLooper.dispatchNext() listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID)) - assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), - isInvalidBatteryState(USI_DEVICE_ID)) + assertThat( + "battery state matches", + batteryController.getBatteryState(USI_DEVICE_ID), + isInvalidBatteryState(USI_DEVICE_ID), + ) } @Test @@ -595,22 +632,27 @@ class BatteryControllerTests { addInputDevice(USI_DEVICE_ID, supportsUsi = true) testLooper.dispatchNext() val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) - verify(uEventManager) - .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) + verify(uEventManager).addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) // The USI battery state is initially invalid. val listener = createMockListener() batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID) listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID)) - assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), - isInvalidBatteryState(USI_DEVICE_ID)) + assertThat( + "battery state matches", + batteryController.getBatteryState(USI_DEVICE_ID), + isInvalidBatteryState(USI_DEVICE_ID), + ) // A stylus presence is detected. This validates the battery state. batteryController.notifyStylusGestureStarted(USI_DEVICE_ID, TIMESTAMP) listener.verifyNotified(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f) - assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), - matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f)) + assertThat( + "battery state matches", + batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f), + ) } @Test @@ -623,27 +665,35 @@ class BatteryControllerTests { addInputDevice(USI_DEVICE_ID, supportsUsi = true) testLooper.dispatchNext() val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) - verify(uEventManager) - .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) + verify(uEventManager).addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) // The USI battery state is initially invalid. val listener = createMockListener() batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID) listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID)) - assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), - isInvalidBatteryState(USI_DEVICE_ID)) + assertThat( + "battery state matches", + batteryController.getBatteryState(USI_DEVICE_ID), + isInvalidBatteryState(USI_DEVICE_ID), + ) // Since the capacity reported is 0, stylus presence does not validate the battery state. batteryController.notifyStylusGestureStarted(USI_DEVICE_ID, TIMESTAMP) - assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), - isInvalidBatteryState(USI_DEVICE_ID)) + assertThat( + "battery state matches", + batteryController.getBatteryState(USI_DEVICE_ID), + isInvalidBatteryState(USI_DEVICE_ID), + ) // However, if a UEvent reports a battery capacity of 0, the battery state is now valid. uEventListener.value!!.onBatteryUEvent(TIMESTAMP) listener.verifyNotified(USI_DEVICE_ID, status = STATUS_UNKNOWN, capacity = 0f) - assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), - matchesState(USI_DEVICE_ID, status = STATUS_UNKNOWN, capacity = 0f)) + assertThat( + "battery state matches", + batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_UNKNOWN, capacity = 0f), + ) } @Test @@ -722,15 +772,21 @@ class BatteryControllerTests { verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) verify(uEventManager).addListener(uEventListener.capture(), any()) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.21f) - assertThat("battery state matches", batteryController.getBatteryState(BT_DEVICE_ID), - matchesState(BT_DEVICE_ID, capacity = 0.21f)) + assertThat( + "battery state matches", + batteryController.getBatteryState(BT_DEVICE_ID), + matchesState(BT_DEVICE_ID, capacity = 0.21f), + ) // If only the native battery state changes the listener is not notified. `when`(native.getBatteryCapacity(BT_DEVICE_ID)).thenReturn(97) uEventListener.value!!.onBatteryUEvent(TIMESTAMP) listener.verifyNotified(BT_DEVICE_ID, mode = times(1), capacity = 0.21f) - assertThat("battery state matches", batteryController.getBatteryState(BT_DEVICE_ID), - matchesState(BT_DEVICE_ID, capacity = 0.21f)) + assertThat( + "battery state matches", + batteryController.getBatteryState(BT_DEVICE_ID), + matchesState(BT_DEVICE_ID, capacity = 0.21f), + ) } @Test @@ -751,8 +807,11 @@ class BatteryControllerTests { listener.verifyNotified(BT_DEVICE_ID, capacity = 0.21f) // Fall back to the native state when BT is off. - bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", - BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF) + bluetoothListener.value!!.onBluetoothBatteryChanged( + TIMESTAMP, + "AA:BB:CC:DD:EE:FF", + BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF, + ) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.98f) bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", 22) @@ -760,8 +819,11 @@ class BatteryControllerTests { listener.verifyNotified(BT_DEVICE_ID, capacity = 0.22f) // Fall back to the native state when BT battery is unknown. - bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", - BluetoothDevice.BATTERY_LEVEL_UNKNOWN) + bluetoothListener.value!!.onBluetoothBatteryChanged( + TIMESTAMP, + "AA:BB:CC:DD:EE:FF", + BluetoothDevice.BATTERY_LEVEL_UNKNOWN, + ) listener.verifyNotified(BT_DEVICE_ID, mode = times(2), capacity = 0.98f) } @@ -782,10 +844,10 @@ class BatteryControllerTests { batteryController.registerBatteryListener(DEVICE_ID, listener, PID) verify(bluetoothBatteryManager, never()).addMetadataListener(any(), any()) - val metadataListener1 = ArgumentCaptor.forClass( - BluetoothAdapter.OnMetadataChangedListener::class.java) - val metadataListener2 = ArgumentCaptor.forClass( - BluetoothAdapter.OnMetadataChangedListener::class.java) + val metadataListener1 = + ArgumentCaptor.forClass(BluetoothAdapter.OnMetadataChangedListener::class.java) + val metadataListener2 = + ArgumentCaptor.forClass(BluetoothAdapter.OnMetadataChangedListener::class.java) // The metadata listener is added when the first BT input device is monitored. batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) @@ -814,12 +876,16 @@ class BatteryControllerTests { fun testNotifiesBluetoothMetadataBatteryChanges() { `when`(inputManagerRule.mock.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) .thenReturn("AA:BB:CC:DD:EE:FF") - `when`(bluetoothBatteryManager.getMetadata("AA:BB:CC:DD:EE:FF", - BluetoothDevice.METADATA_MAIN_BATTERY)) + `when`( + bluetoothBatteryManager.getMetadata( + "AA:BB:CC:DD:EE:FF", + BluetoothDevice.METADATA_MAIN_BATTERY, + ) + ) .thenReturn("21".toByteArray()) addInputDevice(BT_DEVICE_ID) - val metadataListener = ArgumentCaptor.forClass( - BluetoothAdapter.OnMetadataChangedListener::class.java) + val metadataListener = + ArgumentCaptor.forClass(BluetoothAdapter.OnMetadataChangedListener::class.java) val listener = createMockListener() val bluetoothDevice = createBluetoothDevice("AA:BB:CC:DD:EE:FF") batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) @@ -829,25 +895,44 @@ class BatteryControllerTests { // When the state has not changed, the listener is not notified again. metadataListener.value!!.onMetadataChanged( - bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, "21".toByteArray()) + bluetoothDevice, + BluetoothDevice.METADATA_MAIN_BATTERY, + "21".toByteArray(), + ) listener.verifyNotified(BT_DEVICE_ID, mode = times(1), capacity = 0.21f) metadataListener.value!!.onMetadataChanged( - bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, "25".toByteArray()) + bluetoothDevice, + BluetoothDevice.METADATA_MAIN_BATTERY, + "25".toByteArray(), + ) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f, status = STATUS_UNKNOWN) metadataListener.value!!.onMetadataChanged( - bluetoothDevice, BluetoothDevice.METADATA_MAIN_CHARGING, "true".toByteArray()) + bluetoothDevice, + BluetoothDevice.METADATA_MAIN_CHARGING, + "true".toByteArray(), + ) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f, status = STATUS_CHARGING) metadataListener.value!!.onMetadataChanged( - bluetoothDevice, BluetoothDevice.METADATA_MAIN_CHARGING, "false".toByteArray()) + bluetoothDevice, + BluetoothDevice.METADATA_MAIN_CHARGING, + "false".toByteArray(), + ) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f, status = STATUS_DISCHARGING) metadataListener.value!!.onMetadataChanged( - bluetoothDevice, BluetoothDevice.METADATA_MAIN_CHARGING, null) - listener.verifyNotified(BT_DEVICE_ID, mode = times(2), capacity = 0.25f, - status = STATUS_UNKNOWN) + bluetoothDevice, + BluetoothDevice.METADATA_MAIN_CHARGING, + null, + ) + listener.verifyNotified( + BT_DEVICE_ID, + mode = times(2), + capacity = 0.25f, + status = STATUS_UNKNOWN, + ) } @Test @@ -855,13 +940,17 @@ class BatteryControllerTests { `when`(inputManagerRule.mock.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) .thenReturn("AA:BB:CC:DD:EE:FF") `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))).thenReturn(21) - `when`(bluetoothBatteryManager.getMetadata("AA:BB:CC:DD:EE:FF", - BluetoothDevice.METADATA_MAIN_BATTERY)) + `when`( + bluetoothBatteryManager.getMetadata( + "AA:BB:CC:DD:EE:FF", + BluetoothDevice.METADATA_MAIN_BATTERY, + ) + ) .thenReturn("22".toByteArray()) addInputDevice(BT_DEVICE_ID) val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java) - val metadataListener = ArgumentCaptor.forClass( - BluetoothAdapter.OnMetadataChangedListener::class.java) + val metadataListener = + ArgumentCaptor.forClass(BluetoothAdapter.OnMetadataChangedListener::class.java) val listener = createMockListener() val bluetoothDevice = createBluetoothDevice("AA:BB:CC:DD:EE:FF") batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) @@ -877,13 +966,19 @@ class BatteryControllerTests { listener.verifyNotified(BT_DEVICE_ID, mode = never(), capacity = 0.23f) metadataListener.value!!.onMetadataChanged( - bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, "24".toByteArray()) + bluetoothDevice, + BluetoothDevice.METADATA_MAIN_BATTERY, + "24".toByteArray(), + ) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.24f) // When the battery level from the metadata is no longer valid, we fall back to using the // Bluetooth battery level. metadataListener.value!!.onMetadataChanged( - bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, null) + bluetoothDevice, + BluetoothDevice.METADATA_MAIN_BATTERY, + null, + ) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.23f) } } diff --git a/tests/Input/src/com/android/server/input/InputDataStoreTests.kt b/tests/Input/src/com/android/server/input/InputDataStoreTests.kt index 78c828bafd8f..812bd608ed20 100644 --- a/tests/Input/src/com/android/server/input/InputDataStoreTests.kt +++ b/tests/Input/src/com/android/server/input/InputDataStoreTests.kt @@ -41,8 +41,7 @@ import org.mockito.Mockito /** * Tests for {@link InputDataStore}. * - * Build/Install/Run: - * atest InputTests:InputDataStoreTests + * Build/Install/Run: atest InputTests:InputDataStoreTests */ @Presubmit class InputDataStoreTests { @@ -63,25 +62,32 @@ class InputDataStoreTests { private fun setupInputDataStore() { tempFile = File.createTempFile("input_gestures", ".xml") - inputDataStore = InputDataStore(object : InputDataStore.FileInjector("input_gestures") { - private val atomicFile: AtomicFile = AtomicFile(tempFile) + inputDataStore = + InputDataStore( + object : InputDataStore.FileInjector("input_gestures") { + private val atomicFile: AtomicFile = AtomicFile(tempFile) - override fun openRead(userId: Int): InputStream? { - return atomicFile.openRead() - } + override fun openRead(userId: Int): InputStream? { + return atomicFile.openRead() + } - override fun startWrite(userId: Int): FileOutputStream? { - return atomicFile.startWrite() - } + override fun startWrite(userId: Int): FileOutputStream? { + return atomicFile.startWrite() + } - override fun finishWrite(userId: Int, fos: FileOutputStream?, success: Boolean) { - if (success) { - atomicFile.finishWrite(fos) - } else { - atomicFile.failWrite(fos) + override fun finishWrite( + userId: Int, + fos: FileOutputStream?, + success: Boolean, + ) { + if (success) { + atomicFile.finishWrite(fos) + } else { + atomicFile.failWrite(fos) + } + } } - } - }) + ) } private fun getPrintableXml(inputGestures: List<InputGestureData>): String { @@ -92,164 +98,157 @@ class InputDataStoreTests { @Test fun saveToDiskKeyGesturesOnly() { - val inputGestures = listOf( - InputGestureData.Builder() - .setTrigger( - InputGestureData.createKeyTrigger( - KeyEvent.KEYCODE_H, - KeyEvent.META_META_ON + val inputGestures = + listOf( + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger(KeyEvent.KEYCODE_H, KeyEvent.META_META_ON) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) - .build(), - InputGestureData.Builder() - .setTrigger( - InputGestureData.createKeyTrigger( - KeyEvent.KEYCODE_1, - KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_1, + KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON, + ) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) - .build(), - InputGestureData.Builder() - .setTrigger( - InputGestureData.createKeyTrigger( - KeyEvent.KEYCODE_2, - KeyEvent.META_META_ON + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger(KeyEvent.KEYCODE_2, KeyEvent.META_META_ON) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) - .build() - ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) + .build(), + ) inputDataStore.saveInputGestures(USER_ID, inputGestures) assertEquals( inputGestures, inputDataStore.loadInputGestures(USER_ID), - getPrintableXml(inputGestures) + getPrintableXml(inputGestures), ) } @Test fun saveToDiskTouchpadGestures() { - val inputGestures = listOf( - InputGestureData.Builder() - .setTrigger( - InputGestureData.createTouchpadTrigger( - InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP + val inputGestures = + listOf( + InputGestureData.Builder() + .setTrigger( + InputGestureData.createTouchpadTrigger( + InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP + ) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) - .build() - ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() + ) inputDataStore.saveInputGestures(USER_ID, inputGestures) assertEquals( inputGestures, inputDataStore.loadInputGestures(USER_ID), - getPrintableXml(inputGestures) + getPrintableXml(inputGestures), ) } @Test fun saveToDiskAppLaunchGestures() { - val inputGestures = listOf( - InputGestureData.Builder() - .setTrigger( - InputGestureData.createTouchpadTrigger( - InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP + val inputGestures = + listOf( + InputGestureData.Builder() + .setTrigger( + InputGestureData.createTouchpadTrigger( + InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP + ) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION) - .setAppLaunchData(AppLaunchData.createLaunchDataForRole(RoleManager.ROLE_BROWSER)) - .build(), - InputGestureData.Builder() - .setTrigger( - InputGestureData.createKeyTrigger( - KeyEvent.KEYCODE_2, - KeyEvent.META_META_ON + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION) + .setAppLaunchData( + AppLaunchData.createLaunchDataForRole(RoleManager.ROLE_BROWSER) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION) - .setAppLaunchData(AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CONTACTS)) - .build(), - InputGestureData.Builder() - .setTrigger( - InputGestureData.createKeyTrigger( - KeyEvent.KEYCODE_1, - KeyEvent.META_META_ON + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger(KeyEvent.KEYCODE_2, KeyEvent.META_META_ON) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION) - .setAppLaunchData( - AppLaunchData.createLaunchDataForComponent( - "com.test", - "com.test.BookmarkTest" + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION) + .setAppLaunchData( + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CONTACTS) ) - ) - .build() - ) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger(KeyEvent.KEYCODE_1, KeyEvent.META_META_ON) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION) + .setAppLaunchData( + AppLaunchData.createLaunchDataForComponent( + "com.test", + "com.test.BookmarkTest", + ) + ) + .build(), + ) inputDataStore.saveInputGestures(USER_ID, inputGestures) assertEquals( inputGestures, inputDataStore.loadInputGestures(USER_ID), - getPrintableXml(inputGestures) + getPrintableXml(inputGestures), ) } @Test fun saveToDiskCombinedGestures() { - val inputGestures = listOf( - InputGestureData.Builder() - .setTrigger( - InputGestureData.createKeyTrigger( - KeyEvent.KEYCODE_1, - KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON + val inputGestures = + listOf( + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_1, + KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON, + ) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) - .build(), - InputGestureData.Builder() - .setTrigger( - InputGestureData.createKeyTrigger( - KeyEvent.KEYCODE_2, - KeyEvent.META_META_ON + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger(KeyEvent.KEYCODE_2, KeyEvent.META_META_ON) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) - .build(), - InputGestureData.Builder() - .setTrigger( - InputGestureData.createTouchpadTrigger( - InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createTouchpadTrigger( + InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP + ) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) - .build(), - InputGestureData.Builder() - .setTrigger( - InputGestureData.createKeyTrigger( - KeyEvent.KEYCODE_9, - KeyEvent.META_META_ON + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger(KeyEvent.KEYCODE_9, KeyEvent.META_META_ON) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION) - .setAppLaunchData(AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CONTACTS)) - .build(), - ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION) + .setAppLaunchData( + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CONTACTS) + ) + .build(), + ) inputDataStore.saveInputGestures(USER_ID, inputGestures) assertEquals( inputGestures, inputDataStore.loadInputGestures(USER_ID), - getPrintableXml(inputGestures) + getPrintableXml(inputGestures), ) } @Test fun validXmlParse() { - val xmlData = """ + val xmlData = + """ <?xml version='1.0' encoding='utf-8' standalone='yes' ?> <root> <input_gesture_list> @@ -263,45 +262,42 @@ class InputDataStoreTests { <touchpad_trigger touchpad_gesture_type="1" /> </input_gesture> </input_gesture_list> - </root>""".trimIndent() - val validInputGestures = listOf( - InputGestureData.Builder() - .setTrigger( - InputGestureData.createKeyTrigger( - KeyEvent.KEYCODE_1, - KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON + </root>""" + .trimIndent() + val validInputGestures = + listOf( + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_1, + KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON, + ) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) - .build(), - InputGestureData.Builder() - .setTrigger( - InputGestureData.createKeyTrigger( - KeyEvent.KEYCODE_2, - KeyEvent.META_META_ON + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger(KeyEvent.KEYCODE_2, KeyEvent.META_META_ON) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) - .build(), - InputGestureData.Builder() - .setTrigger( - InputGestureData.createTouchpadTrigger( - InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createTouchpadTrigger( + InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP + ) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) - .build() - ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build(), + ) val inputStream = ByteArrayInputStream(xmlData.toByteArray(Charsets.UTF_8)) - assertEquals( - validInputGestures, - inputDataStore.readInputGesturesXml(inputStream, true) - ) + assertEquals(validInputGestures, inputDataStore.readInputGesturesXml(inputStream, true)) } @Test fun missingTriggerData() { - val xmlData = """ + val xmlData = + """ <?xml version='1.0' encoding='utf-8' standalone='yes' ?> <root> <input_gesture_list> @@ -314,36 +310,33 @@ class InputDataStoreTests { <touchpad_trigger touchpad_gesture_type="1" /> </input_gesture> </input_gesture_list> - </root>""".trimIndent() - val validInputGestures = listOf( - InputGestureData.Builder() - .setTrigger( - InputGestureData.createKeyTrigger( - KeyEvent.KEYCODE_2, - KeyEvent.META_META_ON + </root>""" + .trimIndent() + val validInputGestures = + listOf( + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger(KeyEvent.KEYCODE_2, KeyEvent.META_META_ON) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) - .build(), - InputGestureData.Builder() - .setTrigger( - InputGestureData.createTouchpadTrigger( - InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createTouchpadTrigger( + InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP + ) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) - .build() - ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build(), + ) val inputStream = ByteArrayInputStream(xmlData.toByteArray(Charsets.UTF_8)) - assertEquals( - validInputGestures, - inputDataStore.readInputGesturesXml(inputStream, true) - ) + assertEquals(validInputGestures, inputDataStore.readInputGesturesXml(inputStream, true)) } @Test fun invalidKeycode() { - val xmlData = """ + val xmlData = + """ <?xml version='1.0' encoding='utf-8' standalone='yes' ?> <root> <input_gesture_list> @@ -357,36 +350,36 @@ class InputDataStoreTests { <touchpad_trigger touchpad_gesture_type="1" /> </input_gesture> </input_gesture_list> - </root>""".trimIndent() - val validInputGestures = listOf( - InputGestureData.Builder() - .setTrigger( - InputGestureData.createKeyTrigger( - KeyEvent.KEYCODE_1, - KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON + </root>""" + .trimIndent() + val validInputGestures = + listOf( + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_1, + KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON, + ) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) - .build(), - InputGestureData.Builder() - .setTrigger( - InputGestureData.createTouchpadTrigger( - InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createTouchpadTrigger( + InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP + ) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) - .build() - ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build(), + ) val inputStream = ByteArrayInputStream(xmlData.toByteArray(Charsets.UTF_8)) - assertEquals( - validInputGestures, - inputDataStore.readInputGesturesXml(inputStream, true) - ) + assertEquals(validInputGestures, inputDataStore.readInputGesturesXml(inputStream, true)) } @Test fun invalidTriggerName() { - val xmlData = """ + val xmlData = + """ <?xml version='1.0' encoding='utf-8' standalone='yes' ?> <root> <input_gesture_list> @@ -400,37 +393,34 @@ class InputDataStoreTests { <invalid_trigger_name touchpad_gesture_type="1" /> </input_gesture> </input_gesture_list> - </root>""".trimIndent() - val validInputGestures = listOf( - InputGestureData.Builder() - .setTrigger( - InputGestureData.createKeyTrigger( - KeyEvent.KEYCODE_1, - KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON + </root>""" + .trimIndent() + val validInputGestures = + listOf( + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_1, + KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON, + ) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) - .build(), - InputGestureData.Builder() - .setTrigger( - InputGestureData.createKeyTrigger( - KeyEvent.KEYCODE_2, - KeyEvent.META_META_ON + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger(KeyEvent.KEYCODE_2, KeyEvent.META_META_ON) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) - .build(), - ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) + .build(), + ) val inputStream = ByteArrayInputStream(xmlData.toByteArray(Charsets.UTF_8)) - assertEquals( - validInputGestures, - inputDataStore.readInputGesturesXml(inputStream, true) - ) + assertEquals(validInputGestures, inputDataStore.readInputGesturesXml(inputStream, true)) } @Test fun invalidTouchpadGestureType() { - val xmlData = """ + val xmlData = + """ <?xml version='1.0' encoding='utf-8' standalone='yes' ?> <root> <input_gesture_list> @@ -444,61 +434,55 @@ class InputDataStoreTests { <touchpad_trigger touchpad_gesture_type="9999" /> </input_gesture> </input_gesture_list> - </root>""".trimIndent() - val validInputGestures = listOf( - InputGestureData.Builder() - .setTrigger( - InputGestureData.createKeyTrigger( - KeyEvent.KEYCODE_1, - KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON + </root>""" + .trimIndent() + val validInputGestures = + listOf( + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_1, + KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON, + ) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) - .build(), - InputGestureData.Builder() - .setTrigger( - InputGestureData.createKeyTrigger( - KeyEvent.KEYCODE_2, - KeyEvent.META_META_ON + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger(KeyEvent.KEYCODE_2, KeyEvent.META_META_ON) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) - .build(), - ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) + .build(), + ) val inputStream = ByteArrayInputStream(xmlData.toByteArray(Charsets.UTF_8)) - assertEquals( - validInputGestures, - inputDataStore.readInputGesturesXml(inputStream, true) - ) + assertEquals(validInputGestures, inputDataStore.readInputGesturesXml(inputStream, true)) } @Test fun emptyInputGestureList() { - val xmlData = """ + val xmlData = + """ <?xml version='1.0' encoding='utf-8' standalone='yes' ?> <root> <input_gesture_list> </input_gesture_list> - </root>""".trimIndent() + </root>""" + .trimIndent() val inputStream = ByteArrayInputStream(xmlData.toByteArray(Charsets.UTF_8)) - assertEquals( - listOf(), - inputDataStore.readInputGesturesXml(inputStream, true) - ) + assertEquals(listOf(), inputDataStore.readInputGesturesXml(inputStream, true)) } @Test fun invalidTag() { - val xmlData = """ + val xmlData = + """ <?xml version='1.0' encoding='utf-8' standalone='yes' ?> <root> <invalid_tag_name> </invalid_tag_name> - </root>""".trimIndent() + </root>""" + .trimIndent() val inputStream = ByteArrayInputStream(xmlData.toByteArray(Charsets.UTF_8)) - assertEquals( - listOf(), - inputDataStore.readInputGesturesXml(inputStream, true) - ) + assertEquals(listOf(), inputDataStore.readInputGesturesXml(inputStream, true)) } -}
\ No newline at end of file +} diff --git a/tests/Input/src/com/android/server/input/InputGestureManagerTests.kt b/tests/Input/src/com/android/server/input/InputGestureManagerTests.kt index e281a3fb1287..5b4a39b49146 100644 --- a/tests/Input/src/com/android/server/input/InputGestureManagerTests.kt +++ b/tests/Input/src/com/android/server/input/InputGestureManagerTests.kt @@ -29,8 +29,7 @@ import org.junit.Test /** * Tests for custom keyboard glyph map configuration. * - * Build/Install/Run: - * atest InputTests:CustomInputGestureManagerTests + * Build/Install/Run: atest InputTests:CustomInputGestureManagerTests */ @Presubmit class InputGestureManagerTests { @@ -48,163 +47,144 @@ class InputGestureManagerTests { @Test fun addRemoveCustomGesture() { - val customGesture = InputGestureData.Builder() - .setTrigger( - InputGestureData.createKeyTrigger( - KeyEvent.KEYCODE_H, - KeyEvent.META_META_ON + val customGesture = + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger(KeyEvent.KEYCODE_H, KeyEvent.META_META_ON) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) - .build() + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() val result = inputGestureManager.addCustomInputGesture(USER_ID, customGesture) assertEquals(InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS, result) assertEquals( listOf(customGesture), - inputGestureManager.getCustomInputGestures(USER_ID, /* filter = */null) + inputGestureManager.getCustomInputGestures(USER_ID, /* filter= */ null), ) inputGestureManager.removeCustomInputGesture(USER_ID, customGesture) assertEquals( listOf<InputGestureData>(), - inputGestureManager.getCustomInputGestures(USER_ID, /* filter = */null) + inputGestureManager.getCustomInputGestures(USER_ID, /* filter= */ null), ) } @Test fun removeNonExistentGesture() { - val customGesture = InputGestureData.Builder() - .setTrigger( - InputGestureData.createKeyTrigger( - KeyEvent.KEYCODE_H, - KeyEvent.META_META_ON + val customGesture = + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger(KeyEvent.KEYCODE_H, KeyEvent.META_META_ON) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) - .build() + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() val result = inputGestureManager.removeCustomInputGesture(USER_ID, customGesture) assertEquals(InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_DOES_NOT_EXIST, result) assertEquals( listOf<InputGestureData>(), - inputGestureManager.getCustomInputGestures(USER_ID, /* filter = */null) + inputGestureManager.getCustomInputGestures(USER_ID, /* filter= */ null), ) } @Test fun addAlreadyExistentGesture() { - val customGesture = InputGestureData.Builder() - .setTrigger( - InputGestureData.createKeyTrigger( - KeyEvent.KEYCODE_H, - KeyEvent.META_META_ON + val customGesture = + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger(KeyEvent.KEYCODE_H, KeyEvent.META_META_ON) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) - .build() + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() inputGestureManager.addCustomInputGesture(USER_ID, customGesture) - val customGesture2 = InputGestureData.Builder() - .setTrigger( - InputGestureData.createKeyTrigger( - KeyEvent.KEYCODE_H, - KeyEvent.META_META_ON + val customGesture2 = + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger(KeyEvent.KEYCODE_H, KeyEvent.META_META_ON) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) - .build() + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) + .build() val result = inputGestureManager.addCustomInputGesture(USER_ID, customGesture2) assertEquals(InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS, result) assertEquals( listOf(customGesture), - inputGestureManager.getCustomInputGestures(USER_ID, /* filter = */null) + inputGestureManager.getCustomInputGestures(USER_ID, /* filter= */ null), ) } @Test fun addRemoveAllExistentGestures() { - val customGesture = InputGestureData.Builder() - .setTrigger( - InputGestureData.createKeyTrigger( - KeyEvent.KEYCODE_H, - KeyEvent.META_META_ON + val customGesture = + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger(KeyEvent.KEYCODE_H, KeyEvent.META_META_ON) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) - .build() + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() inputGestureManager.addCustomInputGesture(USER_ID, customGesture) - val customGesture2 = InputGestureData.Builder() - .setTrigger( - InputGestureData.createKeyTrigger( - KeyEvent.KEYCODE_DEL, - KeyEvent.META_META_ON + val customGesture2 = + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger(KeyEvent.KEYCODE_DEL, KeyEvent.META_META_ON) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) - .build() + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) + .build() inputGestureManager.addCustomInputGesture(USER_ID, customGesture2) assertEquals( listOf(customGesture, customGesture2), - inputGestureManager.getCustomInputGestures(USER_ID, /* filter = */null) + inputGestureManager.getCustomInputGestures(USER_ID, /* filter= */ null), ) - inputGestureManager.removeAllCustomInputGestures(USER_ID, /* filter = */null) + inputGestureManager.removeAllCustomInputGestures(USER_ID, /* filter= */ null) assertEquals( listOf<InputGestureData>(), - inputGestureManager.getCustomInputGestures(USER_ID, /* filter = */null) + inputGestureManager.getCustomInputGestures(USER_ID, /* filter= */ null), ) } @Test fun filteringBasedOnTouchpadOrKeyGestures() { - val customKeyGesture = InputGestureData.Builder() - .setTrigger( - InputGestureData.createKeyTrigger( - KeyEvent.KEYCODE_H, - KeyEvent.META_META_ON + val customKeyGesture = + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger(KeyEvent.KEYCODE_H, KeyEvent.META_META_ON) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) - .build() + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() inputGestureManager.addCustomInputGesture(USER_ID, customKeyGesture) - val customTouchpadGesture = InputGestureData.Builder() - .setTrigger( - InputGestureData.createTouchpadTrigger( - InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP + val customTouchpadGesture = + InputGestureData.Builder() + .setTrigger( + InputGestureData.createTouchpadTrigger( + InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP + ) ) - ) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) - .build() + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) + .build() inputGestureManager.addCustomInputGesture(USER_ID, customTouchpadGesture) assertEquals( listOf(customTouchpadGesture, customKeyGesture), - inputGestureManager.getCustomInputGestures(USER_ID, /* filter = */null) + inputGestureManager.getCustomInputGestures(USER_ID, /* filter= */ null), ) assertEquals( listOf(customKeyGesture), - inputGestureManager.getCustomInputGestures(USER_ID, InputGestureData.Filter.KEY) + inputGestureManager.getCustomInputGestures(USER_ID, InputGestureData.Filter.KEY), ) assertEquals( listOf(customTouchpadGesture), - inputGestureManager.getCustomInputGestures( - USER_ID, - InputGestureData.Filter.TOUCHPAD - ) + inputGestureManager.getCustomInputGestures(USER_ID, InputGestureData.Filter.TOUCHPAD), ) inputGestureManager.removeAllCustomInputGestures(USER_ID, InputGestureData.Filter.KEY) assertEquals( listOf(customTouchpadGesture), - inputGestureManager.getCustomInputGestures(USER_ID, /* filter = */null) + inputGestureManager.getCustomInputGestures(USER_ID, /* filter= */ null), ) - inputGestureManager.removeAllCustomInputGestures( - USER_ID, - InputGestureData.Filter.TOUCHPAD - ) + inputGestureManager.removeAllCustomInputGestures(USER_ID, InputGestureData.Filter.TOUCHPAD) assertEquals( listOf<InputGestureData>(), - inputGestureManager.getCustomInputGestures(USER_ID, /* filter = */null) + inputGestureManager.getCustomInputGestures(USER_ID, /* filter= */ null), ) } -}
\ No newline at end of file +} diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt index 71c7a6b1119d..1858b1da916b 100644 --- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt +++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt @@ -16,7 +16,6 @@ package com.android.server.input - import android.Manifest import android.content.Context import android.content.ContextWrapper @@ -39,14 +38,14 @@ import android.platform.test.annotations.EnableFlags import android.platform.test.annotations.Presubmit import android.platform.test.flag.junit.SetFlagsRule import android.provider.Settings -import android.view.View.OnKeyListener +import android.test.mock.MockContentResolver import android.view.InputDevice import android.view.KeyCharacterMap import android.view.KeyEvent import android.view.SurfaceHolder import android.view.SurfaceView +import android.view.View.OnKeyListener import android.view.WindowManager -import android.test.mock.MockContentResolver import androidx.test.platform.app.InstrumentationRegistry import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity import com.android.dx.mockito.inline.extended.ExtendedMockito @@ -75,28 +74,33 @@ import org.mockito.Mockito.never import org.mockito.Mockito.spy import org.mockito.Mockito.times import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.verifyNoInteractions +import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.`when` import org.mockito.stubbing.OngoingStubbing /** * Tests for {@link InputManagerService}. * - * Build/Install/Run: - * atest InputTests:InputManagerServiceTests + * Build/Install/Run: atest InputTests:InputManagerServiceTests */ @Presubmit class InputManagerServiceTests { companion object { - val ACTION_KEY_EVENTS = listOf( - KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_META_LEFT), - KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_META_RIGHT), - KeyEvent( /* downTime= */0, /* eventTime= */0, /* action= */0, /* code= */0, - /* repeat= */0, KeyEvent.META_META_ON + val ACTION_KEY_EVENTS = + listOf( + KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_META_LEFT), + KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_META_RIGHT), + KeyEvent( + /* downTime= */ 0, + /* eventTime= */ 0, + /* action= */ 0, + /* code= */ 0, + /* repeat= */ 0, + KeyEvent.META_META_ON, + ), ) - ) } @get:Rule @@ -108,32 +112,24 @@ class InputManagerServiceTests { .mockStatic(InputSettings::class.java) .build()!! - @get:Rule - val setFlagsRule = SetFlagsRule() + @get:Rule val setFlagsRule = SetFlagsRule() - @get:Rule - val fakeSettingsProviderRule = FakeSettingsProvider.rule()!! + @get:Rule val fakeSettingsProviderRule = FakeSettingsProvider.rule()!! - @Mock - private lateinit var native: NativeInputManagerService + @Mock private lateinit var native: NativeInputManagerService - @Mock - private lateinit var wmCallbacks: InputManagerService.WindowManagerCallbacks + @Mock private lateinit var wmCallbacks: InputManagerService.WindowManagerCallbacks - @Mock - private lateinit var windowManagerInternal: WindowManagerInternal + @Mock private lateinit var windowManagerInternal: WindowManagerInternal - @Mock - private lateinit var packageManagerInternal: PackageManagerInternal + @Mock private lateinit var packageManagerInternal: PackageManagerInternal - @Mock - private lateinit var uEventManager: UEventManager + @Mock private lateinit var uEventManager: UEventManager @Mock private lateinit var kbdController: InputManagerService.KeyboardBacklightControllerInterface - @Mock - private lateinit var kcm: KeyCharacterMap + @Mock private lateinit var kcm: KeyCharacterMap private lateinit var service: InputManagerService private lateinit var localService: InputManagerInternal @@ -147,44 +143,50 @@ class InputManagerServiceTests { fun setup() { context = spy(ContextWrapper(InstrumentationRegistry.getInstrumentation().getContext())) fakePermissionEnforcer = FakePermissionEnforcer() - doReturn(Context.PERMISSION_ENFORCER_SERVICE).`when`(context).getSystemServiceName( - eq(PermissionEnforcer::class.java) - ) - doReturn(fakePermissionEnforcer).`when`(context).getSystemService( - eq(Context.PERMISSION_ENFORCER_SERVICE) - ) + doReturn(Context.PERMISSION_ENFORCER_SERVICE) + .`when`(context) + .getSystemServiceName(eq(PermissionEnforcer::class.java)) + doReturn(fakePermissionEnforcer) + .`when`(context) + .getSystemService(eq(Context.PERMISSION_ENFORCER_SERVICE)) contentResolver = MockContentResolver(context) contentResolver.addProvider(Settings.AUTHORITY, FakeSettingsProvider()) whenever(context.contentResolver).thenReturn(contentResolver) testLooper = TestLooper() service = - InputManagerService(object : InputManagerService.Injector( - context, testLooper.looper, testLooper.looper, uEventManager) { - override fun getNativeService( - service: InputManagerService? - ): NativeInputManagerService { - return native - } - - override fun registerLocalService(service: InputManagerInternal?) { - localService = service!! - } - - override fun getKeyboardBacklightController( - nativeService: NativeInputManagerService? - ): InputManagerService.KeyboardBacklightControllerInterface { - return kbdController - } - }, fakePermissionEnforcer) + InputManagerService( + object : + InputManagerService.Injector( + context, + testLooper.looper, + testLooper.looper, + uEventManager, + ) { + override fun getNativeService( + service: InputManagerService? + ): NativeInputManagerService { + return native + } + + override fun registerLocalService(service: InputManagerInternal?) { + localService = service!! + } + + override fun getKeyboardBacklightController( + nativeService: NativeInputManagerService? + ): InputManagerService.KeyboardBacklightControllerInterface { + return kbdController + } + }, + fakePermissionEnforcer, + ) inputManagerGlobalSession = InputManagerGlobal.createTestSession(service) val inputManager = InputManager(context) whenever(context.getSystemService(InputManager::class.java)).thenReturn(inputManager) whenever(context.getSystemService(Context.INPUT_SERVICE)).thenReturn(inputManager) whenever(context.checkCallingOrSelfPermission(Manifest.permission.MANAGE_KEY_GESTURES)) - .thenReturn( - PackageManager.PERMISSION_GRANTED - ) + .thenReturn(PackageManager.PERMISSION_GRANTED) ExtendedMockito.doReturn(windowManagerInternal).`when` { LocalServices.getService(eq(WindowManagerInternal::class.java)) @@ -192,9 +194,7 @@ class InputManagerServiceTests { ExtendedMockito.doReturn(packageManagerInternal).`when` { LocalServices.getService(eq(PackageManagerInternal::class.java)) } - ExtendedMockito.doReturn(kcm).`when` { - KeyCharacterMap.load(anyInt()) - } + ExtendedMockito.doReturn(kcm).`when` { KeyCharacterMap.load(anyInt()) } assertTrue("Local service must be registered", this::localService.isInitialized) service.setWindowManagerCallbacks(wmCallbacks) @@ -219,9 +219,7 @@ class InputManagerServiceTests { fun testInputSettingsUpdatedOnSystemRunning() { verifyNoInteractions(native) - runWithShellPermissionIdentity { - service.systemRunning() - } + runWithShellPermissionIdentity { service.systemRunning() } verify(native).setPointerSpeed(anyInt()) verify(native).setTouchpadPointerSpeed(anyInt()) @@ -238,8 +236,7 @@ class InputManagerServiceTests { verify(native).setStylusPointerIconEnabled(anyBoolean()) // Called thrice at boot, since there are individual callbacks to update the // key repeat timeout, the key repeat delay and whether key repeat enabled. - verify(native, times(3)).setKeyRepeatConfiguration(anyInt(), anyInt(), - anyBoolean()) + verify(native, times(3)).setKeyRepeatConfiguration(anyInt(), anyInt(), anyBoolean()) } @Test @@ -259,7 +256,9 @@ class InputManagerServiceTests { localService.setTypeAssociation(inputPort, type) - assertThat(service.getDeviceTypeAssociations()).asList().containsExactly(inputPort, type) + assertThat(service.getDeviceTypeAssociations()) + .asList() + .containsExactly(inputPort, type) .inOrder() } @@ -290,8 +289,8 @@ class InputManagerServiceTests { fun testActionKeyEventsForwardedToFocusedWindow_whenCorrectlyRequested() { service.systemRunning() overrideSendActionKeyEventsToFocusedWindow( - /* hasPermission = */true, - /* hasPrivateFlag = */true + /* hasPermission = */ true, + /* hasPrivateFlag = */ true, ) whenever(wmCallbacks.interceptKeyBeforeDispatching(any(), any(), anyInt())).thenReturn(-1) @@ -304,8 +303,8 @@ class InputManagerServiceTests { fun testActionKeyEventsNotForwardedToFocusedWindow_whenNoPermissions() { service.systemRunning() overrideSendActionKeyEventsToFocusedWindow( - /* hasPermission = */false, - /* hasPrivateFlag = */true + /* hasPermission = */ false, + /* hasPrivateFlag = */ true, ) whenever(wmCallbacks.interceptKeyBeforeDispatching(any(), any(), anyInt())).thenReturn(-1) @@ -318,8 +317,8 @@ class InputManagerServiceTests { fun testActionKeyEventsNotForwardedToFocusedWindow_whenNoPrivateFlag() { service.systemRunning() overrideSendActionKeyEventsToFocusedWindow( - /* hasPermission = */true, - /* hasPrivateFlag = */false + /* hasPermission = */ true, + /* hasPrivateFlag = */ false, ) whenever(wmCallbacks.interceptKeyBeforeDispatching(any(), any(), anyInt())).thenReturn(-1) @@ -362,13 +361,20 @@ class InputManagerServiceTests { fun testKeyEventsForwardedToFocusedWindow_whenWmAllows() { service.systemRunning() overrideSendActionKeyEventsToFocusedWindow( - /* hasPermission = */false, - /* hasPrivateFlag = */false + /* hasPermission = */ false, + /* hasPrivateFlag = */ false, ) whenever(wmCallbacks.interceptKeyBeforeDispatching(any(), any(), anyInt())).thenReturn(0) - val event = KeyEvent( /* downTime= */0, /* eventTime= */0, KeyEvent.ACTION_DOWN, - KeyEvent.KEYCODE_SPACE, /* repeat= */0, KeyEvent.META_CTRL_ON) + val event = + KeyEvent( + /* downTime= */ 0, + /* eventTime= */ 0, + KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_SPACE, + /* repeat= */ 0, + KeyEvent.META_CTRL_ON, + ) assertEquals(0, service.interceptKeyBeforeDispatching(null, event, 0)) } @@ -399,13 +405,20 @@ class InputManagerServiceTests { fun testKeyEventsNotForwardedToFocusedWindow_whenWmConsumes() { service.systemRunning() overrideSendActionKeyEventsToFocusedWindow( - /* hasPermission = */false, - /* hasPrivateFlag = */false + /* hasPermission = */ false, + /* hasPrivateFlag = */ false, ) whenever(wmCallbacks.interceptKeyBeforeDispatching(any(), any(), anyInt())).thenReturn(-1) - val event = KeyEvent( /* downTime= */0, /* eventTime= */0, KeyEvent.ACTION_DOWN, - KeyEvent.KEYCODE_SPACE, /* repeat= */0, KeyEvent.META_CTRL_ON) + val event = + KeyEvent( + /* downTime= */ 0, + /* eventTime= */ 0, + KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_SPACE, + /* repeat= */ 0, + KeyEvent.META_CTRL_ON, + ) assertEquals(-1, service.interceptKeyBeforeDispatching(null, event, 0)) } @@ -420,19 +433,20 @@ class InputManagerServiceTests { } private fun createVirtualDisplays(count: Int): AutoClosingVirtualDisplays { - val displayManager: DisplayManager = context.getSystemService( - DisplayManager::class.java - ) as DisplayManager + val displayManager: DisplayManager = + context.getSystemService(DisplayManager::class.java) as DisplayManager val virtualDisplays = mutableListOf<VirtualDisplay>() for (i in 0 until count) { - virtualDisplays.add(displayManager.createVirtualDisplay( + virtualDisplays.add( + displayManager.createVirtualDisplay( /* displayName= */ "testVirtualDisplay$i", /* width= */ 100, /* height= */ 100, /* densityDpi= */ 100, /* surface= */ null, - /* flags= */ 0 - )) + /* flags= */ 0, + ) + ) } return AutoClosingVirtualDisplays(virtualDisplays) } @@ -441,26 +455,26 @@ class InputManagerServiceTests { private fun createKeycodeAEvent(inputDevice: InputDevice, action: Int): KeyEvent { val eventTime = SystemClock.uptimeMillis() return KeyEvent( - /* downTime= */ eventTime, - /* eventTime= */ eventTime, - /* action= */ action, - /* code= */ KeyEvent.KEYCODE_A, - /* repeat= */ 0, - /* metaState= */ 0, - /* deviceId= */ inputDevice.id, - /* scanCode= */ 0, - /* flags= */ KeyEvent.FLAG_FROM_SYSTEM, - /* source= */ InputDevice.SOURCE_KEYBOARD + /* downTime= */ eventTime, + /* eventTime= */ eventTime, + /* action= */ action, + /* code= */ KeyEvent.KEYCODE_A, + /* repeat= */ 0, + /* metaState= */ 0, + /* deviceId= */ inputDevice.id, + /* scanCode= */ 0, + /* flags= */ KeyEvent.FLAG_FROM_SYSTEM, + /* source= */ InputDevice.SOURCE_KEYBOARD, ) } private fun createInputDevice(): InputDevice { return InputDevice.Builder() - .setId(123) - .setName("abc") - .setDescriptor("def") - .setSources(InputDevice.SOURCE_KEYBOARD) - .build() + .setId(123) + .setName("abc") + .setDescriptor("def") + .setSources(InputDevice.SOURCE_KEYBOARD) + .build() } @Test @@ -485,8 +499,8 @@ class InputManagerServiceTests { // Associate input device with display service.addUniqueIdAssociationByDescriptor( - inputDevice.descriptor, - virtualDisplays[0].display.displayId.toString() + inputDevice.descriptor, + virtualDisplays[0].display.displayId.toString(), ) // Simulate 2 different KeyEvents @@ -513,8 +527,8 @@ class InputManagerServiceTests { // Associate with Display 2 service.addUniqueIdAssociationByDescriptor( - inputDevice.descriptor, - virtualDisplays[1].display.displayId.toString() + inputDevice.descriptor, + virtualDisplays[1].display.displayId.toString(), ) // Simulate a KeyEvent @@ -548,8 +562,8 @@ class InputManagerServiceTests { // Associate input device with display service.addUniqueIdAssociationByPort( - inputDevice.name, - virtualDisplays[0].display.displayId.toString() + inputDevice.name, + virtualDisplays[0].display.displayId.toString(), ) // Simulate 2 different KeyEvents @@ -576,8 +590,8 @@ class InputManagerServiceTests { // Associate with Display 2 service.addUniqueIdAssociationByPort( - inputDevice.name, - virtualDisplays[1].display.displayId.toString() + inputDevice.name, + virtualDisplays[1].display.displayId.toString(), ) // Simulate a KeyEvent @@ -619,7 +633,7 @@ class InputManagerServiceTests { ExtendedMockito.verify { InputSettings.setAccessibilityBounceKeysThreshold( any(), - eq(InputSettings.DEFAULT_BOUNCE_KEYS_THRESHOLD_MILLIS) + eq(InputSettings.DEFAULT_BOUNCE_KEYS_THRESHOLD_MILLIS), ) } } @@ -635,9 +649,7 @@ class InputManagerServiceTests { .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE) .build() service.handleKeyGestureEvent(toggleMouseKeysEvent) - ExtendedMockito.verify { - InputSettings.setAccessibilityMouseKeysEnabled(any(), eq(true)) - } + ExtendedMockito.verify { InputSettings.setAccessibilityMouseKeysEnabled(any(), eq(true)) } } @Test @@ -648,9 +660,7 @@ class InputManagerServiceTests { .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE) .build() service.handleKeyGestureEvent(toggleStickyKeysEvent) - ExtendedMockito.verify { - InputSettings.setAccessibilityStickyKeysEnabled(any(), eq(true)) - } + ExtendedMockito.verify { InputSettings.setAccessibilityStickyKeysEnabled(any(), eq(true)) } } @Test @@ -664,7 +674,7 @@ class InputManagerServiceTests { ExtendedMockito.verify { InputSettings.setAccessibilitySlowKeysThreshold( any(), - eq(InputSettings.DEFAULT_SLOW_KEYS_THRESHOLD_MILLIS) + eq(InputSettings.DEFAULT_SLOW_KEYS_THRESHOLD_MILLIS), ) } } @@ -683,25 +693,26 @@ class InputManagerServiceTests { fun overrideSendActionKeyEventsToFocusedWindow( hasPermission: Boolean, - hasPrivateFlag: Boolean + hasPrivateFlag: Boolean, ) { ExtendedMockito.doReturn( - if (hasPermission) { - PermissionChecker.PERMISSION_GRANTED - } else { - PermissionChecker.PERMISSION_HARD_DENIED - } - ).`when` { - PermissionChecker.checkPermissionForDataDelivery( - any(), - eq(Manifest.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW), - anyInt(), - anyInt(), - any(), - any(), - any() + if (hasPermission) { + PermissionChecker.PERMISSION_GRANTED + } else { + PermissionChecker.PERMISSION_HARD_DENIED + } ) - } + .`when` { + PermissionChecker.checkPermissionForDataDelivery( + any(), + eq(Manifest.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW), + anyInt(), + anyInt(), + any(), + any(), + any(), + ) + } val info = KeyInterceptionInfo( /* type = */0, @@ -711,8 +722,7 @@ class InputManagerServiceTests { 0 }, "title", - /* uid = */0, - /* inputFeatureFlags = */ 0 + /* uid = */0 ) whenever(windowManagerInternal.getKeyInterceptionInfoFromToken(any())).thenReturn(info) } diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index 163dda84a71c..cdc4256a5fd4 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -77,8 +77,7 @@ import org.mockito.kotlin.times /** * Tests for {@link KeyGestureController}. * - * Build/Install/Run: - * atest InputTests:KeyGestureControllerTests + * Build/Install/Run: atest InputTests:KeyGestureControllerTests */ @Presubmit @RunWith(JUnitParamsRunner::class) @@ -86,23 +85,29 @@ class KeyGestureControllerTests { companion object { const val DEVICE_ID = 1 - val HOME_GESTURE_COMPLETE_EVENT = KeyGestureEvent.Builder() - .setDeviceId(DEVICE_ID) - .setKeycodes(intArrayOf(KeyEvent.KEYCODE_H)) - .setModifierState(KeyEvent.META_META_ON or KeyEvent.META_META_LEFT_ON) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) - .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE) - .build() - val MODIFIER = mapOf( - KeyEvent.KEYCODE_CTRL_LEFT to (KeyEvent.META_CTRL_LEFT_ON or KeyEvent.META_CTRL_ON), - KeyEvent.KEYCODE_CTRL_RIGHT to (KeyEvent.META_CTRL_RIGHT_ON or KeyEvent.META_CTRL_ON), - KeyEvent.KEYCODE_ALT_LEFT to (KeyEvent.META_ALT_LEFT_ON or KeyEvent.META_ALT_ON), - KeyEvent.KEYCODE_ALT_RIGHT to (KeyEvent.META_ALT_RIGHT_ON or KeyEvent.META_ALT_ON), - KeyEvent.KEYCODE_SHIFT_LEFT to (KeyEvent.META_SHIFT_LEFT_ON or KeyEvent.META_SHIFT_ON), - KeyEvent.KEYCODE_SHIFT_RIGHT to (KeyEvent.META_SHIFT_RIGHT_ON or KeyEvent.META_SHIFT_ON), - KeyEvent.KEYCODE_META_LEFT to (KeyEvent.META_META_LEFT_ON or KeyEvent.META_META_ON), - KeyEvent.KEYCODE_META_RIGHT to (KeyEvent.META_META_RIGHT_ON or KeyEvent.META_META_ON), - ) + val HOME_GESTURE_COMPLETE_EVENT = + KeyGestureEvent.Builder() + .setDeviceId(DEVICE_ID) + .setKeycodes(intArrayOf(KeyEvent.KEYCODE_H)) + .setModifierState(KeyEvent.META_META_ON or KeyEvent.META_META_LEFT_ON) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + .build() + val MODIFIER = + mapOf( + KeyEvent.KEYCODE_CTRL_LEFT to (KeyEvent.META_CTRL_LEFT_ON or KeyEvent.META_CTRL_ON), + KeyEvent.KEYCODE_CTRL_RIGHT to + (KeyEvent.META_CTRL_RIGHT_ON or KeyEvent.META_CTRL_ON), + KeyEvent.KEYCODE_ALT_LEFT to (KeyEvent.META_ALT_LEFT_ON or KeyEvent.META_ALT_ON), + KeyEvent.KEYCODE_ALT_RIGHT to (KeyEvent.META_ALT_RIGHT_ON or KeyEvent.META_ALT_ON), + KeyEvent.KEYCODE_SHIFT_LEFT to + (KeyEvent.META_SHIFT_LEFT_ON or KeyEvent.META_SHIFT_ON), + KeyEvent.KEYCODE_SHIFT_RIGHT to + (KeyEvent.META_SHIFT_RIGHT_ON or KeyEvent.META_SHIFT_ON), + KeyEvent.KEYCODE_META_LEFT to (KeyEvent.META_META_LEFT_ON or KeyEvent.META_META_ON), + KeyEvent.KEYCODE_META_RIGHT to + (KeyEvent.META_META_RIGHT_ON or KeyEvent.META_META_ON), + ) const val SEARCH_KEY_BEHAVIOR_DEFAULT_SEARCH = 0 const val SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY = 1 const val SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY = 0 @@ -116,15 +121,14 @@ class KeyGestureControllerTests { @JvmField @Rule - val extendedMockitoRule = ExtendedMockitoRule.Builder(this) - .mockStatic(FrameworkStatsLog::class.java) - .mockStatic(SystemProperties::class.java) - .mockStatic(KeyCharacterMap::class.java) - .build()!! + val extendedMockitoRule = + ExtendedMockitoRule.Builder(this) + .mockStatic(FrameworkStatsLog::class.java) + .mockStatic(SystemProperties::class.java) + .mockStatic(KeyCharacterMap::class.java) + .build()!! - @JvmField - @Rule - val rule = SetFlagsRule() + @JvmField @Rule val rule = SetFlagsRule() @Mock private lateinit var iInputManager: IInputManager @Mock private lateinit var packageManager: PackageManager @@ -151,29 +155,35 @@ class KeyGestureControllerTests { currentPid = Process.myPid() tempFile = File.createTempFile("input_gestures", ".xml") inputDataStore = - InputDataStore(object : InputDataStore.FileInjector("input_gestures.xml") { - private val atomicFile: AtomicFile = AtomicFile(tempFile) + InputDataStore( + object : InputDataStore.FileInjector("input_gestures.xml") { + private val atomicFile: AtomicFile = AtomicFile(tempFile) - override fun openRead(userId: Int): InputStream? { - return atomicFile.openRead() - } + override fun openRead(userId: Int): InputStream? { + return atomicFile.openRead() + } - override fun startWrite(userId: Int): FileOutputStream? { - return atomicFile.startWrite() - } + override fun startWrite(userId: Int): FileOutputStream? { + return atomicFile.startWrite() + } - override fun finishWrite(userId: Int, fos: FileOutputStream?, success: Boolean) { - if (success) { - atomicFile.finishWrite(fos) - } else { - atomicFile.failWrite(fos) + override fun finishWrite( + userId: Int, + fos: FileOutputStream?, + success: Boolean, + ) { + if (success) { + atomicFile.finishWrite(fos) + } else { + atomicFile.failWrite(fos) + } } - } - override fun getAtomicFileForUserId(userId: Int): AtomicFile { - return atomicFile + override fun getAtomicFileForUserId(userId: Int): AtomicFile { + return atomicFile + } } - }) + ) startNewInputGlobalTestSession() } @@ -230,11 +240,12 @@ class KeyGestureControllerTests { object : KeyGestureController.Injector() { override fun getAccessibilityShortcutController( context: Context?, - handler: Handler? + handler: Handler?, ): AccessibilityShortcutController { return accessibilityShortcutController } - }) + }, + ) Mockito.`when`(iInputManager.registerKeyGestureHandler(Mockito.any(), Mockito.any())) .thenAnswer { val args = it.arguments @@ -242,14 +253,18 @@ class KeyGestureControllerTests { keyGestureController.registerKeyGestureHandler( args[0] as IntArray, args[1] as IKeyGestureHandler, - SYSTEM_PID + SYSTEM_PID, ) } - } + } keyGestureController.setWindowManagerCallbacks(wmCallbacks) Mockito.`when`(wmCallbacks.isKeyguardLocked(Mockito.anyInt())).thenReturn(false) - Mockito.`when`(accessibilityShortcutController - .isAccessibilityShortcutAvailable(Mockito.anyBoolean())).thenReturn(true) + Mockito.`when`( + accessibilityShortcutController.isAccessibilityShortcutAvailable( + Mockito.anyBoolean() + ) + ) + .thenReturn(true) Mockito.`when`(iInputManager.appLaunchBookmarks) .thenReturn(keyGestureController.appLaunchBookmarks) keyGestureController.systemRunning() @@ -258,9 +273,10 @@ class KeyGestureControllerTests { private fun notifyHomeGestureCompleted() { keyGestureController.notifyKeyGestureCompleted( - DEVICE_ID, intArrayOf(KeyEvent.KEYCODE_H), + DEVICE_ID, + intArrayOf(KeyEvent.KEYCODE_H), KeyEvent.META_META_ON or KeyEvent.META_META_LEFT_ON, - KeyGestureEvent.KEY_GESTURE_TYPE_HOME + KeyGestureEvent.KEY_GESTURE_TYPE_HOME, ) } @@ -273,15 +289,11 @@ class KeyGestureControllerTests { keyGestureController.registerKeyGestureEventListener(listener, 0) notifyHomeGestureCompleted() testLooper.dispatchAll() - assertEquals( - "Listener should get callbacks on key gesture event completed", - 1, - events.size - ) + assertEquals("Listener should get callbacks on key gesture event completed", 1, events.size) assertEquals( "Listener should get callback for key gesture complete event", HOME_GESTURE_COMPLETE_EVENT, - events[0] + events[0], ) // Unregister listener @@ -289,11 +301,7 @@ class KeyGestureControllerTests { keyGestureController.unregisterKeyGestureEventListener(listener, 0) notifyHomeGestureCompleted() testLooper.dispatchAll() - assertEquals( - "Listener should not get callback after being unregistered", - 0, - events.size - ) + assertEquals("Listener should not get callback after being unregistered", 0, events.size) } class TestData( @@ -317,7 +325,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT, intArrayOf(KeyEvent.KEYCODE_A), KeyEvent.META_META_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + H -> Go Home", @@ -325,7 +333,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_HOME, intArrayOf(KeyEvent.KEYCODE_H), KeyEvent.META_META_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + ENTER -> Go Home", @@ -333,7 +341,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_HOME, intArrayOf(KeyEvent.KEYCODE_ENTER), KeyEvent.META_META_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + I -> Launch System Settings", @@ -341,7 +349,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS, intArrayOf(KeyEvent.KEYCODE_I), KeyEvent.META_META_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + L -> Lock", @@ -349,7 +357,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN, intArrayOf(KeyEvent.KEYCODE_L), KeyEvent.META_META_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + N -> Toggle Notification", @@ -357,18 +365,15 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL, intArrayOf(KeyEvent.KEYCODE_N), KeyEvent.META_META_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + S -> Take Screenshot", - intArrayOf( - KeyEvent.KEYCODE_META_LEFT, - KeyEvent.KEYCODE_S - ), + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_S), KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT, intArrayOf(KeyEvent.KEYCODE_S), KeyEvent.META_META_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + ESC -> Back", @@ -376,7 +381,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_BACK, intArrayOf(KeyEvent.KEYCODE_ESCAPE), KeyEvent.META_META_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + DPAD_LEFT -> Back", @@ -384,55 +389,55 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_BACK, intArrayOf(KeyEvent.KEYCODE_DPAD_LEFT), KeyEvent.META_META_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + CTRL + DPAD_UP -> Multi Window Navigation", intArrayOf( KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_CTRL_LEFT, - KeyEvent.KEYCODE_DPAD_UP + KeyEvent.KEYCODE_DPAD_UP, ), KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION, intArrayOf(KeyEvent.KEYCODE_DPAD_UP), KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + CTRL + DPAD_DOWN -> Desktop Mode", intArrayOf( KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_CTRL_LEFT, - KeyEvent.KEYCODE_DPAD_DOWN + KeyEvent.KEYCODE_DPAD_DOWN, ), KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE, intArrayOf(KeyEvent.KEYCODE_DPAD_DOWN), KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + CTRL + DPAD_LEFT -> Splitscreen Navigation Left", intArrayOf( KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_CTRL_LEFT, - KeyEvent.KEYCODE_DPAD_LEFT + KeyEvent.KEYCODE_DPAD_LEFT, ), KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT, intArrayOf(KeyEvent.KEYCODE_DPAD_LEFT), KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + CTRL + DPAD_RIGHT -> Splitscreen Navigation Right", intArrayOf( KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_CTRL_LEFT, - KeyEvent.KEYCODE_DPAD_RIGHT + KeyEvent.KEYCODE_DPAD_RIGHT, ), KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT, intArrayOf(KeyEvent.KEYCODE_DPAD_RIGHT), KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + / -> Open Shortcut Helper", @@ -440,7 +445,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER, intArrayOf(KeyEvent.KEYCODE_SLASH), KeyEvent.META_META_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + ALT -> Toggle Caps Lock", @@ -448,7 +453,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_ALT_LEFT), 0, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "ALT + META -> Toggle Caps Lock", @@ -456,7 +461,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_ALT_LEFT), 0, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + TAB -> Open Overview", @@ -464,7 +469,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS, intArrayOf(KeyEvent.KEYCODE_TAB), KeyEvent.META_META_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "ALT + TAB -> Toggle Recent Apps Switcher", @@ -474,8 +479,8 @@ class KeyGestureControllerTests { KeyEvent.META_ALT_ON, intArrayOf( KeyGestureEvent.ACTION_GESTURE_START, - KeyGestureEvent.ACTION_GESTURE_COMPLETE - ) + KeyGestureEvent.ACTION_GESTURE_COMPLETE, + ), ), TestData( "CTRL + SPACE -> Switch Language Forward", @@ -483,31 +488,31 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH, intArrayOf(KeyEvent.KEYCODE_SPACE), KeyEvent.META_CTRL_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "CTRL + SHIFT + SPACE -> Switch Language Backward", intArrayOf( KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_SHIFT_LEFT, - KeyEvent.KEYCODE_SPACE + KeyEvent.KEYCODE_SPACE, ), KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH, intArrayOf(KeyEvent.KEYCODE_SPACE), KeyEvent.META_CTRL_ON or KeyEvent.META_SHIFT_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "CTRL + ALT + Z -> Accessibility Shortcut", intArrayOf( KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_ALT_LEFT, - KeyEvent.KEYCODE_Z + KeyEvent.KEYCODE_Z, ), KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT, intArrayOf(KeyEvent.KEYCODE_Z), KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + B -> Launch Default Browser", @@ -516,7 +521,7 @@ class KeyGestureControllerTests { intArrayOf(KeyEvent.KEYCODE_B), KeyEvent.META_META_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), - AppLaunchData.createLaunchDataForRole(RoleManager.ROLE_BROWSER) + AppLaunchData.createLaunchDataForRole(RoleManager.ROLE_BROWSER), ), TestData( "META + C -> Launch Default Contacts", @@ -525,7 +530,7 @@ class KeyGestureControllerTests { intArrayOf(KeyEvent.KEYCODE_P), KeyEvent.META_META_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), - AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CONTACTS) + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CONTACTS), ), TestData( "META + E -> Launch Default Email", @@ -534,7 +539,7 @@ class KeyGestureControllerTests { intArrayOf(KeyEvent.KEYCODE_E), KeyEvent.META_META_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), - AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_EMAIL) + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_EMAIL), ), TestData( "META + K -> Launch Default Calendar", @@ -543,7 +548,7 @@ class KeyGestureControllerTests { intArrayOf(KeyEvent.KEYCODE_C), KeyEvent.META_META_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), - AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CALENDAR) + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CALENDAR), ), TestData( "META + M -> Launch Default Maps", @@ -552,7 +557,7 @@ class KeyGestureControllerTests { intArrayOf(KeyEvent.KEYCODE_M), KeyEvent.META_META_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), - AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_MAPS) + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_MAPS), ), TestData( "META + U -> Launch Default Calculator", @@ -561,159 +566,147 @@ class KeyGestureControllerTests { intArrayOf(KeyEvent.KEYCODE_U), KeyEvent.META_META_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), - AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CALCULATOR) + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CALCULATOR), ), TestData( "META + CTRL + DEL -> Trigger Bug Report", intArrayOf( KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_CTRL_LEFT, - KeyEvent.KEYCODE_DEL + KeyEvent.KEYCODE_DEL, ), KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT, intArrayOf(KeyEvent.KEYCODE_DEL), KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "Meta + Alt + 3 -> Toggle Bounce Keys", intArrayOf( KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_ALT_LEFT, - KeyEvent.KEYCODE_3 + KeyEvent.KEYCODE_3, ), KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS, intArrayOf(KeyEvent.KEYCODE_3), KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "Meta + Alt + 4 -> Toggle Mouse Keys", intArrayOf( KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_ALT_LEFT, - KeyEvent.KEYCODE_4 + KeyEvent.KEYCODE_4, ), KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS, intArrayOf(KeyEvent.KEYCODE_4), KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "Meta + Alt + 5 -> Toggle Sticky Keys", intArrayOf( KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_ALT_LEFT, - KeyEvent.KEYCODE_5 + KeyEvent.KEYCODE_5, ), KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS, intArrayOf(KeyEvent.KEYCODE_5), KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "Meta + Alt + 6 -> Toggle Slow Keys", intArrayOf( KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_ALT_LEFT, - KeyEvent.KEYCODE_6 + KeyEvent.KEYCODE_6, ), KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS, intArrayOf(KeyEvent.KEYCODE_6), KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + CTRL + D -> Move a task to next display", intArrayOf( KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_CTRL_LEFT, - KeyEvent.KEYCODE_D + KeyEvent.KEYCODE_D, ), KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY, intArrayOf(KeyEvent.KEYCODE_D), KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + [ -> Resizes a task to fit the left half of the screen", - intArrayOf( - KeyEvent.KEYCODE_META_LEFT, - KeyEvent.KEYCODE_LEFT_BRACKET - ), + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_LEFT_BRACKET), KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW, intArrayOf(KeyEvent.KEYCODE_LEFT_BRACKET), KeyEvent.META_META_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + ] -> Resizes a task to fit the right half of the screen", - intArrayOf( - KeyEvent.KEYCODE_META_LEFT, - KeyEvent.KEYCODE_RIGHT_BRACKET - ), + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_RIGHT_BRACKET), KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW, intArrayOf(KeyEvent.KEYCODE_RIGHT_BRACKET), KeyEvent.META_META_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + '=' -> Toggles maximization of a task to maximized and restore its bounds", - intArrayOf( - KeyEvent.KEYCODE_META_LEFT, - KeyEvent.KEYCODE_EQUALS - ), + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_EQUALS), KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW, intArrayOf(KeyEvent.KEYCODE_EQUALS), KeyEvent.META_META_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + '-' -> Minimizes a freeform task", - intArrayOf( - KeyEvent.KEYCODE_META_LEFT, - KeyEvent.KEYCODE_MINUS - ), + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_MINUS), KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW, intArrayOf(KeyEvent.KEYCODE_MINUS), KeyEvent.META_META_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + ALT + M -> Toggle Magnification", intArrayOf( KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_ALT_LEFT, - KeyEvent.KEYCODE_M + KeyEvent.KEYCODE_M, ), KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION, intArrayOf(KeyEvent.KEYCODE_M), KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + ALT + S -> Activate Select to Speak", intArrayOf( KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_ALT_LEFT, - KeyEvent.KEYCODE_S + KeyEvent.KEYCODE_S, ), KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK, intArrayOf(KeyEvent.KEYCODE_S), KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + ALT + 'V' -> Toggle Voice Access", intArrayOf( KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_ALT_LEFT, - KeyEvent.KEYCODE_V + KeyEvent.KEYCODE_V, ), KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS, intArrayOf(KeyEvent.KEYCODE_V), KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), ) } @@ -727,7 +720,7 @@ class KeyGestureControllerTests { com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES, com.android.hardware.input.Flags.FLAG_ENABLE_VOICE_ACCESS_KEY_GESTURES, com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT, - com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS + com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS, ) fun testKeyGestures(test: TestData) { setupKeyGestureController() @@ -743,26 +736,27 @@ class KeyGestureControllerTests { com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES, com.android.hardware.input.Flags.FLAG_ENABLE_VOICE_ACCESS_KEY_GESTURES, com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT, - com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS + com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS, ) fun testCustomKeyGesturesNotAllowedForSystemGestures(test: TestData) { setupKeyGestureController() - val builder = InputGestureData.Builder() - .setKeyGestureType(test.expectedKeyGestureType) - .setTrigger( - InputGestureData.createKeyTrigger( - test.expectedKeys[0], - test.expectedModifierState + val builder = + InputGestureData.Builder() + .setKeyGestureType(test.expectedKeyGestureType) + .setTrigger( + InputGestureData.createKeyTrigger( + test.expectedKeys[0], + test.expectedModifierState, + ) ) - ) if (test.expectedAppLaunchData != null) { builder.setAppLaunchData(test.expectedAppLaunchData) } assertEquals( test.toString(), InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_RESERVED_GESTURE, - keyGestureController.addCustomInputGesture(0, builder.build().aidlData) + keyGestureController.addCustomInputGesture(0, builder.build().aidlData), ) } @@ -776,7 +770,7 @@ class KeyGestureControllerTests { intArrayOf(KeyEvent.KEYCODE_B), KeyEvent.META_META_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), - AppLaunchData.createLaunchDataForRole(RoleManager.ROLE_BROWSER) + AppLaunchData.createLaunchDataForRole(RoleManager.ROLE_BROWSER), ), TestData( "META + P -> Launch Default Contacts", @@ -785,7 +779,7 @@ class KeyGestureControllerTests { intArrayOf(KeyEvent.KEYCODE_P), KeyEvent.META_META_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), - AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CONTACTS) + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CONTACTS), ), TestData( "META + E -> Launch Default Email", @@ -794,7 +788,7 @@ class KeyGestureControllerTests { intArrayOf(KeyEvent.KEYCODE_E), KeyEvent.META_META_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), - AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_EMAIL) + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_EMAIL), ), TestData( "META + C -> Launch Default Calendar", @@ -803,7 +797,7 @@ class KeyGestureControllerTests { intArrayOf(KeyEvent.KEYCODE_C), KeyEvent.META_META_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), - AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CALENDAR) + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CALENDAR), ), TestData( "META + M -> Launch Default Maps", @@ -812,7 +806,7 @@ class KeyGestureControllerTests { intArrayOf(KeyEvent.KEYCODE_M), KeyEvent.META_META_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), - AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_MAPS) + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_MAPS), ), TestData( "META + U -> Launch Default Calculator", @@ -821,47 +815,47 @@ class KeyGestureControllerTests { intArrayOf(KeyEvent.KEYCODE_U), KeyEvent.META_META_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), - AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CALCULATOR) + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CALCULATOR), ), TestData( "META + SHIFT + B -> Launch Default Browser", intArrayOf( KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_SHIFT_LEFT, - KeyEvent.KEYCODE_B + KeyEvent.KEYCODE_B, ), KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, intArrayOf(KeyEvent.KEYCODE_B), KeyEvent.META_META_ON or KeyEvent.META_SHIFT_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), - AppLaunchData.createLaunchDataForRole(RoleManager.ROLE_BROWSER) + AppLaunchData.createLaunchDataForRole(RoleManager.ROLE_BROWSER), ), TestData( "META + SHIFT + P -> Launch Default Contacts", intArrayOf( KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_SHIFT_LEFT, - KeyEvent.KEYCODE_P + KeyEvent.KEYCODE_P, ), KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, intArrayOf(KeyEvent.KEYCODE_P), KeyEvent.META_META_ON or KeyEvent.META_SHIFT_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), - AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CONTACTS) + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CONTACTS), ), TestData( "META + SHIFT + J -> Launch Target Activity", intArrayOf( KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_SHIFT_LEFT, - KeyEvent.KEYCODE_J + KeyEvent.KEYCODE_J, ), KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, intArrayOf(KeyEvent.KEYCODE_J), KeyEvent.META_META_ON or KeyEvent.META_SHIFT_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), - AppLaunchData.createLaunchDataForComponent("com.test", "com.test.BookmarkTest") - ) + AppLaunchData.createLaunchDataForComponent("com.test", "com.test.BookmarkTest"), + ), ) } @@ -890,7 +884,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS, intArrayOf(KeyEvent.KEYCODE_RECENT_APPS), 0, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "APP_SWITCH -> App Switch", @@ -900,8 +894,8 @@ class KeyGestureControllerTests { 0, intArrayOf( KeyGestureEvent.ACTION_GESTURE_START, - KeyGestureEvent.ACTION_GESTURE_COMPLETE - ) + KeyGestureEvent.ACTION_GESTURE_COMPLETE, + ), ), TestData( "BRIGHTNESS_UP -> Brightness Up", @@ -909,7 +903,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP, intArrayOf(KeyEvent.KEYCODE_BRIGHTNESS_UP), 0, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "BRIGHTNESS_DOWN -> Brightness Down", @@ -917,7 +911,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN, intArrayOf(KeyEvent.KEYCODE_BRIGHTNESS_DOWN), 0, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "KEYBOARD_BACKLIGHT_UP -> Keyboard Backlight Up", @@ -925,7 +919,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP, intArrayOf(KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP), 0, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "KEYBOARD_BACKLIGHT_DOWN -> Keyboard Backlight Down", @@ -933,7 +927,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN, intArrayOf(KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN), 0, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "KEYBOARD_BACKLIGHT_TOGGLE -> Keyboard Backlight Toggle", @@ -941,7 +935,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE, intArrayOf(KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE), 0, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "ALL_APPS -> Open App Drawer", @@ -949,7 +943,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS, intArrayOf(KeyEvent.KEYCODE_ALL_APPS), 0, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "NOTIFICATION -> Toggle Notification Panel", @@ -957,7 +951,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL, intArrayOf(KeyEvent.KEYCODE_NOTIFICATION), 0, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "LANGUAGE_SWITCH -> Switch Language Forward", @@ -965,7 +959,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH, intArrayOf(KeyEvent.KEYCODE_LANGUAGE_SWITCH), 0, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "SHIFT + LANGUAGE_SWITCH -> Switch Language Backward", @@ -973,7 +967,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH, intArrayOf(KeyEvent.KEYCODE_LANGUAGE_SWITCH), KeyEvent.META_SHIFT_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "SCREENSHOT -> Take Screenshot", @@ -981,15 +975,15 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT, intArrayOf(KeyEvent.KEYCODE_SCREENSHOT), 0, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META -> Open Apps Drawer", intArrayOf(KeyEvent.KEYCODE_META_LEFT), - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS, + KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS, intArrayOf(KeyEvent.KEYCODE_META_LEFT), 0, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "SYSRQ -> Take screenshot", @@ -997,7 +991,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT, intArrayOf(KeyEvent.KEYCODE_SYSRQ), 0, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "ESC -> Close All Dialogs", @@ -1005,7 +999,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS, intArrayOf(KeyEvent.KEYCODE_ESCAPE), 0, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "EXPLORER -> Launch Default Browser", @@ -1014,7 +1008,7 @@ class KeyGestureControllerTests { intArrayOf(KeyEvent.KEYCODE_EXPLORER), 0, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), - AppLaunchData.createLaunchDataForRole(RoleManager.ROLE_BROWSER) + AppLaunchData.createLaunchDataForRole(RoleManager.ROLE_BROWSER), ), TestData( "ENVELOPE -> Launch Default Email", @@ -1023,7 +1017,7 @@ class KeyGestureControllerTests { intArrayOf(KeyEvent.KEYCODE_ENVELOPE), 0, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), - AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_EMAIL) + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_EMAIL), ), TestData( "CONTACTS -> Launch Default Contacts", @@ -1032,7 +1026,7 @@ class KeyGestureControllerTests { intArrayOf(KeyEvent.KEYCODE_CONTACTS), 0, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), - AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CONTACTS) + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CONTACTS), ), TestData( "CALENDAR -> Launch Default Calendar", @@ -1041,7 +1035,7 @@ class KeyGestureControllerTests { intArrayOf(KeyEvent.KEYCODE_CALENDAR), 0, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), - AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CALENDAR) + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CALENDAR), ), TestData( "MUSIC -> Launch Default Music", @@ -1050,7 +1044,7 @@ class KeyGestureControllerTests { intArrayOf(KeyEvent.KEYCODE_MUSIC), 0, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), - AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_MUSIC) + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_MUSIC), ), TestData( "CALCULATOR -> Launch Default Calculator", @@ -1059,7 +1053,7 @@ class KeyGestureControllerTests { intArrayOf(KeyEvent.KEYCODE_CALCULATOR), 0, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), - AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CALCULATOR) + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CALCULATOR), ), TestData( "LOCK -> Lock Screen", @@ -1067,7 +1061,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN, intArrayOf(KeyEvent.KEYCODE_LOCK), 0, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "FULLSCREEN -> Turns a task into fullscreen", @@ -1075,7 +1069,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION, intArrayOf(KeyEvent.KEYCODE_FULLSCREEN), 0, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), ) } @@ -1091,31 +1085,32 @@ class KeyGestureControllerTests { @Test fun testKeycodesFullyConsumed_irrespectiveOfHandlers() { setupKeyGestureController() - val testKeys = intArrayOf( - KeyEvent.KEYCODE_RECENT_APPS, - KeyEvent.KEYCODE_APP_SWITCH, - KeyEvent.KEYCODE_BRIGHTNESS_UP, - KeyEvent.KEYCODE_BRIGHTNESS_DOWN, - KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN, - KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP, - KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE, - KeyEvent.KEYCODE_ALL_APPS, - KeyEvent.KEYCODE_NOTIFICATION, - KeyEvent.KEYCODE_SETTINGS, - KeyEvent.KEYCODE_LANGUAGE_SWITCH, - KeyEvent.KEYCODE_SCREENSHOT, - KeyEvent.KEYCODE_META_LEFT, - KeyEvent.KEYCODE_META_RIGHT, - KeyEvent.KEYCODE_ASSIST, - KeyEvent.KEYCODE_VOICE_ASSIST, - KeyEvent.KEYCODE_STYLUS_BUTTON_PRIMARY, - KeyEvent.KEYCODE_STYLUS_BUTTON_SECONDARY, - KeyEvent.KEYCODE_STYLUS_BUTTON_TERTIARY, - KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL, - KeyEvent.KEYCODE_DO_NOT_DISTURB, - KeyEvent.KEYCODE_LOCK, - KeyEvent.KEYCODE_FULLSCREEN - ) + val testKeys = + intArrayOf( + KeyEvent.KEYCODE_RECENT_APPS, + KeyEvent.KEYCODE_APP_SWITCH, + KeyEvent.KEYCODE_BRIGHTNESS_UP, + KeyEvent.KEYCODE_BRIGHTNESS_DOWN, + KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN, + KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP, + KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE, + KeyEvent.KEYCODE_ALL_APPS, + KeyEvent.KEYCODE_NOTIFICATION, + KeyEvent.KEYCODE_SETTINGS, + KeyEvent.KEYCODE_LANGUAGE_SWITCH, + KeyEvent.KEYCODE_SCREENSHOT, + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_META_RIGHT, + KeyEvent.KEYCODE_ASSIST, + KeyEvent.KEYCODE_VOICE_ASSIST, + KeyEvent.KEYCODE_STYLUS_BUTTON_PRIMARY, + KeyEvent.KEYCODE_STYLUS_BUTTON_SECONDARY, + KeyEvent.KEYCODE_STYLUS_BUTTON_TERTIARY, + KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL, + KeyEvent.KEYCODE_DO_NOT_DISTURB, + KeyEvent.KEYCODE_LOCK, + KeyEvent.KEYCODE_FULLSCREEN, + ) for (key in testKeys) { sendKeys(intArrayOf(key), assertNotSentToApps = true) @@ -1130,7 +1125,7 @@ class KeyGestureControllerTests { testKeyGestureNotProduced( "SEARCH -> Default Search", intArrayOf(KeyEvent.KEYCODE_SEARCH), - intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH) + intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH), ) } @@ -1146,7 +1141,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH, intArrayOf(KeyEvent.KEYCODE_SEARCH), 0, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ) ) } @@ -1161,8 +1156,8 @@ class KeyGestureControllerTests { intArrayOf(KeyEvent.KEYCODE_SETTINGS), intArrayOf( KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH, - KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL - ) + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL, + ), ) } @@ -1178,7 +1173,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS, intArrayOf(KeyEvent.KEYCODE_SETTINGS), 0, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ) ) } @@ -1195,7 +1190,7 @@ class KeyGestureControllerTests { KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL, intArrayOf(KeyEvent.KEYCODE_SETTINGS), 0, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ) ) } @@ -1208,15 +1203,11 @@ class KeyGestureControllerTests { keyGestureController.registerKeyGestureEventListener(listener, 0) sendKeys(intArrayOf(KeyEvent.KEYCODE_CAPS_LOCK)) testLooper.dispatchAll() - assertEquals( - "Listener should get callbacks on key gesture event completed", - 1, - events.size - ) + assertEquals("Listener should get callbacks on key gesture event completed", 1, events.size) assertEquals( "Listener should get callback for Toggle Caps Lock key gesture complete event", KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, - events[0].keyGestureType + events[0].keyGestureType, ) } @@ -1231,8 +1222,8 @@ class KeyGestureControllerTests { 0, intArrayOf( KeyGestureEvent.ACTION_GESTURE_START, - KeyGestureEvent.ACTION_GESTURE_COMPLETE - ) + KeyGestureEvent.ACTION_GESTURE_COMPLETE, + ), ), TestData( "POWER + STEM_PRIMARY -> Screenshot Chord", @@ -1242,8 +1233,8 @@ class KeyGestureControllerTests { 0, intArrayOf( KeyGestureEvent.ACTION_GESTURE_START, - KeyGestureEvent.ACTION_GESTURE_COMPLETE - ) + KeyGestureEvent.ACTION_GESTURE_COMPLETE, + ), ), TestData( "BACK + DPAD_CENTER -> TV Trigger Bug Report", @@ -1253,8 +1244,8 @@ class KeyGestureControllerTests { 0, intArrayOf( KeyGestureEvent.ACTION_GESTURE_START, - KeyGestureEvent.ACTION_GESTURE_COMPLETE - ) + KeyGestureEvent.ACTION_GESTURE_COMPLETE, + ), ), ) } @@ -1263,7 +1254,7 @@ class KeyGestureControllerTests { @Parameters(method = "systemGesturesTestArguments_forKeyCombinations") @EnableFlags( com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER, - com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER_MULTI_KEY_GESTURES + com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER_MULTI_KEY_GESTURES, ) fun testKeyCombinationGestures(test: TestData) { setupKeyGestureController() @@ -1278,29 +1269,25 @@ class KeyGestureControllerTests { intArrayOf( KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_ALT_LEFT, - KeyEvent.KEYCODE_Q + KeyEvent.KEYCODE_Q, ), KeyGestureEvent.KEY_GESTURE_TYPE_HOME, intArrayOf(KeyEvent.KEYCODE_Q), KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, - intArrayOf( - KeyGestureEvent.ACTION_GESTURE_COMPLETE - ) + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), ), TestData( "META + ALT + Q -> Launch app", intArrayOf( KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_SHIFT_LEFT, - KeyEvent.KEYCODE_Q + KeyEvent.KEYCODE_Q, ), KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, intArrayOf(KeyEvent.KEYCODE_Q), KeyEvent.META_CTRL_ON or KeyEvent.META_SHIFT_ON, - intArrayOf( - KeyGestureEvent.ACTION_GESTURE_COMPLETE - ), - AppLaunchData.createLaunchDataForComponent("com.test", "com.test.BookmarkTest") + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), + AppLaunchData.createLaunchDataForComponent("com.test", "com.test.BookmarkTest"), ), ) } @@ -1309,31 +1296,27 @@ class KeyGestureControllerTests { @Parameters(method = "customInputGesturesTestArguments") fun testCustomKeyGestures(test: TestData) { setupKeyGestureController() - val trigger = InputGestureData.createKeyTrigger( - test.expectedKeys[0], - test.expectedModifierState - ) - val builder = InputGestureData.Builder() - .setKeyGestureType(test.expectedKeyGestureType) - .setTrigger(trigger) + val trigger = + InputGestureData.createKeyTrigger(test.expectedKeys[0], test.expectedModifierState) + val builder = + InputGestureData.Builder() + .setKeyGestureType(test.expectedKeyGestureType) + .setTrigger(trigger) if (test.expectedAppLaunchData != null) { builder.setAppLaunchData(test.expectedAppLaunchData) } val inputGestureData = builder.build() - assertNull( - test.toString(), - keyGestureController.getInputGesture(0, trigger.aidlTrigger) - ) + assertNull(test.toString(), keyGestureController.getInputGesture(0, trigger.aidlTrigger)) assertEquals( test.toString(), InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS, - keyGestureController.addCustomInputGesture(0, builder.build().aidlData) + keyGestureController.addCustomInputGesture(0, builder.build().aidlData), ) assertEquals( test.toString(), inputGestureData.aidlData, - keyGestureController.getInputGesture(0, trigger.aidlTrigger) + keyGestureController.getInputGesture(0, trigger.aidlTrigger), ) testKeyGestureInternal(test) } @@ -1343,14 +1326,15 @@ class KeyGestureControllerTests { fun testCustomKeyGesturesSavedAndLoadedByController(test: TestData) { val userId = 10 setupKeyGestureController() - val builder = InputGestureData.Builder() - .setKeyGestureType(test.expectedKeyGestureType) - .setTrigger( - InputGestureData.createKeyTrigger( - test.expectedKeys[0], - test.expectedModifierState + val builder = + InputGestureData.Builder() + .setKeyGestureType(test.expectedKeyGestureType) + .setTrigger( + InputGestureData.createKeyTrigger( + test.expectedKeys[0], + test.expectedModifierState, + ) ) - ) if (test.expectedAppLaunchData != null) { builder.setAppLaunchData(test.expectedAppLaunchData) } @@ -1371,11 +1355,12 @@ class KeyGestureControllerTests { assertEquals( "Test: $test doesn't produce correct number of saved input gestures", 1, - savedInputGestures.size + savedInputGestures.size, ) assertEquals( - "Test: $test doesn't produce correct input gesture data", inputGestureData, - InputGestureData(savedInputGestures[0]) + "Test: $test doesn't produce correct input gesture data", + inputGestureData, + InputGestureData(savedInputGestures[0]), ) } @@ -1384,14 +1369,15 @@ class KeyGestureControllerTests { fun testCustomKeyGestureRestoredFromBackup(test: TestData) { val userId = 10 setupKeyGestureController() - val builder = InputGestureData.Builder() - .setKeyGestureType(test.expectedKeyGestureType) - .setTrigger( - InputGestureData.createKeyTrigger( - test.expectedKeys[0], - test.expectedModifierState + val builder = + InputGestureData.Builder() + .setKeyGestureType(test.expectedKeyGestureType) + .setTrigger( + InputGestureData.createKeyTrigger( + test.expectedKeys[0], + test.expectedModifierState, + ) ) - ) if (test.expectedAppLaunchData != null) { builder.setAppLaunchData(test.expectedAppLaunchData) } @@ -1415,7 +1401,7 @@ class KeyGestureControllerTests { assertEquals( "Test: $test doesn't produce correct number of saved input gestures", 0, - savedInputGestures.size + savedInputGestures.size, ) // After the restore, there should be the original gesture re-registered. @@ -1424,11 +1410,12 @@ class KeyGestureControllerTests { assertEquals( "Test: $test doesn't produce correct number of saved input gestures", 1, - savedInputGestures.size + savedInputGestures.size, ) assertEquals( - "Test: $test doesn't produce correct input gesture data", inputGestureData, - InputGestureData(savedInputGestures[0]) + "Test: $test doesn't produce correct input gesture data", + inputGestureData, + InputGestureData(savedInputGestures[0]), ) } @@ -1449,14 +1436,14 @@ class KeyGestureControllerTests { "3 Finger Tap -> Go Home", InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP, KeyGestureEvent.KEY_GESTURE_TYPE_HOME, - KeyGestureEvent.ACTION_GESTURE_COMPLETE + KeyGestureEvent.ACTION_GESTURE_COMPLETE, ), TouchpadTestData( "3 Finger Tap -> Launch app", InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP, KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, KeyGestureEvent.ACTION_GESTURE_COMPLETE, - AppLaunchData.createLaunchDataForComponent("com.test", "com.test.BookmarkTest") + AppLaunchData.createLaunchDataForComponent("com.test", "com.test.BookmarkTest"), ), ) } @@ -1465,9 +1452,10 @@ class KeyGestureControllerTests { @Parameters(method = "customTouchpadGesturesTestArguments") fun testCustomTouchpadGesture(test: TouchpadTestData) { setupKeyGestureController() - val builder = InputGestureData.Builder() - .setKeyGestureType(test.expectedKeyGestureType) - .setTrigger(InputGestureData.createTouchpadTrigger(test.touchpadGestureType)) + val builder = + InputGestureData.Builder() + .setKeyGestureType(test.expectedKeyGestureType) + .setTrigger(InputGestureData.createTouchpadTrigger(test.touchpadGestureType)) if (test.expectedAppLaunchData != null) { builder.setAppLaunchData(test.expectedAppLaunchData) } @@ -1476,13 +1464,11 @@ class KeyGestureControllerTests { keyGestureController.addCustomInputGesture(0, inputGestureData.aidlData) val handledEvents = mutableListOf<KeyGestureEvent>() - val handler = KeyGestureHandler { event, _ -> - handledEvents.add(KeyGestureEvent(event)) - } + val handler = KeyGestureHandler { event, _ -> handledEvents.add(KeyGestureEvent(event)) } keyGestureController.registerKeyGestureHandler( intArrayOf(test.expectedKeyGestureType), handler, - TEST_PID + TEST_PID, ) handledEvents.clear() @@ -1491,23 +1477,23 @@ class KeyGestureControllerTests { assertEquals( "Test: $test doesn't produce correct number of key gesture events", 1, - handledEvents.size + handledEvents.size, ) val event = handledEvents[0] assertEquals( "Test: $test doesn't produce correct key gesture type", test.expectedKeyGestureType, - event.keyGestureType + event.keyGestureType, ) assertEquals( "Test: $test doesn't produce correct key gesture action", test.expectedAction, - event.action + event.action, ) assertEquals( "Test: $test doesn't produce correct app launch data", test.expectedAppLaunchData, - event.appLaunchData + event.appLaunchData, ) keyGestureController.unregisterKeyGestureHandler(handler, TEST_PID) @@ -1518,9 +1504,10 @@ class KeyGestureControllerTests { fun testCustomTouchpadGesturesSavedAndLoadedByController(test: TouchpadTestData) { val userId = 10 setupKeyGestureController() - val builder = InputGestureData.Builder() - .setKeyGestureType(test.expectedKeyGestureType) - .setTrigger(InputGestureData.createTouchpadTrigger(test.touchpadGestureType)) + val builder = + InputGestureData.Builder() + .setKeyGestureType(test.expectedKeyGestureType) + .setTrigger(InputGestureData.createTouchpadTrigger(test.touchpadGestureType)) if (test.expectedAppLaunchData != null) { builder.setAppLaunchData(test.expectedAppLaunchData) } @@ -1540,23 +1527,24 @@ class KeyGestureControllerTests { assertEquals( "Test: $test doesn't produce correct number of saved input gestures", 1, - savedInputGestures.size + savedInputGestures.size, ) assertEquals( - "Test: $test doesn't produce correct input gesture data", inputGestureData, - InputGestureData(savedInputGestures[0]) + "Test: $test doesn't produce correct input gesture data", + inputGestureData, + InputGestureData(savedInputGestures[0]), ) } - @Test @Parameters(method = "customTouchpadGesturesTestArguments") fun testCustomTouchpadGesturesRestoredFromBackup(test: TouchpadTestData) { val userId = 10 setupKeyGestureController() - val builder = InputGestureData.Builder() - .setKeyGestureType(test.expectedKeyGestureType) - .setTrigger(InputGestureData.createTouchpadTrigger(test.touchpadGestureType)) + val builder = + InputGestureData.Builder() + .setKeyGestureType(test.expectedKeyGestureType) + .setTrigger(InputGestureData.createTouchpadTrigger(test.touchpadGestureType)) if (test.expectedAppLaunchData != null) { builder.setAppLaunchData(test.expectedAppLaunchData) } @@ -1579,7 +1567,7 @@ class KeyGestureControllerTests { assertEquals( "Test: $test doesn't produce correct number of saved input gestures", 0, - savedInputGestures.size + savedInputGestures.size, ) // After the restore, there should be the original gesture re-registered. @@ -1588,11 +1576,12 @@ class KeyGestureControllerTests { assertEquals( "Test: $test doesn't produce correct number of saved input gestures", 1, - savedInputGestures.size + savedInputGestures.size, ) assertEquals( - "Test: $test doesn't produce correct input gesture data", inputGestureData, - InputGestureData(savedInputGestures[0]) + "Test: $test doesn't produce correct input gesture data", + inputGestureData, + InputGestureData(savedInputGestures[0]), ) } @@ -1604,7 +1593,7 @@ class KeyGestureControllerTests { intArrayOf(KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN), // Assuming this value is always greater than the accessibility shortcut timeout, which // currently defaults to 3000ms - timeDelayMs = 10000 + timeDelayMs = 10000, ) Mockito.verify(accessibilityShortcutController, times(1)).performAccessibilityShortcut() } @@ -1613,10 +1602,7 @@ class KeyGestureControllerTests { fun testAccessibilityTvShortcutChordPressed() { setupKeyGestureController() - sendKeys( - intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN), - timeDelayMs = 10000 - ) + sendKeys(intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN), timeDelayMs = 10000) Mockito.verify(accessibilityShortcutController, times(1)).performAccessibilityShortcut() } @@ -1626,7 +1612,7 @@ class KeyGestureControllerTests { sendKeys( intArrayOf(KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN), - timeDelayMs = 0 + timeDelayMs = 0, ) Mockito.verify(accessibilityShortcutController, never()).performAccessibilityShortcut() } @@ -1635,10 +1621,7 @@ class KeyGestureControllerTests { fun testAccessibilityTvShortcutChordPressedForLessThanTimeout() { setupKeyGestureController() - sendKeys( - intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN), - timeDelayMs = 0 - ) + sendKeys(intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN), timeDelayMs = 0) Mockito.verify(accessibilityShortcutController, never()).performAccessibilityShortcut() } @@ -1651,14 +1634,14 @@ class KeyGestureControllerTests { keyGestureController.registerKeyGestureHandler( intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME), handler1, - RANDOM_PID1 + RANDOM_PID1, ) assertThrows(IllegalStateException::class.java) { keyGestureController.registerKeyGestureHandler( intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_BACK), handler2, - RANDOM_PID1 + RANDOM_PID1, ) } } @@ -1672,14 +1655,14 @@ class KeyGestureControllerTests { keyGestureController.registerKeyGestureHandler( intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME), handler1, - RANDOM_PID1 + RANDOM_PID1, ) assertThrows(IllegalArgumentException::class.java) { keyGestureController.registerKeyGestureHandler( intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME), handler2, - RANDOM_PID2 + RANDOM_PID2, ) } } @@ -1691,11 +1674,7 @@ class KeyGestureControllerTests { val handler = KeyGestureHandler { _, _ -> } assertThrows(IllegalArgumentException::class.java) { - keyGestureController.registerKeyGestureHandler( - intArrayOf(), - handler, - RANDOM_PID1 - ) + keyGestureController.registerKeyGestureHandler(intArrayOf(), handler, RANDOM_PID1) } } @@ -1708,15 +1687,12 @@ class KeyGestureControllerTests { keyGestureController.registerKeyGestureHandler( intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS), handler1, - TEST_PID + TEST_PID, ) sendKeys(intArrayOf(KeyEvent.KEYCODE_RECENT_APPS)) assertEquals(1, callbackCount) - keyGestureController.unregisterKeyGestureHandler( - handler1, - TEST_PID - ) + keyGestureController.unregisterKeyGestureHandler(handler1, TEST_PID) // Callback should not be sent after unregister sendKeys(intArrayOf(KeyEvent.KEYCODE_RECENT_APPS)) @@ -1725,13 +1701,11 @@ class KeyGestureControllerTests { private fun testKeyGestureInternal(test: TestData) { val handledEvents = mutableListOf<KeyGestureEvent>() - val handler = KeyGestureHandler { event, _ -> - handledEvents.add(KeyGestureEvent(event)) - } + val handler = KeyGestureHandler { event, _ -> handledEvents.add(KeyGestureEvent(event)) } keyGestureController.registerKeyGestureHandler( intArrayOf(test.expectedKeyGestureType), handler, - TEST_PID + TEST_PID, ) handledEvents.clear() @@ -1740,34 +1714,34 @@ class KeyGestureControllerTests { assertEquals( "Test: $test doesn't produce correct number of key gesture events", test.expectedActions.size, - handledEvents.size + handledEvents.size, ) for (i in handledEvents.indices) { val event = handledEvents[i] assertArrayEquals( "Test: $test doesn't produce correct key gesture keycodes", test.expectedKeys, - event.keycodes + event.keycodes, ) assertEquals( "Test: $test doesn't produce correct key gesture modifier state", test.expectedModifierState, - event.modifierState + event.modifierState, ) assertEquals( "Test: $test doesn't produce correct key gesture type", test.expectedKeyGestureType, - event.keyGestureType + event.keyGestureType, ) assertEquals( "Test: $test doesn't produce correct key gesture action", test.expectedActions[i], - event.action + event.action, ) assertEquals( "Test: $test doesn't produce correct app launch data", test.expectedAppLaunchData, - event.appLaunchData + event.appLaunchData, ) } @@ -1777,12 +1751,10 @@ class KeyGestureControllerTests { private fun testKeyGestureNotProduced( testName: String, testKeys: IntArray, - possibleGestures: IntArray + possibleGestures: IntArray, ) { var handledEvents = mutableListOf<KeyGestureEvent>() - val handler = KeyGestureHandler { event, _ -> - handledEvents.add(KeyGestureEvent(event)) - } + val handler = KeyGestureHandler { event, _ -> handledEvents.add(KeyGestureEvent(event)) } keyGestureController.registerKeyGestureHandler(possibleGestures, handler, TEST_PID) handledEvents.clear() @@ -1793,16 +1765,24 @@ class KeyGestureControllerTests { private fun sendKeys( testKeys: IntArray, assertNotSentToApps: Boolean = false, - timeDelayMs: Long = 0 + timeDelayMs: Long = 0, ) { var metaState = 0 val now = SystemClock.uptimeMillis() for (key in testKeys) { - val downEvent = KeyEvent( - now, now, KeyEvent.ACTION_DOWN, key, 0 /*repeat*/, metaState, - DEVICE_ID, 0 /*scancode*/, 0 /*flags*/, - InputDevice.SOURCE_KEYBOARD - ) + val downEvent = + KeyEvent( + now, + now, + KeyEvent.ACTION_DOWN, + key, + 0 /*repeat*/, + metaState, + DEVICE_ID, + 0 /*scancode*/, + 0 /*flags*/, + InputDevice.SOURCE_KEYBOARD, + ) interceptKey(downEvent, assertNotSentToApps) metaState = metaState or MODIFIER.getOrDefault(key, 0) @@ -1816,11 +1796,19 @@ class KeyGestureControllerTests { } for (key in testKeys.reversed()) { - val upEvent = KeyEvent( - now, now, KeyEvent.ACTION_UP, key, 0 /*repeat*/, metaState, - DEVICE_ID, 0 /*scancode*/, 0 /*flags*/, - InputDevice.SOURCE_KEYBOARD - ) + val upEvent = + KeyEvent( + now, + now, + KeyEvent.ACTION_UP, + key, + 0 /*repeat*/, + metaState, + DEVICE_ID, + 0 /*scancode*/, + 0 /*flags*/, + InputDevice.SOURCE_KEYBOARD, + ) interceptKey(upEvent, assertNotSentToApps) metaState = metaState and MODIFIER.getOrDefault(key, 0).inv() @@ -1833,13 +1821,9 @@ class KeyGestureControllerTests { keyGestureController.interceptKeyBeforeQueueing(event, FLAG_INTERACTIVE) testLooper.dispatchAll() - val consumed = - keyGestureController.interceptKeyBeforeDispatching(null, event, 0) == -1L + val consumed = keyGestureController.interceptKeyBeforeDispatching(null, event, 0) == -1L if (assertNotSentToApps) { - assertTrue( - "interceptKeyBeforeDispatching should consume all events $event", - consumed - ) + assertTrue("interceptKeyBeforeDispatching should consume all events $event", consumed) } if (!consumed) { keyGestureController.interceptUnhandledKey(event, null) diff --git a/tests/Input/src/com/android/server/input/KeyRemapperTests.kt b/tests/Input/src/com/android/server/input/KeyRemapperTests.kt index 4f4c97bef4c0..1fa985647513 100644 --- a/tests/Input/src/com/android/server/input/KeyRemapperTests.kt +++ b/tests/Input/src/com/android/server/input/KeyRemapperTests.kt @@ -51,31 +51,32 @@ private fun createKeyboard(deviceId: Int): InputDevice = /** * Tests for {@link KeyRemapper}. * - * Build/Install/Run: - * atest InputTests:KeyRemapperTests + * Build/Install/Run: atest InputTests:KeyRemapperTests */ @Presubmit class KeyRemapperTests { companion object { const val DEVICE_ID = 1 - val REMAPPABLE_KEYS = intArrayOf( - KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_CTRL_RIGHT, - KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_META_RIGHT, - KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.KEYCODE_ALT_RIGHT, - KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SHIFT_RIGHT, - KeyEvent.KEYCODE_CAPS_LOCK - ) + val REMAPPABLE_KEYS = + intArrayOf( + KeyEvent.KEYCODE_CTRL_LEFT, + KeyEvent.KEYCODE_CTRL_RIGHT, + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_META_RIGHT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_ALT_RIGHT, + KeyEvent.KEYCODE_SHIFT_LEFT, + KeyEvent.KEYCODE_SHIFT_RIGHT, + KeyEvent.KEYCODE_CAPS_LOCK, + ) } - @get:Rule - val rule = MockitoJUnit.rule()!! + @get:Rule val rule = MockitoJUnit.rule()!! - @get:Rule - val inputManagerRule = MockInputManagerRule() + @get:Rule val inputManagerRule = MockInputManagerRule() - @Mock - private lateinit var native: NativeInputManagerService + @Mock private lateinit var native: NativeInputManagerService private lateinit var mKeyRemapper: KeyRemapper private lateinit var context: Context private lateinit var dataStore: PersistentDataStore @@ -84,24 +85,22 @@ class KeyRemapperTests { @Before fun setup() { context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) - dataStore = PersistentDataStore(object : PersistentDataStore.Injector() { - override fun openRead(): InputStream? { - throw FileNotFoundException() - } - - override fun startWrite(): FileOutputStream? { - throw IOException() - } - - override fun finishWrite(fos: FileOutputStream?, success: Boolean) {} - }) + dataStore = + PersistentDataStore( + object : PersistentDataStore.Injector() { + override fun openRead(): InputStream? { + throw FileNotFoundException() + } + + override fun startWrite(): FileOutputStream? { + throw IOException() + } + + override fun finishWrite(fos: FileOutputStream?, success: Boolean) {} + } + ) testLooper = TestLooper() - mKeyRemapper = KeyRemapper( - context, - native, - dataStore, - testLooper.looper - ) + mKeyRemapper = KeyRemapper(context, native, dataStore, testLooper.looper) val inputManager = InputManager(context) Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) .thenReturn(inputManager) @@ -131,7 +130,7 @@ class KeyRemapperTests { assertEquals( "Remapping should include mapping from $fromKeyCode to $toKeyCode", toKeyCode, - remapping.getOrDefault(fromKeyCode, -1) + remapping.getOrDefault(fromKeyCode, -1), ) } @@ -141,7 +140,7 @@ class KeyRemapperTests { assertEquals( "Remapping size should be 0 after clearAllModifierKeyRemappings", 0, - mKeyRemapper.keyRemapping.size + mKeyRemapper.keyRemapping.size, ) } } @@ -159,7 +158,7 @@ class KeyRemapperTests { assertEquals( "Remapping should not be done if modifier key remapping is disabled", 0, - remapping.size + remapping.size, ) } } @@ -168,7 +167,8 @@ class KeyRemapperTests { init { Settings.Global.putString( context.contentResolver, - "settings_new_keyboard_modifier_key", enabled.toString() + "settings_new_keyboard_modifier_key", + enabled.toString(), ) } @@ -176,8 +176,8 @@ class KeyRemapperTests { Settings.Global.putString( context.contentResolver, "settings_new_keyboard_modifier_key", - "" + "", ) } } -}
\ No newline at end of file +} diff --git a/tests/Input/src/com/android/server/input/KeyboardBacklightControllerTests.kt b/tests/Input/src/com/android/server/input/KeyboardBacklightControllerTests.kt index 644d5a0679de..a0cf88809af4 100644 --- a/tests/Input/src/com/android/server/input/KeyboardBacklightControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyboardBacklightControllerTests.kt @@ -26,11 +26,10 @@ import android.hardware.input.IKeyboardBacklightState import android.hardware.input.InputManager import android.hardware.lights.Light import android.os.SystemProperties -import android.os.UEventObserver import android.os.test.TestLooper import android.platform.test.annotations.Presubmit -import android.view.InputDevice import android.util.TypedValue +import android.view.InputDevice import androidx.test.annotation.UiThreadTest import androidx.test.core.app.ApplicationProvider import com.android.dx.mockito.inline.extended.ExtendedMockito @@ -65,12 +64,7 @@ private fun createKeyboard(deviceId: Int): InputDevice = .setExternal(true) .build() -private fun createLight(lightId: Int, lightType: Int): Light = - createLight( - lightId, - lightType, - null - ) +private fun createLight(lightId: Int, lightType: Int): Light = createLight(lightId, lightType, null) private fun createLight(lightId: Int, lightType: Int, suggestedBrightnessLevels: IntArray?): Light = Light( @@ -79,13 +73,13 @@ private fun createLight(lightId: Int, lightType: Int, suggestedBrightnessLevels: 1, lightType, Light.LIGHT_CAPABILITY_BRIGHTNESS, - suggestedBrightnessLevels + suggestedBrightnessLevels, ) + /** * Tests for {@link KeyboardBacklightController}. * - * Build/Install/Run: - * atest InputTests:KeyboardBacklightControllerTests + * Build/Install/Run: atest InputTests:KeyboardBacklightControllerTests */ @Presubmit class KeyboardBacklightControllerTests { @@ -100,21 +94,15 @@ class KeyboardBacklightControllerTests { @get:Rule val extendedMockitoRule = ExtendedMockitoRule.Builder(this).mockStatic(SystemProperties::class.java).build()!! - @get:Rule - val inputManagerRule = MockInputManagerRule() - - @Mock - private lateinit var native: NativeInputManagerService - @Mock - private lateinit var uEventManager: UEventManager - @Mock - private lateinit var resources: Resources + @get:Rule val inputManagerRule = MockInputManagerRule() + + @Mock private lateinit var native: NativeInputManagerService + @Mock private lateinit var resources: Resources private lateinit var keyboardBacklightController: KeyboardBacklightController private lateinit var context: Context private lateinit var testLooper: TestLooper private var lightColorMap: HashMap<Int, Int> = HashMap() private var lastBacklightState: KeyboardBacklightState? = null - private var sysfsNodeChanges = 0 private var lastAnimationValues = IntArray(2) @Before @@ -135,9 +123,6 @@ class KeyboardBacklightControllerTests { lightColorMap.getOrDefault(args[1] as Int, 0) } lightColorMap.clear() - `when`(native.sysfsNodeChanged(any())).then { - sysfsNodeChanges++ - } } private fun setupConfig() { @@ -153,22 +138,23 @@ class KeyboardBacklightControllerTests { `when`(resources.getInteger(R.integer.config_keyboardBacklightTimeoutMs)) .thenReturn(USER_INACTIVITY_THRESHOLD_MILLIS) `when`( - resources.getValue( - eq(R.dimen.config_autoKeyboardBrightnessSmoothingConstant), - any(TypedValue::class.java), - anyBoolean() + resources.getValue( + eq(R.dimen.config_autoKeyboardBrightnessSmoothingConstant), + any(TypedValue::class.java), + anyBoolean(), + ) ) - ).then { - val args = it.arguments - val outValue = args[1] as TypedValue - outValue.data = java.lang.Float.floatToRawIntBits(1.0f) - Unit - } + .then { + val args = it.arguments + val outValue = args[1] as TypedValue + outValue.data = java.lang.Float.floatToRawIntBits(1.0f) + Unit + } } private fun setupController() { - keyboardBacklightController = KeyboardBacklightController(context, native, - testLooper.looper, FakeAnimatorFactory(), uEventManager) + keyboardBacklightController = + KeyboardBacklightController(context, native, testLooper.looper, FakeAnimatorFactory()) } @Test @@ -180,8 +166,11 @@ class KeyboardBacklightControllerTests { `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight, - DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL) + assertIncrementDecrementForLevels( + keyboardWithBacklight, + keyboardBacklight, + DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL, + ) } @Test @@ -204,12 +193,8 @@ class KeyboardBacklightControllerTests { val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) val keyboardInputLight = createLight(SECOND_LIGHT_ID, Light.LIGHT_TYPE_INPUT) `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn( - listOf( - keyboardBacklight, - keyboardInputLight - ) - ) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)) + .thenReturn(listOf(keyboardBacklight, keyboardInputLight)) keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) incrementKeyboardBacklight(DEVICE_ID) @@ -239,22 +224,22 @@ class KeyboardBacklightControllerTests { assertEquals( "Backlight state device Id should be $DEVICE_ID", DEVICE_ID, - lastBacklightState!!.deviceId + lastBacklightState!!.deviceId, ) assertEquals( "Backlight state brightnessLevel should be 1", 1, - lastBacklightState!!.brightnessLevel + lastBacklightState!!.brightnessLevel, ) assertEquals( "Backlight state maxBrightnessLevel should be $maxLevel", maxLevel, - lastBacklightState!!.maxBrightnessLevel + lastBacklightState!!.maxBrightnessLevel, ) assertEquals( "Backlight state isTriggeredByKeyPress should be true", true, - lastBacklightState!!.isTriggeredByKeyPress + lastBacklightState!!.isTriggeredByKeyPress, ) // Unregister listener @@ -278,7 +263,7 @@ class KeyboardBacklightControllerTests { assertNotEquals( "Keyboard backlight level should be incremented to a non-zero value", 0, - lightColorMap[LIGHT_ID] + lightColorMap[LIGHT_ID], ) testLooper.moveTimeForward((USER_INACTIVITY_THRESHOLD_MILLIS + 1000).toLong()) @@ -286,7 +271,7 @@ class KeyboardBacklightControllerTests { assertEquals( "Keyboard backlight level should be turned off after inactivity", 0, - lightColorMap[LIGHT_ID] + lightColorMap[LIGHT_ID], ) } @@ -304,80 +289,21 @@ class KeyboardBacklightControllerTests { assertNotEquals( "Keyboard backlight level should be incremented to a non-zero value", 0, - lightColorMap[LIGHT_ID] + lightColorMap[LIGHT_ID], ) keyboardBacklightController.handleInteractiveStateChange(false /* isDisplayOn */) assertEquals( "Keyboard backlight level should be turned off after display is turned off", 0, - lightColorMap[LIGHT_ID] + lightColorMap[LIGHT_ID], ) keyboardBacklightController.handleInteractiveStateChange(true /* isDisplayOn */) assertEquals( "Keyboard backlight level should be turned on after display is turned on", currentValue, - lightColorMap[LIGHT_ID] - ) - } - - @Test - fun testKeyboardBacklightSysfsNodeAdded_AfterInputDeviceAdded() { - setupController() - var counter = sysfsNodeChanges - keyboardBacklightController.onKeyboardBacklightUEvent(UEventObserver.UEvent( - "ACTION=add\u0000SUBSYSTEM=leds\u0000DEVPATH=/xyz/leds/abc::no_backlight\u0000" - )) - assertEquals( - "Should not reload sysfs node if UEvent path doesn't contain kbd_backlight", - counter, - sysfsNodeChanges - ) - - keyboardBacklightController.onKeyboardBacklightUEvent(UEventObserver.UEvent( - "ACTION=add\u0000SUBSYSTEM=power\u0000DEVPATH=/xyz/leds/abc::kbd_backlight\u0000" - )) - assertEquals( - "Should not reload sysfs node if UEvent doesn't belong to subsystem LED", - counter, - sysfsNodeChanges - ) - - keyboardBacklightController.onKeyboardBacklightUEvent(UEventObserver.UEvent( - "ACTION=remove\u0000SUBSYSTEM=leds\u0000DEVPATH=/xyz/leds/abc::kbd_backlight\u0000" - )) - assertEquals( - "Should not reload sysfs node if UEvent doesn't have ACTION(add)", - counter, - sysfsNodeChanges - ) - - keyboardBacklightController.onKeyboardBacklightUEvent(UEventObserver.UEvent( - "ACTION=add\u0000SUBSYSTEM=leds\u0000DEVPATH=/xyz/pqr/abc::kbd_backlight\u0000" - )) - assertEquals( - "Should not reload sysfs node if UEvent path doesn't belong to leds/ directory", - counter, - sysfsNodeChanges - ) - - keyboardBacklightController.onKeyboardBacklightUEvent(UEventObserver.UEvent( - "ACTION=add\u0000SUBSYSTEM=leds\u0000DEVPATH=/xyz/leds/abc::kbd_backlight\u0000" - )) - assertEquals( - "Should reload sysfs node if a valid Keyboard backlight LED UEvent occurs", - ++counter, - sysfsNodeChanges - ) - - keyboardBacklightController.onKeyboardBacklightUEvent(UEventObserver.UEvent( - "ACTION=add\u0000SUBSYSTEM=leds\u0000DEVPATH=/xyz/leds/abc:kbd_backlight:red\u0000" - )) - assertEquals( - "Should reload sysfs node if a valid Keyboard backlight LED UEvent occurs", - ++counter, - sysfsNodeChanges + lightColorMap[LIGHT_ID], ) } @@ -398,12 +324,12 @@ class KeyboardBacklightControllerTests { assertEquals( "Should start animation from level 0", DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[0], - lastAnimationValues[0] + lastAnimationValues[0], ) assertEquals( "Should start animation to level 1", DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1], - lastAnimationValues[1] + lastAnimationValues[1], ) } @@ -412,8 +338,8 @@ class KeyboardBacklightControllerTests { setupController() val keyboardWithBacklight = createKeyboard(DEVICE_ID) val suggestedLevels = intArrayOf(0, 22, 63, 135, 196, 255) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, - suggestedLevels) + val keyboardBacklight = + createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, suggestedLevels) `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) @@ -426,14 +352,17 @@ class KeyboardBacklightControllerTests { setupController() val keyboardWithBacklight = createKeyboard(DEVICE_ID) val suggestedLevels = IntArray(MAX_BRIGHTNESS_CHANGE_STEPS + 1) { 10 * (it + 1) } - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, - suggestedLevels) + val keyboardBacklight = + createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, suggestedLevels) `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight, - DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL) + assertIncrementDecrementForLevels( + keyboardWithBacklight, + keyboardBacklight, + DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL, + ) } @Test @@ -441,15 +370,18 @@ class KeyboardBacklightControllerTests { setupController() val keyboardWithBacklight = createKeyboard(DEVICE_ID) val suggestedLevels = intArrayOf(22, 63, 135, 196) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, - suggestedLevels) + val keyboardBacklight = + createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, suggestedLevels) `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) // Framework will add the lowest and maximum levels if not provided via config - assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight, - intArrayOf(0, 22, 63, 135, 196, 255)) + assertIncrementDecrementForLevels( + keyboardWithBacklight, + keyboardBacklight, + intArrayOf(0, 22, 63, 135, 196, 255), + ) } @Test @@ -457,15 +389,18 @@ class KeyboardBacklightControllerTests { setupController() val keyboardWithBacklight = createKeyboard(DEVICE_ID) val suggestedLevels = intArrayOf(22, 63, 135, 400, 196, 1000) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, - suggestedLevels) + val keyboardBacklight = + createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, suggestedLevels) `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) // Framework will drop out of bound levels in the config - assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight, - intArrayOf(0, 22, 63, 135, 196, 255)) + assertIncrementDecrementForLevels( + keyboardWithBacklight, + keyboardBacklight, + intArrayOf(0, 22, 63, 135, 196, 255), + ) } @Test @@ -480,7 +415,7 @@ class KeyboardBacklightControllerTests { assertEquals( "Light value should be changed to ambient provided value", Color.argb(1, 0, 0, 0), - lightColorMap[LIGHT_ID] + lightColorMap[LIGHT_ID], ) incrementKeyboardBacklight(DEVICE_ID) @@ -488,7 +423,7 @@ class KeyboardBacklightControllerTests { assertEquals( "Light value for level after increment post Ambient change is mismatched", Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1], 0, 0, 0), - lightColorMap[LIGHT_ID] + lightColorMap[LIGHT_ID], ) } @@ -504,7 +439,7 @@ class KeyboardBacklightControllerTests { assertEquals( "Light value should be changed to ambient provided value", Color.argb(254, 0, 0, 0), - lightColorMap[LIGHT_ID] + lightColorMap[LIGHT_ID], ) decrementKeyboardBacklight(DEVICE_ID) @@ -513,7 +448,7 @@ class KeyboardBacklightControllerTests { assertEquals( "Light value for level after decrement post Ambient change is mismatched", Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[numLevels - 2], 0, 0, 0), - lightColorMap[LIGHT_ID] + lightColorMap[LIGHT_ID], ) } @@ -529,21 +464,21 @@ class KeyboardBacklightControllerTests { assertEquals( "Light value should be changed to the first level", Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1], 0, 0, 0), - lightColorMap[LIGHT_ID] + lightColorMap[LIGHT_ID], ) sendAmbientBacklightValue(100) assertNotEquals( "Light value should not change based on ambient changes after manual changes", Color.argb(100, 0, 0, 0), - lightColorMap[LIGHT_ID] + lightColorMap[LIGHT_ID], ) } private fun assertIncrementDecrementForLevels( device: InputDevice, light: Light, - expectedLevels: IntArray + expectedLevels: IntArray, ) { val deviceId = device.id val lightId = light.id @@ -552,7 +487,7 @@ class KeyboardBacklightControllerTests { assertEquals( "Light value for level $level mismatched", Color.argb(expectedLevels[level], 0, 0, 0), - lightColorMap[lightId] + lightColorMap[lightId], ) } @@ -561,7 +496,7 @@ class KeyboardBacklightControllerTests { assertEquals( "Light value for max level mismatched", Color.argb(MAX_BRIGHTNESS, 0, 0, 0), - lightColorMap[lightId] + lightColorMap[lightId], ) for (level in expectedLevels.size - 2 downTo 0) { @@ -569,7 +504,7 @@ class KeyboardBacklightControllerTests { assertEquals( "Light value for level $level mismatched", Color.argb(expectedLevels[level], 0, 0, 0), - lightColorMap[lightId] + lightColorMap[lightId], ) } @@ -578,7 +513,7 @@ class KeyboardBacklightControllerTests { assertEquals( "Light value for min level mismatched", Color.argb(0, 0, 0, 0), - lightColorMap[lightId] + lightColorMap[lightId], ) } @@ -586,14 +521,15 @@ class KeyboardBacklightControllerTests { override fun onBrightnessChanged( deviceId: Int, state: IKeyboardBacklightState, - isTriggeredByKeyPress: Boolean + isTriggeredByKeyPress: Boolean, ) { - lastBacklightState = KeyboardBacklightState( - deviceId, - state.brightnessLevel, - state.maxBrightnessLevel, - isTriggeredByKeyPress - ) + lastBacklightState = + KeyboardBacklightState( + deviceId, + state.brightnessLevel, + state.maxBrightnessLevel, + isTriggeredByKeyPress, + ) } } @@ -619,7 +555,7 @@ class KeyboardBacklightControllerTests { val deviceId: Int, val brightnessLevel: Int, val maxBrightnessLevel: Int, - val isTriggeredByKeyPress: Boolean + val isTriggeredByKeyPress: Boolean, ) private inner class FakeAnimatorFactory : KeyboardBacklightController.AnimatorFactory { diff --git a/tests/Input/src/com/android/server/input/KeyboardGlyphManagerTests.kt b/tests/Input/src/com/android/server/input/KeyboardGlyphManagerTests.kt index 5da0beb9cc8a..7c3a6a60d3b6 100644 --- a/tests/Input/src/com/android/server/input/KeyboardGlyphManagerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyboardGlyphManagerTests.kt @@ -49,8 +49,7 @@ import org.mockito.junit.MockitoJUnit /** * Tests for custom keyboard glyph map configuration. * - * Build/Install/Run: - * atest InputTests:KeyboardGlyphManagerTests + * Build/Install/Run: atest InputTests:KeyboardGlyphManagerTests */ @Presubmit class KeyboardGlyphManagerTests { @@ -66,15 +65,11 @@ class KeyboardGlyphManagerTests { const val RECEIVER_NAME = "DummyReceiver" } - @get:Rule - val setFlagsRule = SetFlagsRule() - @get:Rule - val mockitoRule = MockitoJUnit.rule()!! - @get:Rule - val inputManagerRule = MockInputManagerRule() + @get:Rule val setFlagsRule = SetFlagsRule() + @get:Rule val mockitoRule = MockitoJUnit.rule()!! + @get:Rule val inputManagerRule = MockInputManagerRule() - @Mock - private lateinit var packageManager: PackageManager + @Mock private lateinit var packageManager: PackageManager private lateinit var keyboardGlyphManager: KeyboardGlyphManager private lateinit var context: Context @@ -99,7 +94,8 @@ class KeyboardGlyphManagerTests { .thenReturn(inputManager) keyboardDevice = createKeyboard(DEVICE_ID, VENDOR_ID, PRODUCT_ID, 0, "", "") - Mockito.`when`(inputManagerRule.mock.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID, DEVICE_ID2)) + Mockito.`when`(inputManagerRule.mock.inputDeviceIds) + .thenReturn(intArrayOf(DEVICE_ID, DEVICE_ID2)) Mockito.`when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardDevice) val keyboardDevice2 = createKeyboard(DEVICE_ID2, VENDOR_ID2, PRODUCT_ID2, 0, "", "") @@ -110,19 +106,22 @@ class KeyboardGlyphManagerTests { Mockito.`when`(context.packageManager).thenReturn(packageManager) val info = createMockReceiver() - Mockito.`when`(packageManager.queryBroadcastReceiversAsUser(Mockito.any(), Mockito.anyInt(), - Mockito.anyInt())).thenReturn(listOf(info)) + Mockito.`when`( + packageManager.queryBroadcastReceiversAsUser( + Mockito.any(), + Mockito.anyInt(), + Mockito.anyInt(), + ) + ) + .thenReturn(listOf(info)) Mockito.`when`(packageManager.getReceiverInfo(Mockito.any(), Mockito.anyInt())) .thenReturn(info.activityInfo) val resources = context.resources Mockito.`when`( - packageManager.getResourcesForApplication( - Mockito.any( - ApplicationInfo::class.java - ) + packageManager.getResourcesForApplication(Mockito.any(ApplicationInfo::class.java)) ) - ).thenReturn(resources) + .thenReturn(resources) } private fun createMockReceiver(): ResolveInfo { @@ -134,7 +133,7 @@ class KeyboardGlyphManagerTests { info.activityInfo.metaData = Bundle() info.activityInfo.metaData.putInt( InputManager.META_DATA_KEYBOARD_GLYPH_MAPS, - R.xml.keyboard_glyph_maps + R.xml.keyboard_glyph_maps, ) info.serviceInfo = ServiceInfo() info.serviceInfo.packageName = PACKAGE_NAME @@ -147,15 +146,15 @@ class KeyboardGlyphManagerTests { fun testGlyphMapsLoaded() { assertNotNull( "Glyph map for test keyboard(deviceId=$DEVICE_ID) must exist", - keyboardGlyphManager.getKeyGlyphMap(DEVICE_ID) + keyboardGlyphManager.getKeyGlyphMap(DEVICE_ID), ) assertNotNull( "Glyph map for test keyboard(deviceId=$DEVICE_ID2) must exist", - keyboardGlyphManager.getKeyGlyphMap(DEVICE_ID2) + keyboardGlyphManager.getKeyGlyphMap(DEVICE_ID2), ) assertNull( "Glyph map for non-existing keyboard must be null", - keyboardGlyphManager.getKeyGlyphMap(-2) + keyboardGlyphManager.getKeyGlyphMap(-2), ) } @@ -178,16 +177,15 @@ class KeyboardGlyphManagerTests { assertEquals(2, hardwareShortcuts.size) assertEquals( KeyEvent.KEYCODE_BACK, - hardwareShortcuts[KeyCombination(KeyEvent.META_FUNCTION_ON, KeyEvent.KEYCODE_1)] + hardwareShortcuts[KeyCombination(KeyEvent.META_FUNCTION_ON, KeyEvent.KEYCODE_1)], ) assertEquals( KeyEvent.KEYCODE_HOME, hardwareShortcuts[ KeyCombination( KeyEvent.META_FUNCTION_ON or KeyEvent.META_META_ON, - KeyEvent.KEYCODE_2 - ) - ] + KeyEvent.KEYCODE_2, + )], ) } } diff --git a/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt index 4440a839caef..c83036ce05ba 100644 --- a/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt @@ -65,7 +65,7 @@ fun createKeyboard( productId: Int, deviceBus: Int, languageTag: String, - layoutType: String + layoutType: String, ): InputDevice = InputDevice.Builder() .setId(deviceId) @@ -84,8 +84,7 @@ fun createKeyboard( /** * Tests for {@link Default UI} and {@link New UI}. * - * Build/Install/Run: - * atest InputTests:KeyboardLayoutManagerTests + * Build/Install/Run: atest InputTests:KeyboardLayoutManagerTests */ @Presubmit class KeyboardLayoutManagerTests { @@ -118,19 +117,15 @@ class KeyboardLayoutManagerTests { @JvmField @Rule - val extendedMockitoRule = ExtendedMockitoRule.Builder(this) - .mockStatic(FrameworkStatsLog::class.java).build()!! + val extendedMockitoRule = + ExtendedMockitoRule.Builder(this).mockStatic(FrameworkStatsLog::class.java).build()!! - @get:Rule - val inputManagerRule = MockInputManagerRule() + @get:Rule val inputManagerRule = MockInputManagerRule() - @Mock - private lateinit var native: NativeInputManagerService + @Mock private lateinit var native: NativeInputManagerService - @Mock - private lateinit var packageManager: PackageManager - @Mock - private lateinit var notificationManager: NotificationManager + @Mock private lateinit var packageManager: PackageManager + @Mock private lateinit var notificationManager: NotificationManager private lateinit var keyboardLayoutManager: KeyboardLayoutManager private lateinit var imeInfo: InputMethodInfo @@ -149,23 +144,25 @@ class KeyboardLayoutManagerTests { @Before fun setup() { context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) - dataStore = PersistentDataStore(object : PersistentDataStore.Injector() { - override fun openRead(): InputStream? { - throw FileNotFoundException() - } - - override fun startWrite(): FileOutputStream? { - throw IOException() - } - - override fun finishWrite(fos: FileOutputStream?, success: Boolean) {} - }) + dataStore = + PersistentDataStore( + object : PersistentDataStore.Injector() { + override fun openRead(): InputStream? { + throw FileNotFoundException() + } + + override fun startWrite(): FileOutputStream? { + throw IOException() + } + + override fun finishWrite(fos: FileOutputStream?, success: Boolean) {} + } + ) testLooper = TestLooper() - keyboardLayoutManager = Mockito.spy( - KeyboardLayoutManager(context, native, dataStore, testLooper.looper) - ) + keyboardLayoutManager = + Mockito.spy(KeyboardLayoutManager(context, native, dataStore, testLooper.looper)) Mockito.`when`(context.getSystemService(Mockito.eq(Context.NOTIFICATION_SERVICE))) - .thenReturn(notificationManager) + .thenReturn(notificationManager) setupInputDevices() setupBroadcastReceiver() setupIme() @@ -176,47 +173,72 @@ class KeyboardLayoutManagerTests { Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) .thenReturn(inputManager) - keyboardDevice = createKeyboard(DEVICE_ID, DEFAULT_VENDOR_ID, DEFAULT_PRODUCT_ID, - DEFAULT_DEVICE_BUS, "", "") - vendorSpecificKeyboardDevice = createKeyboard(VENDOR_SPECIFIC_DEVICE_ID, 1, 1, - 1, "", "") - englishDvorakKeyboardDevice = createKeyboard(ENGLISH_DVORAK_DEVICE_ID, DEFAULT_VENDOR_ID, - DEFAULT_PRODUCT_ID, DEFAULT_DEVICE_BUS, "en", "dvorak") - englishQwertyKeyboardDevice = createKeyboard(ENGLISH_QWERTY_DEVICE_ID, DEFAULT_VENDOR_ID, - DEFAULT_PRODUCT_ID, DEFAULT_DEVICE_BUS, "en", "qwerty") - Mockito.`when`(inputManagerRule.mock.inputDeviceIds) - .thenReturn(intArrayOf( + keyboardDevice = + createKeyboard( DEVICE_ID, - VENDOR_SPECIFIC_DEVICE_ID, + DEFAULT_VENDOR_ID, + DEFAULT_PRODUCT_ID, + DEFAULT_DEVICE_BUS, + "", + "", + ) + vendorSpecificKeyboardDevice = createKeyboard(VENDOR_SPECIFIC_DEVICE_ID, 1, 1, 1, "", "") + englishDvorakKeyboardDevice = + createKeyboard( ENGLISH_DVORAK_DEVICE_ID, - ENGLISH_QWERTY_DEVICE_ID - )) + DEFAULT_VENDOR_ID, + DEFAULT_PRODUCT_ID, + DEFAULT_DEVICE_BUS, + "en", + "dvorak", + ) + englishQwertyKeyboardDevice = + createKeyboard( + ENGLISH_QWERTY_DEVICE_ID, + DEFAULT_VENDOR_ID, + DEFAULT_PRODUCT_ID, + DEFAULT_DEVICE_BUS, + "en", + "qwerty", + ) + Mockito.`when`(inputManagerRule.mock.inputDeviceIds) + .thenReturn( + intArrayOf( + DEVICE_ID, + VENDOR_SPECIFIC_DEVICE_ID, + ENGLISH_DVORAK_DEVICE_ID, + ENGLISH_QWERTY_DEVICE_ID, + ) + ) Mockito.`when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardDevice) Mockito.`when`(inputManagerRule.mock.getInputDevice(VENDOR_SPECIFIC_DEVICE_ID)) .thenReturn(vendorSpecificKeyboardDevice) Mockito.`when`(inputManagerRule.mock.getInputDevice(ENGLISH_DVORAK_DEVICE_ID)) .thenReturn(englishDvorakKeyboardDevice) Mockito.`when`(inputManagerRule.mock.getInputDevice(ENGLISH_QWERTY_DEVICE_ID)) - .thenReturn(englishQwertyKeyboardDevice) + .thenReturn(englishQwertyKeyboardDevice) } private fun setupBroadcastReceiver() { Mockito.`when`(context.packageManager).thenReturn(packageManager) val info = createMockReceiver() - Mockito.`when`(packageManager.queryBroadcastReceiversAsUser(Mockito.any(), Mockito.anyInt(), - Mockito.anyInt())).thenReturn(listOf(info)) + Mockito.`when`( + packageManager.queryBroadcastReceiversAsUser( + Mockito.any(), + Mockito.anyInt(), + Mockito.anyInt(), + ) + ) + .thenReturn(listOf(info)) Mockito.`when`(packageManager.getReceiverInfo(Mockito.any(), Mockito.anyInt())) .thenReturn(info.activityInfo) val resources = context.resources Mockito.`when`( - packageManager.getResourcesForApplication( - Mockito.any( - ApplicationInfo::class.java - ) + packageManager.getResourcesForApplication(Mockito.any(ApplicationInfo::class.java)) ) - ).thenReturn(resources) + .thenReturn(resources) } private fun setupIme() { @@ -229,22 +251,21 @@ class KeyboardLayoutManagerTests { assertNotEquals( "Keyboard layout API should not return empty array", 0, - keyboardLayouts.size + keyboardLayouts.size, ) assertTrue( "Keyboard layout API should provide English(US) layout", - hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) + hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR), ) } @Test fun testGetKeyboardLayout() { - val keyboardLayout = - keyboardLayoutManager.getKeyboardLayout(ENGLISH_US_LAYOUT_DESCRIPTOR) - assertEquals("getKeyboardLayout API should return correct Layout from " + - "available layouts", + val keyboardLayout = keyboardLayoutManager.getKeyboardLayout(ENGLISH_US_LAYOUT_DESCRIPTOR) + assertEquals( + "getKeyboardLayout API should return correct Layout from " + "available layouts", ENGLISH_US_LAYOUT_DESCRIPTOR, - keyboardLayout!!.descriptor + keyboardLayout!!.descriptor, ) } @@ -253,32 +274,44 @@ class KeyboardLayoutManagerTests { val imeSubtype = createImeSubtype() keyboardLayoutManager.setKeyboardLayoutForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype, - ENGLISH_UK_LAYOUT_DESCRIPTOR + keyboardDevice.identifier, + USER_ID, + imeInfo, + imeSubtype, + ENGLISH_UK_LAYOUT_DESCRIPTOR, ) var result = keyboardLayoutManager.getKeyboardLayoutForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype + keyboardDevice.identifier, + USER_ID, + imeInfo, + imeSubtype, ) assertEquals( "getKeyboardLayoutForInputDevice API should return the set layout", ENGLISH_UK_LAYOUT_DESCRIPTOR, - result.layoutDescriptor + result.layoutDescriptor, ) // This should replace previously set layout keyboardLayoutManager.setKeyboardLayoutForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype, - ENGLISH_US_LAYOUT_DESCRIPTOR + keyboardDevice.identifier, + USER_ID, + imeInfo, + imeSubtype, + ENGLISH_US_LAYOUT_DESCRIPTOR, ) result = keyboardLayoutManager.getKeyboardLayoutForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype + keyboardDevice.identifier, + USER_ID, + imeInfo, + imeSubtype, ) assertEquals( "getKeyboardLayoutForInputDevice API should return the last set layout", ENGLISH_US_LAYOUT_DESCRIPTOR, - result.layoutDescriptor + result.layoutDescriptor, ) } @@ -288,32 +321,42 @@ class KeyboardLayoutManagerTests { keyboardLayoutManager.setKeyboardLayoutOverrideForInputDevice( keyboardDevice.identifier, - ENGLISH_UK_LAYOUT_DESCRIPTOR + ENGLISH_UK_LAYOUT_DESCRIPTOR, ) var result = keyboardLayoutManager.getKeyboardLayoutForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype + keyboardDevice.identifier, + USER_ID, + imeInfo, + imeSubtype, ) assertEquals(LAYOUT_SELECTION_CRITERIA_DEVICE, result.selectionCriteria) assertEquals( "getKeyboardLayoutForInputDevice API should return the set layout", ENGLISH_UK_LAYOUT_DESCRIPTOR, - result.layoutDescriptor + result.layoutDescriptor, ) // This should replace the overriding layout set above keyboardLayoutManager.setKeyboardLayoutForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype, - ENGLISH_US_LAYOUT_DESCRIPTOR - ) - result = keyboardLayoutManager.getKeyboardLayoutForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype + keyboardDevice.identifier, + USER_ID, + imeInfo, + imeSubtype, + ENGLISH_US_LAYOUT_DESCRIPTOR, ) + result = + keyboardLayoutManager.getKeyboardLayoutForInputDevice( + keyboardDevice.identifier, + USER_ID, + imeInfo, + imeSubtype, + ) assertEquals(LAYOUT_SELECTION_CRITERIA_USER, result.selectionCriteria) assertEquals( "getKeyboardLayoutForInputDevice API should return the user set layout", ENGLISH_US_LAYOUT_DESCRIPTOR, - result.layoutDescriptor + result.layoutDescriptor, ) } @@ -322,54 +365,67 @@ class KeyboardLayoutManagerTests { // Check Layouts for "hi-Latn". It should return all 'Latn' keyboard layouts var keyboardLayouts = keyboardLayoutManager.getKeyboardLayoutListForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, - createImeSubtypeForLanguageTag("hi-Latn") + keyboardDevice.identifier, + USER_ID, + imeInfo, + createImeSubtypeForLanguageTag("hi-Latn"), ) assertNotEquals( "getKeyboardLayoutListForInputDevice API should return the list of " + - "supported layouts with matching script code", + "supported layouts with matching script code", 0, - keyboardLayouts.size + keyboardLayouts.size, ) - assertTrue("getKeyboardLayoutListForInputDevice API should return a list " + + assertTrue( + "getKeyboardLayoutListForInputDevice API should return a list " + "containing English(US) layout for hi-Latn", - containsLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) + containsLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR), ) - assertTrue("getKeyboardLayoutListForInputDevice API should return a list " + + assertTrue( + "getKeyboardLayoutListForInputDevice API should return a list " + "containing English(No script code) layout for hi-Latn", containsLayout( keyboardLayouts, - createLayoutDescriptor("keyboard_layout_english_without_script_code") - ) + createLayoutDescriptor("keyboard_layout_english_without_script_code"), + ), ) // Check Layouts for "hi" which by default uses 'Deva' script. keyboardLayouts = keyboardLayoutManager.getKeyboardLayoutListForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, - createImeSubtypeForLanguageTag("hi") + keyboardDevice.identifier, + USER_ID, + imeInfo, + createImeSubtypeForLanguageTag("hi"), ) - assertEquals("getKeyboardLayoutListForInputDevice API should return empty " + + assertEquals( + "getKeyboardLayoutListForInputDevice API should return empty " + "list if no supported layouts available", 0, - keyboardLayouts.size + keyboardLayouts.size, ) // If user manually selected some layout, always provide it in the layout list val imeSubtype = createImeSubtypeForLanguageTag("hi") keyboardLayoutManager.setKeyboardLayoutForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype, - ENGLISH_US_LAYOUT_DESCRIPTOR + keyboardDevice.identifier, + USER_ID, + imeInfo, + imeSubtype, + ENGLISH_US_LAYOUT_DESCRIPTOR, ) keyboardLayouts = keyboardLayoutManager.getKeyboardLayoutListForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, - imeSubtype + keyboardDevice.identifier, + USER_ID, + imeInfo, + imeSubtype, ) - assertEquals("getKeyboardLayoutListForInputDevice API should return user " + + assertEquals( + "getKeyboardLayoutListForInputDevice API should return user " + "selected layout even if the script is incompatible with IME", - 1, - keyboardLayouts.size + 1, + keyboardLayouts.size, ) // Special case Japanese: UScript ignores provided script code for certain language tags @@ -377,63 +433,71 @@ class KeyboardLayoutManagerTests { // script from language tags and match those. keyboardLayouts = keyboardLayoutManager.getKeyboardLayoutListForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, - createImeSubtypeForLanguageTag("ja-Latn-JP") + keyboardDevice.identifier, + USER_ID, + imeInfo, + createImeSubtypeForLanguageTag("ja-Latn-JP"), ) assertNotEquals( "getKeyboardLayoutListForInputDevice API should return the list of " + - "supported layouts with matching script code for ja-Latn-JP", + "supported layouts with matching script code for ja-Latn-JP", 0, - keyboardLayouts.size + keyboardLayouts.size, ) - assertTrue("getKeyboardLayoutListForInputDevice API should return a list " + + assertTrue( + "getKeyboardLayoutListForInputDevice API should return a list " + "containing English(US) layout for ja-Latn-JP", - containsLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) + containsLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR), ) - assertTrue("getKeyboardLayoutListForInputDevice API should return a list " + + assertTrue( + "getKeyboardLayoutListForInputDevice API should return a list " + "containing English(No script code) layout for ja-Latn-JP", containsLayout( keyboardLayouts, - createLayoutDescriptor("keyboard_layout_english_without_script_code") - ) + createLayoutDescriptor("keyboard_layout_english_without_script_code"), + ), ) // If script code not explicitly provided for Japanese should rely on Uscript to find // derived script code and hence no suitable layout will be found. keyboardLayouts = keyboardLayoutManager.getKeyboardLayoutListForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, - createImeSubtypeForLanguageTag("ja-JP") + keyboardDevice.identifier, + USER_ID, + imeInfo, + createImeSubtypeForLanguageTag("ja-JP"), ) assertEquals( "getKeyboardLayoutListForInputDevice API should return empty list of " + - "supported layouts with matching script code for ja-JP", + "supported layouts with matching script code for ja-JP", 0, - keyboardLayouts.size + keyboardLayouts.size, ) // If IME doesn't have a corresponding language tag, then should show all available // layouts no matter the script code. keyboardLayouts = keyboardLayoutManager.getKeyboardLayoutListForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, null + keyboardDevice.identifier, + USER_ID, + imeInfo, + null, ) assertNotEquals( "getKeyboardLayoutListForInputDevice API should return all layouts if" + "language tag or subtype not provided", 0, - keyboardLayouts.size + keyboardLayouts.size, ) - assertTrue("getKeyboardLayoutListForInputDevice API should contain Latin " + - "layouts if language tag or subtype not provided", - containsLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) + assertTrue( + "getKeyboardLayoutListForInputDevice API should contain Latin " + + "layouts if language tag or subtype not provided", + containsLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR), ) - assertTrue("getKeyboardLayoutListForInputDevice API should contain Cyrillic " + - "layouts if language tag or subtype not provided", - containsLayout( - keyboardLayouts, - createLayoutDescriptor("keyboard_layout_russian") - ) + assertTrue( + "getKeyboardLayoutListForInputDevice API should contain Cyrillic " + + "layouts if language tag or subtype not provided", + containsLayout(keyboardLayouts, createLayoutDescriptor("keyboard_layout_russian")), ) } @@ -442,46 +506,50 @@ class KeyboardLayoutManagerTests { assertCorrectLayout( keyboardDevice, createImeSubtypeForLanguageTag("en-US"), - ENGLISH_US_LAYOUT_DESCRIPTOR + ENGLISH_US_LAYOUT_DESCRIPTOR, ) assertCorrectLayout( keyboardDevice, createImeSubtypeForLanguageTag("en-GB"), - ENGLISH_UK_LAYOUT_DESCRIPTOR + ENGLISH_UK_LAYOUT_DESCRIPTOR, ) assertCorrectLayout( keyboardDevice, createImeSubtypeForLanguageTag("de"), - GERMAN_LAYOUT_DESCRIPTOR + GERMAN_LAYOUT_DESCRIPTOR, ) assertCorrectLayout( keyboardDevice, createImeSubtypeForLanguageTag("fr-FR"), - createLayoutDescriptor("keyboard_layout_french") + createLayoutDescriptor("keyboard_layout_french"), ) assertCorrectLayout( keyboardDevice, createImeSubtypeForLanguageTag("ru"), - createLayoutDescriptor("keyboard_layout_russian") + createLayoutDescriptor("keyboard_layout_russian"), ) assertEquals( "getDefaultKeyboardLayoutForInputDevice should return " + - "KeyboardLayoutSelectionResult.FAILED when no layout available", + "KeyboardLayoutSelectionResult.FAILED when no layout available", KeyboardLayoutSelectionResult.FAILED, keyboardLayoutManager.getKeyboardLayoutForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, - createImeSubtypeForLanguageTag("it") - ) + keyboardDevice.identifier, + USER_ID, + imeInfo, + createImeSubtypeForLanguageTag("it"), + ), ) assertEquals( "getDefaultKeyboardLayoutForInputDevice should return " + - "KeyboardLayoutSelectionResult.FAILED when no layout for script code is" + - "available", + "KeyboardLayoutSelectionResult.FAILED when no layout for script code is" + + "available", KeyboardLayoutSelectionResult.FAILED, keyboardLayoutManager.getKeyboardLayoutForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, - createImeSubtypeForLanguageTag("en-Deva") - ) + keyboardDevice.identifier, + USER_ID, + imeInfo, + createImeSubtypeForLanguageTag("en-Deva"), + ), ) } @@ -490,72 +558,75 @@ class KeyboardLayoutManagerTests { assertCorrectLayout( keyboardDevice, createImeSubtypeForLanguageTagAndLayoutType("en-US", "qwerty"), - ENGLISH_US_LAYOUT_DESCRIPTOR + ENGLISH_US_LAYOUT_DESCRIPTOR, ) assertCorrectLayout( keyboardDevice, createImeSubtypeForLanguageTagAndLayoutType("en-US", "dvorak"), - createLayoutDescriptor("keyboard_layout_english_us_dvorak") + createLayoutDescriptor("keyboard_layout_english_us_dvorak"), ) // Try to match layout type even if country doesn't match assertCorrectLayout( keyboardDevice, createImeSubtypeForLanguageTagAndLayoutType("en-GB", "dvorak"), - createLayoutDescriptor("keyboard_layout_english_us_dvorak") + createLayoutDescriptor("keyboard_layout_english_us_dvorak"), ) // Choose layout based on layout type priority, if layout type is not provided by IME // (Qwerty > Dvorak > Extended) assertCorrectLayout( keyboardDevice, createImeSubtypeForLanguageTagAndLayoutType("en-US", ""), - ENGLISH_US_LAYOUT_DESCRIPTOR + ENGLISH_US_LAYOUT_DESCRIPTOR, ) assertCorrectLayout( keyboardDevice, createImeSubtypeForLanguageTagAndLayoutType("en-GB", "qwerty"), - ENGLISH_UK_LAYOUT_DESCRIPTOR + ENGLISH_UK_LAYOUT_DESCRIPTOR, ) assertCorrectLayout( keyboardDevice, createImeSubtypeForLanguageTagAndLayoutType("de", "qwertz"), - GERMAN_LAYOUT_DESCRIPTOR + GERMAN_LAYOUT_DESCRIPTOR, ) // Wrong layout type should match with language if provided layout type not available assertCorrectLayout( keyboardDevice, createImeSubtypeForLanguageTagAndLayoutType("de", "qwerty"), - GERMAN_LAYOUT_DESCRIPTOR + GERMAN_LAYOUT_DESCRIPTOR, ) assertCorrectLayout( keyboardDevice, createImeSubtypeForLanguageTagAndLayoutType("fr-FR", "azerty"), - createLayoutDescriptor("keyboard_layout_french") + createLayoutDescriptor("keyboard_layout_french"), ) assertCorrectLayout( keyboardDevice, createImeSubtypeForLanguageTagAndLayoutType("ru", "qwerty"), - createLayoutDescriptor("keyboard_layout_russian_qwerty") + createLayoutDescriptor("keyboard_layout_russian_qwerty"), ) // If layout type is empty then prioritize KCM with empty layout type assertCorrectLayout( keyboardDevice, createImeSubtypeForLanguageTagAndLayoutType("ru", ""), - createLayoutDescriptor("keyboard_layout_russian") + createLayoutDescriptor("keyboard_layout_russian"), ) - assertEquals("getDefaultKeyboardLayoutForInputDevice should return " + + assertEquals( + "getDefaultKeyboardLayoutForInputDevice should return " + "KeyboardLayoutSelectionResult.FAILED when no layout for script code is" + "available", KeyboardLayoutSelectionResult.FAILED, keyboardLayoutManager.getKeyboardLayoutForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, - createImeSubtypeForLanguageTagAndLayoutType("en-Deva-US", "") - ) + keyboardDevice.identifier, + USER_ID, + imeInfo, + createImeSubtypeForLanguageTagAndLayoutType("en-Deva-US", ""), + ), ) // If prefer layout with empty country over mismatched country assertCorrectLayout( keyboardDevice, createImeSubtypeForLanguageTagAndLayoutType("en-AU", "qwerty"), - ENGLISH_US_LAYOUT_DESCRIPTOR + ENGLISH_US_LAYOUT_DESCRIPTOR, ) } @@ -567,7 +638,7 @@ class KeyboardLayoutManagerTests { assertCorrectLayout( englishDvorakKeyboardDevice, frenchSubtype, - createLayoutDescriptor("keyboard_layout_english_us_dvorak") + createLayoutDescriptor("keyboard_layout_english_us_dvorak"), ) // Back to back changing HW keyboards with same product and vendor ID but different @@ -575,7 +646,7 @@ class KeyboardLayoutManagerTests { assertCorrectLayout( englishQwertyKeyboardDevice, frenchSubtype, - createLayoutDescriptor("keyboard_layout_english_us") + createLayoutDescriptor("keyboard_layout_english_us"), ) // Fallback to IME information if the HW provided layout script is incompatible with the @@ -583,62 +654,72 @@ class KeyboardLayoutManagerTests { assertCorrectLayout( englishDvorakKeyboardDevice, createImeSubtypeForLanguageTagAndLayoutType("ru", ""), - createLayoutDescriptor("keyboard_layout_russian") + createLayoutDescriptor("keyboard_layout_russian"), ) } @Test fun testConfigurationLogged_onInputDeviceAdded_VirtualKeyboardBasedSelection() { - val imeInfos = listOf( - KeyboardLayoutManager.ImeInfo(0, imeInfo, - createImeSubtypeForLanguageTagAndLayoutType("de-Latn", "qwertz"))) + val imeInfos = + listOf( + KeyboardLayoutManager.ImeInfo( + 0, + imeInfo, + createImeSubtypeForLanguageTagAndLayoutType("de-Latn", "qwertz"), + ) + ) Mockito.doReturn(imeInfos).`when`(keyboardLayoutManager).imeInfoListForLayoutMapping keyboardLayoutManager.onInputDeviceAdded(keyboardDevice.id) ExtendedMockito.verify { FrameworkStatsLog.write( - ArgumentMatchers.eq(FrameworkStatsLog.KEYBOARD_CONFIGURED), - ArgumentMatchers.anyBoolean(), - ArgumentMatchers.eq(keyboardDevice.vendorId), - ArgumentMatchers.eq(keyboardDevice.productId), - ArgumentMatchers.eq( - createByteArray( - KeyboardMetricsCollector.DEFAULT_LANGUAGE_TAG, - LAYOUT_TYPE_DEFAULT, - GERMAN_LAYOUT_NAME, - KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD, - "de-Latn", - LAYOUT_TYPE_QWERTZ - ), - ), - ArgumentMatchers.eq(keyboardDevice.deviceBus), + ArgumentMatchers.eq(FrameworkStatsLog.KEYBOARD_CONFIGURED), + ArgumentMatchers.anyBoolean(), + ArgumentMatchers.eq(keyboardDevice.vendorId), + ArgumentMatchers.eq(keyboardDevice.productId), + ArgumentMatchers.eq( + createByteArray( + KeyboardMetricsCollector.DEFAULT_LANGUAGE_TAG, + LAYOUT_TYPE_DEFAULT, + GERMAN_LAYOUT_NAME, + KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD, + "de-Latn", + LAYOUT_TYPE_QWERTZ, + ) + ), + ArgumentMatchers.eq(keyboardDevice.deviceBus), ) } } @Test fun testConfigurationLogged_onInputDeviceAdded_DeviceBasedSelection() { - val imeInfos = listOf( - KeyboardLayoutManager.ImeInfo(0, imeInfo, - createImeSubtypeForLanguageTagAndLayoutType("de-Latn", "qwertz"))) + val imeInfos = + listOf( + KeyboardLayoutManager.ImeInfo( + 0, + imeInfo, + createImeSubtypeForLanguageTagAndLayoutType("de-Latn", "qwertz"), + ) + ) Mockito.doReturn(imeInfos).`when`(keyboardLayoutManager).imeInfoListForLayoutMapping keyboardLayoutManager.onInputDeviceAdded(englishQwertyKeyboardDevice.id) ExtendedMockito.verify { FrameworkStatsLog.write( - ArgumentMatchers.eq(FrameworkStatsLog.KEYBOARD_CONFIGURED), - ArgumentMatchers.anyBoolean(), - ArgumentMatchers.eq(englishQwertyKeyboardDevice.vendorId), - ArgumentMatchers.eq(englishQwertyKeyboardDevice.productId), - ArgumentMatchers.eq( - createByteArray( - "en", - LAYOUT_TYPE_QWERTY, - ENGLISH_US_LAYOUT_NAME, - KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEVICE, - "de-Latn", - LAYOUT_TYPE_QWERTZ - ) - ), - ArgumentMatchers.eq(keyboardDevice.deviceBus), + ArgumentMatchers.eq(FrameworkStatsLog.KEYBOARD_CONFIGURED), + ArgumentMatchers.anyBoolean(), + ArgumentMatchers.eq(englishQwertyKeyboardDevice.vendorId), + ArgumentMatchers.eq(englishQwertyKeyboardDevice.productId), + ArgumentMatchers.eq( + createByteArray( + "en", + LAYOUT_TYPE_QWERTY, + ENGLISH_US_LAYOUT_NAME, + KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEVICE, + "de-Latn", + LAYOUT_TYPE_QWERTZ, + ) + ), + ArgumentMatchers.eq(keyboardDevice.deviceBus), ) } } @@ -650,21 +731,21 @@ class KeyboardLayoutManagerTests { keyboardLayoutManager.onInputDeviceAdded(keyboardDevice.id) ExtendedMockito.verify { FrameworkStatsLog.write( - ArgumentMatchers.eq(FrameworkStatsLog.KEYBOARD_CONFIGURED), - ArgumentMatchers.anyBoolean(), - ArgumentMatchers.eq(keyboardDevice.vendorId), - ArgumentMatchers.eq(keyboardDevice.productId), - ArgumentMatchers.eq( - createByteArray( - KeyboardMetricsCollector.DEFAULT_LANGUAGE_TAG, - LAYOUT_TYPE_DEFAULT, - "Default", - KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEFAULT, - KeyboardMetricsCollector.DEFAULT_LANGUAGE_TAG, - LAYOUT_TYPE_DEFAULT - ), - ), - ArgumentMatchers.eq(keyboardDevice.deviceBus), + ArgumentMatchers.eq(FrameworkStatsLog.KEYBOARD_CONFIGURED), + ArgumentMatchers.anyBoolean(), + ArgumentMatchers.eq(keyboardDevice.vendorId), + ArgumentMatchers.eq(keyboardDevice.productId), + ArgumentMatchers.eq( + createByteArray( + KeyboardMetricsCollector.DEFAULT_LANGUAGE_TAG, + LAYOUT_TYPE_DEFAULT, + "Default", + KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEFAULT, + KeyboardMetricsCollector.DEFAULT_LANGUAGE_TAG, + LAYOUT_TYPE_DEFAULT, + ) + ), + ArgumentMatchers.eq(keyboardDevice.deviceBus), ) } } @@ -674,82 +755,86 @@ class KeyboardLayoutManagerTests { val imeInfos = listOf(KeyboardLayoutManager.ImeInfo(0, imeInfo, createImeSubtype())) Mockito.doReturn(imeInfos).`when`(keyboardLayoutManager).imeInfoListForLayoutMapping keyboardLayoutManager.onInputDeviceChanged(keyboardDevice.id) - ExtendedMockito.verify({ - FrameworkStatsLog.write( + ExtendedMockito.verify( + { + FrameworkStatsLog.write( ArgumentMatchers.eq(FrameworkStatsLog.KEYBOARD_CONFIGURED), ArgumentMatchers.anyBoolean(), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt(), ArgumentMatchers.any(ByteArray::class.java), ArgumentMatchers.anyInt(), - ) - }, Mockito.times(0)) + ) + }, + Mockito.times(0), + ) } @Test fun testNotificationShown_onInputDeviceChanged() { val imeInfos = listOf(KeyboardLayoutManager.ImeInfo(0, imeInfo, createImeSubtype())) Mockito.doReturn(imeInfos).`when`(keyboardLayoutManager).imeInfoListForLayoutMapping - Mockito.doReturn(false).`when`(keyboardLayoutManager).isVirtualDevice( - ArgumentMatchers.eq(keyboardDevice.id) - ) + Mockito.doReturn(false) + .`when`(keyboardLayoutManager) + .isVirtualDevice(ArgumentMatchers.eq(keyboardDevice.id)) keyboardLayoutManager.onInputDeviceChanged(keyboardDevice.id) - ExtendedMockito.verify( - notificationManager, - Mockito.times(1) - ).notifyAsUser( - ArgumentMatchers.isNull(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any(), - ArgumentMatchers.any() - ) + ExtendedMockito.verify(notificationManager, Mockito.times(1)) + .notifyAsUser( + ArgumentMatchers.isNull(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.any(), + ArgumentMatchers.any(), + ) } @Test fun testNotificationNotShown_onInputDeviceChanged_forVirtualDevice() { val imeInfos = listOf(KeyboardLayoutManager.ImeInfo(0, imeInfo, createImeSubtype())) Mockito.doReturn(imeInfos).`when`(keyboardLayoutManager).imeInfoListForLayoutMapping - Mockito.doReturn(true).`when`(keyboardLayoutManager).isVirtualDevice( - ArgumentMatchers.eq(keyboardDevice.id) - ) + Mockito.doReturn(true) + .`when`(keyboardLayoutManager) + .isVirtualDevice(ArgumentMatchers.eq(keyboardDevice.id)) keyboardLayoutManager.onInputDeviceChanged(keyboardDevice.id) - ExtendedMockito.verify( - notificationManager, - Mockito.never() - ).notifyAsUser( - ArgumentMatchers.isNull(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any(), - ArgumentMatchers.any() - ) + ExtendedMockito.verify(notificationManager, Mockito.never()) + .notifyAsUser( + ArgumentMatchers.isNull(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.any(), + ArgumentMatchers.any(), + ) } private fun assertCorrectLayout( device: InputDevice, imeSubtype: InputMethodSubtype, - expectedLayout: String + expectedLayout: String, ) { - val result = keyboardLayoutManager.getKeyboardLayoutForInputDevice( - device.identifier, USER_ID, imeInfo, imeSubtype - ) + val result = + keyboardLayoutManager.getKeyboardLayoutForInputDevice( + device.identifier, + USER_ID, + imeInfo, + imeSubtype, + ) assertEquals( "getDefaultKeyboardLayoutForInputDevice should return $expectedLayout", expectedLayout, - result.layoutDescriptor + result.layoutDescriptor, ) } private fun createImeSubtype(): InputMethodSubtype = - createImeSubtypeForLanguageTagAndLayoutType(null, null) + createImeSubtypeForLanguageTagAndLayoutType(null, null) private fun createImeSubtypeForLanguageTag(languageTag: String): InputMethodSubtype = - createImeSubtypeForLanguageTagAndLayoutType(languageTag, null) + createImeSubtypeForLanguageTagAndLayoutType(languageTag, null) private fun createImeSubtypeForLanguageTagAndLayoutType( - languageTag: String?, - layoutType: String? + languageTag: String?, + layoutType: String?, ): InputMethodSubtype { - val builder = InputMethodSubtype.InputMethodSubtypeBuilder() + val builder = + InputMethodSubtype.InputMethodSubtypeBuilder() .setSubtypeId(nextImeSubtypeId++) .setIsAuxiliary(false) .setSubtypeMode("keyboard") @@ -762,39 +847,39 @@ class KeyboardLayoutManagerTests { } private fun createByteArray( - expectedLanguageTag: String, - expectedLayoutType: Int, - expectedLayoutName: String, - expectedCriteria: Int, - expectedImeLanguageTag: String, - expectedImeLayoutType: Int + expectedLanguageTag: String, + expectedLayoutType: Int, + expectedLayoutName: String, + expectedCriteria: Int, + expectedImeLanguageTag: String, + expectedImeLayoutType: Int, ): ByteArray { val proto = ProtoOutputStream() - val keyboardLayoutConfigToken = proto.start( - KeyboardConfiguredProto.RepeatedKeyboardLayoutConfig.KEYBOARD_LAYOUT_CONFIG) + val keyboardLayoutConfigToken = + proto.start(KeyboardConfiguredProto.RepeatedKeyboardLayoutConfig.KEYBOARD_LAYOUT_CONFIG) proto.write( - KeyboardConfiguredProto.KeyboardLayoutConfig.KEYBOARD_LANGUAGE_TAG, - expectedLanguageTag + KeyboardConfiguredProto.KeyboardLayoutConfig.KEYBOARD_LANGUAGE_TAG, + expectedLanguageTag, ) proto.write( - KeyboardConfiguredProto.KeyboardLayoutConfig.KEYBOARD_LAYOUT_TYPE, - expectedLayoutType + KeyboardConfiguredProto.KeyboardLayoutConfig.KEYBOARD_LAYOUT_TYPE, + expectedLayoutType, ) proto.write( - KeyboardConfiguredProto.KeyboardLayoutConfig.KEYBOARD_LAYOUT_NAME, - expectedLayoutName + KeyboardConfiguredProto.KeyboardLayoutConfig.KEYBOARD_LAYOUT_NAME, + expectedLayoutName, ) proto.write( - KeyboardConfiguredProto.KeyboardLayoutConfig.LAYOUT_SELECTION_CRITERIA, - expectedCriteria + KeyboardConfiguredProto.KeyboardLayoutConfig.LAYOUT_SELECTION_CRITERIA, + expectedCriteria, ) proto.write( - KeyboardConfiguredProto.KeyboardLayoutConfig.IME_LANGUAGE_TAG, - expectedImeLanguageTag + KeyboardConfiguredProto.KeyboardLayoutConfig.IME_LANGUAGE_TAG, + expectedImeLanguageTag, ) proto.write( - KeyboardConfiguredProto.KeyboardLayoutConfig.IME_LAYOUT_TYPE, - expectedImeLayoutType + KeyboardConfiguredProto.KeyboardLayoutConfig.IME_LAYOUT_TYPE, + expectedImeLayoutType, ) proto.end(keyboardLayoutConfigToken) return proto.bytes @@ -830,7 +915,7 @@ class KeyboardLayoutManagerTests { info.activityInfo.metaData = Bundle() info.activityInfo.metaData.putInt( InputManager.META_DATA_KEYBOARD_LAYOUTS, - R.xml.keyboard_layouts + R.xml.keyboard_layouts, ) info.serviceInfo = ServiceInfo() info.serviceInfo.packageName = PACKAGE_NAME diff --git a/tests/Input/src/com/android/server/input/KeyboardMetricsCollectorTests.kt b/tests/Input/src/com/android/server/input/KeyboardMetricsCollectorTests.kt index 0615941eda09..5251f3d3f1d3 100644 --- a/tests/Input/src/com/android/server/input/KeyboardMetricsCollectorTests.kt +++ b/tests/Input/src/com/android/server/input/KeyboardMetricsCollectorTests.kt @@ -33,7 +33,7 @@ private fun createKeyboard( productId: Int, deviceBus: Int, languageTag: String?, - layoutType: String? + layoutType: String?, ): InputDevice = InputDevice.Builder() .setId(deviceId) @@ -52,16 +52,17 @@ private fun createKeyboard( private fun createImeSubtype( imeSubtypeId: Int, languageTag: ULocale?, - layoutType: String + layoutType: String, ): InputMethodSubtype = - InputMethodSubtype.InputMethodSubtypeBuilder().setSubtypeId(imeSubtypeId) - .setPhysicalKeyboardHint(languageTag, layoutType).build() + InputMethodSubtype.InputMethodSubtypeBuilder() + .setSubtypeId(imeSubtypeId) + .setPhysicalKeyboardHint(languageTag, layoutType) + .build() /** * Tests for {@link KeyboardMetricsCollector}. * - * Build/Install/Run: - * atest InputTests:KeyboardMetricsCollectorTests + * Build/Install/Run: atest InputTests:KeyboardMetricsCollectorTests */ @Presubmit class KeyboardMetricsCollectorTests { @@ -77,15 +78,16 @@ class KeyboardMetricsCollectorTests { fun testCreateKeyboardConfigurationEvent_throwsExceptionWithoutAnyLayoutConfiguration() { assertThrows(IllegalStateException::class.java) { KeyboardMetricsCollector.KeyboardConfigurationEvent.Builder( - createKeyboard( - DEVICE_ID, - DEFAULT_VENDOR_ID, - DEFAULT_PRODUCT_ID, - DEFAULT_DEVICE_BUS, - null, - null + createKeyboard( + DEVICE_ID, + DEFAULT_VENDOR_ID, + DEFAULT_PRODUCT_ID, + DEFAULT_DEVICE_BUS, + null, + null, + ) ) - ).build() + .build() } } @@ -93,66 +95,78 @@ class KeyboardMetricsCollectorTests { fun testCreateKeyboardConfigurationEvent_throwsExceptionWithInvalidLayoutSelectionCriteria() { assertThrows(IllegalStateException::class.java) { KeyboardMetricsCollector.KeyboardConfigurationEvent.Builder( - createKeyboard( - DEVICE_ID, - DEFAULT_VENDOR_ID, - DEFAULT_PRODUCT_ID, - DEFAULT_DEVICE_BUS, + createKeyboard( + DEVICE_ID, + DEFAULT_VENDOR_ID, + DEFAULT_PRODUCT_ID, + DEFAULT_DEVICE_BUS, + null, + null, + ) + ) + .addLayoutSelection( + createImeSubtype(1, ULocale.forLanguageTag("en-US"), "qwerty"), null, - null + 123, ) - ).addLayoutSelection(createImeSubtype(1, ULocale.forLanguageTag("en-US"), "qwerty"), - null, 123).build() + .build() } } @Test fun testCreateKeyboardConfigurationEvent_withMultipleConfigurations() { - val builder = KeyboardMetricsCollector.KeyboardConfigurationEvent.Builder( - createKeyboard( - DEVICE_ID, - DEFAULT_VENDOR_ID, - DEFAULT_PRODUCT_ID, - DEFAULT_DEVICE_BUS, - "de-CH", - "qwertz" + val builder = + KeyboardMetricsCollector.KeyboardConfigurationEvent.Builder( + createKeyboard( + DEVICE_ID, + DEFAULT_VENDOR_ID, + DEFAULT_PRODUCT_ID, + DEFAULT_DEVICE_BUS, + "de-CH", + "qwertz", + ) ) - ) - val event = builder.addLayoutSelection( - createImeSubtype(1, ULocale.forLanguageTag("en-US"), "qwerty"), - "English(US)(Qwerty)", - KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD - ).addLayoutSelection( - createImeSubtype(2, ULocale.forLanguageTag("en-US"), "azerty"), - null, // Default layout type - KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_USER - ).addLayoutSelection( - createImeSubtype(3, ULocale.forLanguageTag("en-US"), "qwerty"), - "German", - KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEVICE - ).setIsFirstTimeConfiguration(true).build() + val event = + builder + .addLayoutSelection( + createImeSubtype(1, ULocale.forLanguageTag("en-US"), "qwerty"), + "English(US)(Qwerty)", + KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD, + ) + .addLayoutSelection( + createImeSubtype(2, ULocale.forLanguageTag("en-US"), "azerty"), + null, // Default layout type + KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_USER, + ) + .addLayoutSelection( + createImeSubtype(3, ULocale.forLanguageTag("en-US"), "qwerty"), + "German", + KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEVICE, + ) + .setIsFirstTimeConfiguration(true) + .build() assertEquals( "KeyboardConfigurationEvent should pick vendor ID from provided InputDevice", DEFAULT_VENDOR_ID, - event.vendorId + event.vendorId, ) assertEquals( "KeyboardConfigurationEvent should pick product ID from provided InputDevice", DEFAULT_PRODUCT_ID, - event.productId + event.productId, ) assertEquals( - "KeyboardConfigurationEvent should pick device bus from provided InputDevice", - DEFAULT_DEVICE_BUS, - event.deviceBus + "KeyboardConfigurationEvent should pick device bus from provided InputDevice", + DEFAULT_DEVICE_BUS, + event.deviceBus, ) assertTrue(event.isFirstConfiguration) assertEquals( "KeyboardConfigurationEvent should contain 3 configurations provided", 3, - event.layoutConfigurations.size + event.layoutConfigurations.size, ) assertExpectedLayoutConfiguration( event.layoutConfigurations[0], @@ -185,21 +199,25 @@ class KeyboardMetricsCollectorTests { @Test fun testCreateKeyboardConfigurationEvent_withDefaultLanguageTag() { - val builder = KeyboardMetricsCollector.KeyboardConfigurationEvent.Builder( - createKeyboard( - DEVICE_ID, - DEFAULT_VENDOR_ID, - DEFAULT_PRODUCT_ID, - DEFAULT_DEVICE_BUS, - "und", // Undefined language tag - "azerty" + val builder = + KeyboardMetricsCollector.KeyboardConfigurationEvent.Builder( + createKeyboard( + DEVICE_ID, + DEFAULT_VENDOR_ID, + DEFAULT_PRODUCT_ID, + DEFAULT_DEVICE_BUS, + "und", // Undefined language tag + "azerty", + ) ) - ) - val event = builder.addLayoutSelection( - createImeSubtype(4, null, "qwerty"), // Default language tag - "German", - KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEVICE - ).build() + val event = + builder + .addLayoutSelection( + createImeSubtype(4, null, "qwerty"), // Default language tag + "German", + KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEVICE, + ) + .build() assertExpectedLayoutConfiguration( event.layoutConfigurations[0], @@ -219,7 +237,7 @@ class KeyboardMetricsCollectorTests { expectedSelectedLayout: String, expectedLayoutSelectionCriteria: Int, expectedImeLanguageTag: String, - expectedImeLayoutType: Int + expectedImeLayoutType: Int, ) { assertEquals(expectedKeyboardLanguageTag, configuration.keyboardLanguageTag) assertEquals(expectedKeyboardLayoutType, configuration.keyboardLayoutType) diff --git a/tests/Input/src/com/android/server/input/PointerIconCacheTest.kt b/tests/Input/src/com/android/server/input/PointerIconCacheTest.kt index 47e7ac720a08..d9ae7f339ed9 100644 --- a/tests/Input/src/com/android/server/input/PointerIconCacheTest.kt +++ b/tests/Input/src/com/android/server/input/PointerIconCacheTest.kt @@ -34,19 +34,14 @@ import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -/** - * Tests for {@link PointerIconCache}. - */ +/** Tests for {@link PointerIconCache}. */ @Presubmit class PointerIconCacheTest { - @get:Rule - val rule = MockitoJUnit.rule()!! + @get:Rule val rule = MockitoJUnit.rule()!! - @Mock - private lateinit var native: NativeInputManagerService - @Mock - private lateinit var defaultDisplay: Display + @Mock private lateinit var native: NativeInputManagerService + @Mock private lateinit var defaultDisplay: Display private lateinit var context: Context private lateinit var testLooper: TestLooper @@ -56,9 +51,10 @@ class PointerIconCacheTest { fun setup() { whenever(defaultDisplay.displayId).thenReturn(Display.DEFAULT_DISPLAY) - context = object : ContextWrapper(InstrumentationRegistry.getInstrumentation().context) { - override fun getDisplay() = defaultDisplay - } + context = + object : ContextWrapper(InstrumentationRegistry.getInstrumentation().context) { + override fun getDisplay() = defaultDisplay + } testLooper = TestLooper() cache = PointerIconCache(context, native, Handler(testLooper.looper)) diff --git a/tests/Input/src/com/android/test/input/InputEventAssignerTest.kt b/tests/Input/src/com/android/test/input/InputEventAssignerTest.kt index 015e188fc98e..faa68bd484f6 100644 --- a/tests/Input/src/com/android/test/input/InputEventAssignerTest.kt +++ b/tests/Input/src/com/android/test/input/InputEventAssignerTest.kt @@ -17,10 +17,9 @@ package com.android.test.input import android.view.InputDevice.SOURCE_MOUSE -import android.view.InputDevice.SOURCE_TOUCHSCREEN import android.view.InputDevice.SOURCE_STYLUS import android.view.InputDevice.SOURCE_TOUCHPAD - +import android.view.InputDevice.SOURCE_TOUCHSCREEN import android.view.InputEventAssigner import android.view.KeyEvent import android.view.MotionEvent @@ -28,13 +27,13 @@ import org.junit.Assert.assertEquals import org.junit.Test sealed class StreamEvent + private data object Vsync : StreamEvent() + data class MotionEventData(val action: Int, val source: Int, val id: Int, val expectedId: Int) : StreamEvent() -/** - * Create a MotionEvent with the provided action, eventTime, and source - */ +/** Create a MotionEvent with the provided action, eventTime, and source */ fun createMotionEvent(action: Int, eventTime: Long, source: Int): MotionEvent { val downTime: Long = 10 val x = 1f @@ -47,8 +46,22 @@ fun createMotionEvent(action: Int, eventTime: Long, source: Int): MotionEvent { val deviceId = 1 val edgeFlags = 0 val displayId = 0 - return MotionEvent.obtain(downTime, eventTime, action, x, y, pressure, size, metaState, - xPrecision, yPrecision, deviceId, edgeFlags, source, displayId) + return MotionEvent.obtain( + downTime, + eventTime, + action, + x, + y, + pressure, + size, + metaState, + xPrecision, + yPrecision, + deviceId, + edgeFlags, + source, + displayId, + ) } private fun createKeyEvent(action: Int, eventTime: Long): KeyEvent { @@ -58,11 +71,10 @@ private fun createKeyEvent(action: Int, eventTime: Long): KeyEvent { } /** - * Check that the correct eventIds are assigned in a stream. The stream consists of motion - * events or vsync (processed frame) - * Each streamEvent should have unique ids when writing tests - * The test passes even if two events get assigned the same eventId, since the mapping is - * streamEventId -> motionEventId and streamEvents have unique ids + * Check that the correct eventIds are assigned in a stream. The stream consists of motion events or + * vsync (processed frame) Each streamEvent should have unique ids when writing tests The test + * passes even if two events get assigned the same eventId, since the mapping is streamEventId -> + * motionEventId and streamEvents have unique ids */ private fun checkEventStream(vararg streamEvents: StreamEvent) { val assigner = InputEventAssigner() @@ -90,9 +102,7 @@ class InputEventAssignerTest { private const val TAG = "InputEventAssignerTest" } - /** - * A single event should be assigned to the next available frame. - */ + /** A single event should be assigned to the next available frame. */ @Test fun testTouchMove() { checkEventStream( @@ -145,7 +155,7 @@ class InputEventAssignerTest { MotionEventData(MotionEvent.ACTION_DOWN, source, id = 1, expectedId = 1), MotionEventData(MotionEvent.ACTION_MOVE, source, id = 2, expectedId = 1), Vsync, - MotionEventData(MotionEvent.ACTION_MOVE, source, id = 4, expectedId = 4) + MotionEventData(MotionEvent.ACTION_MOVE, source, id = 4, expectedId = 4), ) } @@ -169,57 +179,47 @@ class InputEventAssignerTest { testDownAndMove(SOURCE_TOUCHPAD) } - /** - * After an up event, motion events should be assigned their own event id - */ + /** After an up event, motion events should be assigned their own event id */ @Test fun testMouseDownUpAndScroll() { checkEventStream( MotionEventData(MotionEvent.ACTION_DOWN, SOURCE_MOUSE, id = 1, expectedId = 1), MotionEventData(MotionEvent.ACTION_UP, SOURCE_MOUSE, id = 2, expectedId = 2), - MotionEventData(MotionEvent.ACTION_SCROLL, SOURCE_MOUSE, id = 3, expectedId = 3) + MotionEventData(MotionEvent.ACTION_SCROLL, SOURCE_MOUSE, id = 3, expectedId = 3), ) } - /** - * After an up event, motion events should be assigned their own event id - */ + /** After an up event, motion events should be assigned their own event id */ @Test fun testStylusDownUpAndHover() { checkEventStream( MotionEventData(MotionEvent.ACTION_DOWN, SOURCE_STYLUS, id = 1, expectedId = 1), MotionEventData(MotionEvent.ACTION_UP, SOURCE_STYLUS, id = 2, expectedId = 2), - MotionEventData(MotionEvent.ACTION_HOVER_ENTER, SOURCE_STYLUS, id = 3, expectedId = 3) + MotionEventData(MotionEvent.ACTION_HOVER_ENTER, SOURCE_STYLUS, id = 3, expectedId = 3), ) } - /** - * After a cancel event, motion events should be assigned their own event id - */ + /** After a cancel event, motion events should be assigned their own event id */ @Test fun testMouseDownCancelAndScroll() { checkEventStream( MotionEventData(MotionEvent.ACTION_DOWN, SOURCE_MOUSE, id = 1, expectedId = 1), MotionEventData(MotionEvent.ACTION_CANCEL, SOURCE_MOUSE, id = 2, expectedId = 2), - MotionEventData(MotionEvent.ACTION_SCROLL, SOURCE_MOUSE, id = 3, expectedId = 3) + MotionEventData(MotionEvent.ACTION_SCROLL, SOURCE_MOUSE, id = 3, expectedId = 3), ) } - /** - * After a cancel event, motion events should be assigned their own event id - */ + /** After a cancel event, motion events should be assigned their own event id */ @Test fun testStylusDownCancelAndHover() { checkEventStream( MotionEventData(MotionEvent.ACTION_DOWN, SOURCE_STYLUS, id = 1, expectedId = 1), MotionEventData(MotionEvent.ACTION_CANCEL, SOURCE_STYLUS, id = 2, expectedId = 2), - MotionEventData(MotionEvent.ACTION_HOVER_ENTER, SOURCE_STYLUS, id = 3, expectedId = 3) + MotionEventData(MotionEvent.ACTION_HOVER_ENTER, SOURCE_STYLUS, id = 3, expectedId = 3), ) } - /** - * KeyEvents are processed immediately, so the latest event should be returned. - */ + /** KeyEvents are processed immediately, so the latest event should be returned. */ @Test fun testKeyEvent() { val assigner = InputEventAssigner() diff --git a/tests/Input/src/com/android/test/input/InputEventSenderAndReceiverTest.kt b/tests/Input/src/com/android/test/input/InputEventSenderAndReceiverTest.kt index 075cf0cc5a45..acd5a7d3f9e2 100644 --- a/tests/Input/src/com/android/test/input/InputEventSenderAndReceiverTest.kt +++ b/tests/Input/src/com/android/test/input/InputEventSenderAndReceiverTest.kt @@ -22,8 +22,8 @@ import android.view.InputChannel import android.view.InputEvent import android.view.InputEventReceiver import android.view.KeyEvent -import org.junit.Assert.assertEquals import org.junit.After +import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -42,12 +42,17 @@ private fun assertKeyEvent(expected: KeyEvent, received: KeyEvent) { } private fun getTestKeyEvent(): KeyEvent { - return KeyEvent(1 /*downTime*/, 1 /*eventTime*/, KeyEvent.ACTION_DOWN, - KeyEvent.KEYCODE_A, 0 /*repeat*/) + return KeyEvent( + 1 /*downTime*/, + 1 /*eventTime*/, + KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_A, + 0, /*repeat*/ + ) } private class CrashingInputEventReceiver(channel: InputChannel, looper: Looper) : - InputEventReceiver(channel, looper) { + InputEventReceiver(channel, looper) { override fun onInputEvent(event: InputEvent) { try { throw IllegalArgumentException("This receiver crashes when it receives input event") @@ -61,6 +66,7 @@ class InputEventSenderAndReceiverTest { companion object { private const val TAG = "InputEventSenderAndReceiverTest" } + private val mHandlerThread = HandlerThread("Process input events") private lateinit var mReceiver: SpyInputEventReceiver private lateinit var mSender: SpyInputEventSender @@ -98,8 +104,8 @@ class InputEventSenderAndReceiverTest { // The timeline case is slightly unusual because it goes from InputConsumer to InputPublisher. @Test fun testSendAndReceiveTimeline() { - val sent = SpyInputEventSender.Timeline( - inputEventId = 1, gpuCompletedTime = 2, presentTime = 3) + val sent = + SpyInputEventSender.Timeline(inputEventId = 1, gpuCompletedTime = 2, presentTime = 3) mReceiver.reportTimeline(sent.inputEventId, sent.gpuCompletedTime, sent.presentTime) val received = mSender.getTimeline() assertEquals(sent, received) @@ -110,8 +116,8 @@ class InputEventSenderAndReceiverTest { // event processing. @Test fun testSendAndReceiveInvalidTimeline() { - val sent = SpyInputEventSender.Timeline( - inputEventId = 1, gpuCompletedTime = 3, presentTime = 2) + val sent = + SpyInputEventSender.Timeline(inputEventId = 1, gpuCompletedTime = 3, presentTime = 2) mReceiver.reportTimeline(sent.inputEventId, sent.gpuCompletedTime, sent.presentTime) mSender.assertNoEvents() // Sender will no longer receive callbacks for this fd, even if receiver sends a valid @@ -123,9 +129,8 @@ class InputEventSenderAndReceiverTest { /** * If a receiver throws an exception during 'onInputEvent' execution, the 'finally' block still * completes, and therefore, finishInputEvent is called. Make sure that there's no crash in the - * native layer in these circumstances. - * In this test, we are reusing the 'mHandlerThread', but we are creating new sender and - * receiver. + * native layer in these circumstances. In this test, we are reusing the 'mHandlerThread', but + * we are creating new sender and receiver. */ @Test fun testCrashingReceiverDoesNotCrash() { diff --git a/tests/Input/src/com/android/test/input/KeyCharacterMapTest.kt b/tests/Input/src/com/android/test/input/KeyCharacterMapTest.kt index 860d9f680c4c..4ca08d839c54 100644 --- a/tests/Input/src/com/android/test/input/KeyCharacterMapTest.kt +++ b/tests/Input/src/com/android/test/input/KeyCharacterMapTest.kt @@ -18,12 +18,9 @@ package com.android.test.input import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule - import android.view.KeyCharacterMap import android.view.KeyEvent - import com.android.hardware.input.Flags - import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Rule @@ -32,13 +29,10 @@ import org.junit.Test /** * Tests for {@link KeyCharacterMap}. * - * <p>Build/Install/Run: - * atest KeyCharacterMapTest - * + * <p>Build/Install/Run: atest KeyCharacterMapTest */ class KeyCharacterMapTest { - @get:Rule - val setFlagsRule = SetFlagsRule() + @get:Rule val setFlagsRule = SetFlagsRule() @Test @EnableFlags(Flags.FLAG_REMOVE_FALLBACK_MODIFIERS) @@ -47,28 +41,33 @@ class KeyCharacterMapTest { val keyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD) // One modifier fallback. - val oneModifierFallback = keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_SPACE, - KeyEvent.META_CTRL_ON) + val oneModifierFallback = + keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_SPACE, KeyEvent.META_CTRL_ON) assertEquals(KeyEvent.KEYCODE_LANGUAGE_SWITCH, oneModifierFallback.keyCode) assertEquals(0, oneModifierFallback.metaState) // Multiple modifier fallback. - val twoModifierFallback = keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_DEL, - KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON) + val twoModifierFallback = + keyCharacterMap.getFallbackAction( + KeyEvent.KEYCODE_DEL, + KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON, + ) assertEquals(KeyEvent.KEYCODE_BACK, twoModifierFallback.keyCode) assertEquals(0, twoModifierFallback.metaState) // No default button, fallback only. - val keyOnlyFallback = - keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_BUTTON_A, 0) + val keyOnlyFallback = keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_BUTTON_A, 0) assertEquals(KeyEvent.KEYCODE_DPAD_CENTER, keyOnlyFallback.keyCode) assertEquals(0, keyOnlyFallback.metaState) // A key event that is not an exact match for a fallback. Expect a null return. // E.g. Ctrl + Space -> LanguageSwitch // Ctrl + Alt + Space -> Ctrl + Alt + Space (No fallback). - val noMatchFallback = keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_SPACE, - KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON) + val noMatchFallback = + keyCharacterMap.getFallbackAction( + KeyEvent.KEYCODE_SPACE, + KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON, + ) assertNull(noMatchFallback) } } diff --git a/tests/Input/src/com/android/test/input/MockInputManagerRule.kt b/tests/Input/src/com/android/test/input/MockInputManagerRule.kt index cef985600c40..8fa5f4a03749 100644 --- a/tests/Input/src/com/android/test/input/MockInputManagerRule.kt +++ b/tests/Input/src/com/android/test/input/MockInputManagerRule.kt @@ -21,8 +21,8 @@ import org.junit.rules.ExternalResource import org.mockito.Mockito /** - * A test rule that temporarily replaces the [IInputManager] connection to the server with a mock - * to be used for testing. + * A test rule that temporarily replaces the [IInputManager] connection to the server with a mock to + * be used for testing. */ class MockInputManagerRule : ExternalResource() { diff --git a/tests/Input/src/com/android/test/input/MotionPredictorTest.kt b/tests/Input/src/com/android/test/input/MotionPredictorTest.kt index d3eeac147c2a..784eb1b37765 100644 --- a/tests/Input/src/com/android/test/input/MotionPredictorTest.kt +++ b/tests/Input/src/com/android/test/input/MotionPredictorTest.kt @@ -26,11 +26,10 @@ import android.view.MotionEvent.ACTION_MOVE import android.view.MotionEvent.PointerCoords import android.view.MotionEvent.PointerProperties import android.view.MotionPredictor - import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry - +import java.time.Duration import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull @@ -40,14 +39,12 @@ import org.junit.runner.RunWith import org.mockito.Mockito.mock import org.mockito.Mockito.`when` -import java.time.Duration - private fun getStylusMotionEvent( - eventTime: Duration, - action: Int, - x: Float, - y: Float, - ): MotionEvent{ + eventTime: Duration, + action: Int, + x: Float, + y: Float, +): MotionEvent { val pointerCount = 1 val properties = arrayOfNulls<MotionEvent.PointerProperties>(pointerCount) val coords = arrayOfNulls<MotionEvent.PointerCoords>(pointerCount) @@ -61,21 +58,32 @@ private fun getStylusMotionEvent( coords[i]!!.y = y } - return MotionEvent.obtain(/*downTime=*/0, eventTime.toMillis(), action, properties.size, - properties, coords, /*metaState=*/0, /*buttonState=*/0, - /*xPrecision=*/0f, /*yPrecision=*/0f, /*deviceId=*/0, /*edgeFlags=*/0, - InputDevice.SOURCE_STYLUS, /*flags=*/0) + return MotionEvent.obtain( + /*downTime=*/ 0, + eventTime.toMillis(), + action, + properties.size, + properties, + coords, + /*metaState=*/ 0, + /*buttonState=*/ 0, + /*xPrecision=*/ 0f, + /*yPrecision=*/ 0f, + /*deviceId=*/ 0, + /*edgeFlags=*/ 0, + InputDevice.SOURCE_STYLUS, + /*flags=*/ 0, + ) } private fun getPredictionContext(offset: Duration, enablePrediction: Boolean): Context { val context = mock(Context::class.java) val resources: Resources = mock(Resources::class.java) `when`(context.getResources()).thenReturn(resources) - `when`(resources.getInteger( - com.android.internal.R.integer.config_motionPredictionOffsetNanos)).thenReturn( - offset.toNanos().toInt()) - `when`(resources.getBoolean( - com.android.internal.R.bool.config_enableMotionPrediction)).thenReturn(enablePrediction) + `when`(resources.getInteger(com.android.internal.R.integer.config_motionPredictionOffsetNanos)) + .thenReturn(offset.toNanos().toInt()) + `when`(resources.getBoolean(com.android.internal.R.bool.config_enableMotionPrediction)) + .thenReturn(enablePrediction) return context } @@ -88,38 +96,36 @@ class MotionPredictorTest { @Before fun setUp() { instrumentation.uiAutomation.executeShellCommand( - "setprop persist.input.enable_motion_prediction true") + "setprop persist.input.enable_motion_prediction true" + ) } @After fun tearDown() { instrumentation.uiAutomation.executeShellCommand( - "setprop persist.input.enable_motion_prediction $initialPropertyValue") + "setprop persist.input.enable_motion_prediction $initialPropertyValue" + ) } /** - * In a typical usage, app will send the event to the predictor and then call .predict to draw - * a prediction. Here, we send 2 events to the predictor and check the returned event. - * Input: - * t = 0 x = 0 y = 0 - * t = 4 x = 10 y = 20 - * Output (expected): - * t = 12 x = 30 y = 60 ± error + * In a typical usage, app will send the event to the predictor and then call .predict to draw a + * prediction. Here, we send 2 events to the predictor and check the returned event. Input: t = + * 0 x = 0 y = 0 t = 4 x = 10 y = 20 Output (expected): t = 12 x = 30 y = 60 ± error * * Historical data is ignored for simplicity. */ @Test fun testPredictedCoordinatesAndTime() { - val context = getPredictionContext( - /*offset=*/Duration.ofMillis(1), /*enablePrediction=*/true) + val context = + getPredictionContext(/* offset= */ Duration.ofMillis(1), /* enablePrediction= */ true) val predictor = MotionPredictor(context) var eventTime = Duration.ofMillis(0) - val downEvent = getStylusMotionEvent(eventTime, ACTION_DOWN, /*x=*/0f, /*y=*/0f) + val downEvent = getStylusMotionEvent(eventTime, ACTION_DOWN, /* x= */ 0f, /* y= */ 0f) // ACTION_DOWN t=0 x=0 y=0 predictor.record(downEvent) eventTime += Duration.ofMillis(4) - val moveEvent = getStylusMotionEvent(eventTime, ACTION_MOVE, /*x=*/10f, /*y=*/20f) + val moveEvent = getStylusMotionEvent(eventTime, ACTION_MOVE, /* x= */ 10f, /* y= */ 20f) // ACTION_MOVE t=1 x=1 y=2 predictor.record(moveEvent) @@ -129,7 +135,7 @@ class MotionPredictorTest { // Prediction will happen for t=12 (since it is the next input interval after the requested // time, 8, plus the model offset, 1). assertEquals(12, predicted!!.eventTime) - assertEquals(30f, predicted.x, /*delta=*/10f) - assertEquals(60f, predicted.y, /*delta=*/15f) + assertEquals(30f, predicted.x, /* delta= */ 10f) + assertEquals(60f, predicted.y, /* delta= */ 15f) } } diff --git a/tests/Input/src/com/android/test/input/PointerEventDispatcherTest.kt b/tests/Input/src/com/android/test/input/PointerEventDispatcherTest.kt index f311bc222d22..3c4ba5a3cd09 100644 --- a/tests/Input/src/com/android/test/input/PointerEventDispatcherTest.kt +++ b/tests/Input/src/com/android/test/input/PointerEventDispatcherTest.kt @@ -21,11 +21,10 @@ import android.view.InputChannel import android.view.InputDevice import android.view.MotionEvent import android.view.WindowManagerPolicyConstants.PointerEventListener - import com.android.server.UiThread import com.android.server.wm.PointerEventDispatcher -import org.junit.Assert.assertEquals import org.junit.After +import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -39,6 +38,7 @@ class PointerEventDispatcherTest { companion object { private const val TAG = "PointerEventDispatcherTest" } + private val mHandlerThread = HandlerThread("Process input events") private lateinit var mSender: SpyInputEventSender private lateinit var mPointerEventDispatcher: PointerEventDispatcher @@ -75,8 +75,15 @@ class PointerEventDispatcherTest { // The MotionEvent properties aren't important for this test, as long as the event // is a pointer event, so that it gets processed by CrashingPointerEventListener val downTime = 0L - val motionEvent = MotionEvent.obtain(downTime, downTime, - MotionEvent.ACTION_DOWN, 0f /* x */, 0f /* y */, 0 /* metaState */) + val motionEvent = + MotionEvent.obtain( + downTime, + downTime, + MotionEvent.ACTION_DOWN, + 0f /* x */, + 0f /* y */, + 0, /* metaState */ + ) motionEvent.source = InputDevice.SOURCE_TOUCHSCREEN val seq = 10 mSender.sendInputEvent(seq, motionEvent) diff --git a/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt index abfe549f3d22..443ef6937c53 100644 --- a/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt +++ b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt @@ -42,8 +42,7 @@ import platform.test.screenshot.matchers.PixelPerfectMatcher /** * Unit tests for PointerIcon. * - * Run with: - * atest InputTests:com.android.test.input.PointerIconLoadingTest + * Run with: atest InputTests:com.android.test.input.PointerIconLoadingTest */ @SmallTest @RunWith(AndroidJUnit4::class) @@ -51,16 +50,19 @@ class PointerIconLoadingTest { private lateinit var context: Context private lateinit var exactScreenshotMatcher: BitmapMatcher - @get:Rule - val testName = TestName() + @get:Rule val testName = TestName() @get:Rule - val screenshotRule = ScreenshotTestRule(GoldenPathManager( - InstrumentationRegistry.getInstrumentation().getContext(), - ASSETS_PATH, - TEST_OUTPUT_PATH, - PathConfig() - ), disableIconPool = false) + val screenshotRule = + ScreenshotTestRule( + GoldenPathManager( + InstrumentationRegistry.getInstrumentation().getContext(), + ASSETS_PATH, + TEST_OUTPUT_PATH, + PathConfig(), + ), + disableIconPool = false, + ) @Before fun setUp() { @@ -86,22 +88,26 @@ class PointerIconLoadingTest { theme.setTo(context.getTheme()) theme.applyStyle( PointerIcon.vectorFillStyleToResource(PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_GREEN), - /* force= */ true) - theme.applyStyle(PointerIcon.vectorStrokeStyleToResource( - PointerIcon.POINTER_ICON_VECTOR_STYLE_STROKE_WHITE), /* force= */ true) + /* force= */ true, + ) + theme.applyStyle( + PointerIcon.vectorStrokeStyleToResource( + PointerIcon.POINTER_ICON_VECTOR_STYLE_STROKE_WHITE + ), + /* force= */ true, + ) val pointerIcon = PointerIcon.getLoadedSystemIcon( ContextThemeWrapper(context, theme), PointerIcon.TYPE_ARROW, /* useLargeIcons= */ false, - /* pointerScale= */ 1f) + /* pointerScale= */ 1f, + ) - pointerIcon.getBitmap().assertAgainstGolden( - screenshotRule, - testName.methodName, - exactScreenshotMatcher - ) + pointerIcon + .getBitmap() + .assertAgainstGolden(screenshotRule, testName.methodName, exactScreenshotMatcher) } @Test @@ -113,22 +119,26 @@ class PointerIconLoadingTest { theme.setTo(context.getTheme()) theme.applyStyle( PointerIcon.vectorFillStyleToResource(PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_BLACK), - /* force= */ true) - theme.applyStyle(PointerIcon.vectorStrokeStyleToResource( - PointerIcon.POINTER_ICON_VECTOR_STYLE_STROKE_BLACK), /* force= */ true) + /* force= */ true, + ) + theme.applyStyle( + PointerIcon.vectorStrokeStyleToResource( + PointerIcon.POINTER_ICON_VECTOR_STYLE_STROKE_BLACK + ), + /* force= */ true, + ) val pointerIcon = PointerIcon.getLoadedSystemIcon( ContextThemeWrapper(context, theme), PointerIcon.TYPE_ARROW, /* useLargeIcons= */ false, - /* pointerScale= */ 1f) + /* pointerScale= */ 1f, + ) - pointerIcon.getBitmap().assertAgainstGolden( - screenshotRule, - testName.methodName, - exactScreenshotMatcher - ) + pointerIcon + .getBitmap() + .assertAgainstGolden(screenshotRule, testName.methodName, exactScreenshotMatcher) } @Test @@ -140,11 +150,14 @@ class PointerIconLoadingTest { theme.setTo(context.getTheme()) theme.applyStyle( PointerIcon.vectorFillStyleToResource(PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_BLACK), - /* force= */ true) + /* force= */ true, + ) theme.applyStyle( PointerIcon.vectorStrokeStyleToResource( - PointerIcon.POINTER_ICON_VECTOR_STYLE_STROKE_WHITE), - /* force= */ true) + PointerIcon.POINTER_ICON_VECTOR_STYLE_STROKE_WHITE + ), + /* force= */ true, + ) val pointerScale = 2f val pointerIcon = @@ -152,13 +165,12 @@ class PointerIconLoadingTest { ContextThemeWrapper(context, theme), PointerIcon.TYPE_ARROW, /* useLargeIcons= */ false, - pointerScale) + pointerScale, + ) - pointerIcon.getBitmap().assertAgainstGolden( - screenshotRule, - testName.methodName, - exactScreenshotMatcher - ) + pointerIcon + .getBitmap() + .assertAgainstGolden(screenshotRule, testName.methodName, exactScreenshotMatcher) } companion object { diff --git a/tests/Input/src/com/android/test/input/SpyInputEventSenderAndReceiver.kt b/tests/Input/src/com/android/test/input/SpyInputEventSenderAndReceiver.kt index 5cbfce534b15..05aa5e91132b 100644 --- a/tests/Input/src/com/android/test/input/SpyInputEventSenderAndReceiver.kt +++ b/tests/Input/src/com/android/test/input/SpyInputEventSenderAndReceiver.kt @@ -26,7 +26,6 @@ import android.view.KeyEvent import android.view.MotionEvent import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.TimeUnit - import org.junit.Assert.assertNull private fun <T> getEvent(queue: LinkedBlockingQueue<T>): T? { @@ -39,7 +38,7 @@ private fun <T> assertNoEvents(queue: LinkedBlockingQueue<T>) { } class SpyInputEventReceiver(channel: InputChannel, looper: Looper) : - InputEventReceiver(channel, looper) { + InputEventReceiver(channel, looper) { private val mInputEvents = LinkedBlockingQueue<InputEvent>() override fun onInputEvent(event: InputEvent) { @@ -57,8 +56,9 @@ class SpyInputEventReceiver(channel: InputChannel, looper: Looper) : } class SpyInputEventSender(channel: InputChannel, looper: Looper) : - InputEventSender(channel, looper) { + InputEventSender(channel, looper) { data class FinishedSignal(val seq: Int, val handled: Boolean) + data class Timeline(val inputEventId: Int, val gpuCompletedTime: Long, val presentTime: Long) private val mFinishedSignals = LinkedBlockingQueue<FinishedSignal>() diff --git a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt index 1a0837b6d3d7..49b224a751b6 100644 --- a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt +++ b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt @@ -59,9 +59,7 @@ import org.junit.runners.Parameterized class UinputRecordingIntegrationTests { companion object { - /** - * Add new test cases by adding a new [TestData] to the following list. - */ + /** Add new test cases by adding a new [TestData] to the following list. */ @JvmStatic @Parameterized.Parameters(name = "{0}") fun data(): Iterable<Any> = @@ -74,12 +72,10 @@ class UinputRecordingIntegrationTests { vendorId = 0x0603, productId = 0x7806, deviceSources = InputDevice.SOURCE_TOUCHSCREEN, - ), + ) ) - /** - * Use the debug mode to see the JSON-encoded received events in logcat. - */ + /** Use the debug mode to see the JSON-encoded received events in logcat. */ const val DEBUG_RECEIVED_EVENTS = false const val INPUT_DEVICE_SOURCE_ALL = -1 @@ -101,14 +97,11 @@ class UinputRecordingIntegrationTests { private lateinit var instrumentation: Instrumentation private lateinit var parser: InputJsonParser - @get:Rule - val debugInputRule = DebugInputRule() + @get:Rule val debugInputRule = DebugInputRule() - @get:Rule - val testName = TestName() + @get:Rule val testName = TestName() - @Parameterized.Parameter(0) - lateinit var testData: TestData + @Parameterized.Parameter(0) lateinit var testData: TestData @Before fun setUp() { @@ -120,43 +113,47 @@ class UinputRecordingIntegrationTests { @Test fun testEvemuRecording() { VirtualDisplayActivityScenario.AutoClose<CaptureEventActivity>( - testName, - size = testData.displaySize - ).use { scenario -> - scenario.activity.window.decorView.requestUnbufferedDispatch(INPUT_DEVICE_SOURCE_ALL) - - EvemuDevice( - instrumentation, - testData.deviceSources, - testData.vendorId, - testData.productId, - testData.uinputRecordingResource, - scenario.virtualDisplay.display - ).use { evemuDevice -> - - evemuDevice.injectEvents() - - if (DEBUG_RECEIVED_EVENTS) { - printReceivedEventsToLogcat(scenario.activity) - fail("Test cannot pass in debug mode!") - } - - val verifier = EventVerifier( - BatchedEventSplitter { scenario.activity.getInputEvent() } + testName, + size = testData.displaySize, + ) + .use { scenario -> + scenario.activity.window.decorView.requestUnbufferedDispatch( + INPUT_DEVICE_SOURCE_ALL ) - verifyEvents(verifier) - scenario.activity.assertNoEvents() + + EvemuDevice( + instrumentation, + testData.deviceSources, + testData.vendorId, + testData.productId, + testData.uinputRecordingResource, + scenario.virtualDisplay.display, + ) + .use { evemuDevice -> + evemuDevice.injectEvents() + + if (DEBUG_RECEIVED_EVENTS) { + printReceivedEventsToLogcat(scenario.activity) + fail("Test cannot pass in debug mode!") + } + + val verifier = + EventVerifier( + BatchedEventSplitter { scenario.activity.getInputEvent() } + ) + verifyEvents(verifier) + scenario.activity.assertNoEvents() + } } - } } private fun printReceivedEventsToLogcat(activity: CaptureEventActivity) { val getNextEvent = BatchedEventSplitter { activity.getInputEvent() } var receivedEvent: InputEvent? = getNextEvent() while (receivedEvent != null) { - Log.d(TAG, - parser.encodeEvent(receivedEvent)?.toString() - ?: "(Failed to encode received event)" + Log.d( + TAG, + parser.encodeEvent(receivedEvent)?.toString() ?: "(Failed to encode received event)", ) receivedEvent = getNextEvent() } diff --git a/tests/Input/src/com/android/test/input/UnresponsiveGestureMonitorActivity.kt b/tests/Input/src/com/android/test/input/UnresponsiveGestureMonitorActivity.kt index 1e44617af111..0366abed1d48 100644 --- a/tests/Input/src/com/android/test/input/UnresponsiveGestureMonitorActivity.kt +++ b/tests/Input/src/com/android/test/input/UnresponsiveGestureMonitorActivity.kt @@ -1,17 +1,15 @@ /** * Copyright (c) 2020 The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. + * 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. */ // InputMonitor is deprecated, but we still need to test it. diff --git a/tests/Input/src/com/android/test/input/ViewFrameInfoTest.kt b/tests/Input/src/com/android/test/input/ViewFrameInfoTest.kt index 6ef1ecdae59b..8405a67332ef 100644 --- a/tests/Input/src/com/android/test/input/ViewFrameInfoTest.kt +++ b/tests/Input/src/com/android/test/input/ViewFrameInfoTest.kt @@ -28,6 +28,7 @@ class ViewFrameInfoTest { companion object { private const val TAG = "ViewFrameInfoTest" } + private val mViewFrameInfo = ViewFrameInfo() private var mTimeStarted: Long = 0 @@ -65,8 +66,8 @@ class ViewFrameInfoTest { // The values inside FrameInfo should match those from ViewFrameInfo after we update them mViewFrameInfo.populateFrameInfo(frameInfo) assertThat(frameInfo.frameInfo[FrameInfo.INPUT_EVENT_ID]).isEqualTo(139) - assertThat(frameInfo.frameInfo[FrameInfo.FLAGS]).isEqualTo( - FrameInfo.FLAG_WINDOW_VISIBILITY_CHANGED) + assertThat(frameInfo.frameInfo[FrameInfo.FLAGS]) + .isEqualTo(FrameInfo.FLAG_WINDOW_VISIBILITY_CHANGED) assertThat(frameInfo.frameInfo[FrameInfo.DRAW_START]).isGreaterThan(mTimeStarted) } -}
\ No newline at end of file +} diff --git a/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java b/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java index ed256e72b415..d30ec1699d03 100644 --- a/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java @@ -552,7 +552,7 @@ public class ProcessedPerfettoProtoLogImplTest { } final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); - assertThrows(IllegalStateException.class, reader::readProtoLogTrace); + assertThrows(java.net.SocketException.class, reader::readProtoLogTrace); } @Test diff --git a/tools/aapt2/cmd/Compile.h b/tools/aapt2/cmd/Compile.h index e244546476b0..88d61b534209 100644 --- a/tools/aapt2/cmd/Compile.h +++ b/tools/aapt2/cmd/Compile.h @@ -75,7 +75,7 @@ class CompileCommand : public Command { AddOptionalSwitch("--preserve-visibility-of-styleables", "If specified, apply the same visibility rules for\n" "styleables as are used for all other resources.\n" - "Otherwise, all stylesables will be made public.", + "Otherwise, all styleables will be made public.", &options_.preserve_visibility_of_styleables); AddOptionalFlag("--visibility", "Sets the visibility of the compiled resources to the specified\n" diff --git a/vendor/google_testing/integration/tests/scenarios/screenshots/cuttlefish/flexiglass/six_digits_pin_delete.png b/vendor/google_testing/integration/tests/scenarios/screenshots/cuttlefish/flexiglass/six_digits_pin_delete.png Binary files differnew file mode 100644 index 000000000000..d4e72fbd6e2f --- /dev/null +++ b/vendor/google_testing/integration/tests/scenarios/screenshots/cuttlefish/flexiglass/six_digits_pin_delete.png |