diff options
166 files changed, 3826 insertions, 1871 deletions
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/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/core/api/current.txt b/core/api/current.txt index 151a6738505c..27aa3518f958 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"; @@ -27222,12 +27223,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 { @@ -44050,6 +44073,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"; @@ -56113,6 +56137,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"; 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..b92df4cf7884 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -18977,10 +18977,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/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/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/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/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/security/flags.aconfig b/core/java/android/security/flags.aconfig index edfb78e59fe3..a2403826fe32 100644 --- a/core/java/android/security/flags.aconfig +++ b/core/java/android/security/flags.aconfig @@ -68,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/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/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/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 9a62045f3435..c275ed3a3b06 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -11518,12 +11518,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()) { @@ -11548,6 +11560,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 { @@ -11574,6 +11596,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/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/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index 983be682b8aa..527fcdf852f3 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -64,7 +64,7 @@ public enum DesktopModeFlags { ENABLE_DESKTOP_COMPAT_UI_VISIBILITY_STATUS(Flags::enableCompatUiVisibilityStatus, true), 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, true), ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX( 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/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/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/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 f62ce278f28a..636968dd1152 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -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" diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml index 36564cd90d05..9b3a6cba5f23 100644 --- a/core/res/res/xml/sms_short_codes.xml +++ b/core/res/res/xml/sms_short_codes.xml @@ -358,7 +358,7 @@ <!-- USA: 5-6 digits (premium codes from https://www.premiumsmsrefunds.com/ShortCodes.htm), visual voicemail code for T-Mobile: 122 --> - <shortcode country="us" pattern="\\d{5,6}" free="122|\\d{5,6}" /> + <shortcode country="us" pattern="\\d{5,6}" premium="20433|21(?:344|472)|22715|23(?:333|847)|24(?:15|28)0|25209|27(?:449|606|663)|28498|305(?:00|83)|32(?:340|941)|33(?:166|786|849)|34746|35(?:182|564)|37975|38(?:135|146|254)|41(?:366|463)|42335|43(?:355|500)|44(?:578|711|811)|45814|46(?:157|173|327)|46666|47553|48(?:221|277|669)|50(?:844|920)|51(?:062|368)|52944|54(?:723|892)|55928|56483|57370|59(?:182|187|252|342)|60339|61(?:266|982)|62478|64(?:219|898)|65(?:108|500)|69(?:208|388)|70877|71851|72(?:078|087|465)|73(?:288|588|882|909|997)|74(?:034|332|815)|76426|79213|81946|83177|84(?:103|685)|85797|86(?:234|236|666)|89616|90(?:715|842|938)|91(?:362|958)|94719|95297|96(?:040|666|835|969)|97(?:142|294|688)|99(?:689|796|807)" standard="44567|244444" free="122|87902|21696|24614|28003|30356|33669|40196|41064|41270|43753|44034|46645|52413|56139|57969|61785|66975|75136|76227|81398|83952|85140|86566|86799|95737|96684|99245|611611|96831|10907" /> <!--Uruguay : 1-6 digits (standard system default, not country specific) --> <shortcode country="uy" pattern="\\d{1,6}" free="55002|191289" /> 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/content/res/XmlResourcesFlaggedTest.kt b/core/tests/coretests/src/android/content/res/XmlResourcesFlaggedTest.kt new file mode 100644 index 000000000000..8c20ba0d7fbe --- /dev/null +++ b/core/tests/coretests/src/android/content/res/XmlResourcesFlaggedTest.kt @@ -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 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) +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()) + } +}
\ No newline at end of file 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/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/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/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/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/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/PictureProfile.java b/media/java/android/media/quality/PictureProfile.java index 8a585efe032c..3bccd89c91c3 100644 --- a/media/java/android/media/quality/PictureProfile.java +++ b/media/java/android/media/quality/PictureProfile.java @@ -114,6 +114,18 @@ 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"; + private PictureProfile(@NonNull Parcel in) { mId = in.readString(); 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 b5afa6afa5e0..6ac1656b77aa 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 @@ -47,7 +47,7 @@ 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); + 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); diff --git a/media/tests/projection/Android.bp b/media/tests/projection/Android.bp index 0b02d3cb4250..0b4b7dbbca1f 100644 --- a/media/tests/projection/Android.bp +++ b/media/tests/projection/Android.bp @@ -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/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/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/values-v36/styles_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v36/styles_expressive.xml index de48f99215fb..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> @@ -125,4 +136,4 @@ <item name="android:layout_marginEnd">@dimen/settingslib_expressive_space_extrasmall4</item> <item name="android:background">@drawable/settingslib_expressive_button_background_outline</item> </style> -</resources>
\ No newline at end of file +</resources> diff --git a/packages/SettingsLib/SettingsTheme/res/values-v36/themes_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v36/themes_expressive.xml index 5173ebeaa9a1..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> @@ -66,4 +66,4 @@ <item name="buttonBarNegativeButtonStyle">@style/Widget.SettingsLib.DialogButton.Outline</item> <item name="buttonBarNeutralButtonStyle">@style/Widget.SettingsLib.DialogButton</item> </style> -</resources>
\ No newline at end of file +</resources> diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt index cca43b92ef19..84d61fc86073 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt @@ -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,7 +511,7 @@ open class WifiUtils { AdvancedProtectionManager.FEATURE_ID_DISALLOW_WEP, AdvancedProtectionManager.SUPPORT_DIALOG_TYPE_BLOCKED_INTERACTION) intent.putExtra(DIALOG_WINDOW_TYPE, dialogWindowType) - withContext(Dispatchers.Main) { onStartActivity(intent) } + withContext(Dispatchers.Main) { onStartAapmActivity(intent) } } else if (wifiManager.isWepSupported == true && wifiManager.queryWepAllowed()) { withContext(Dispatchers.Main) { onAllowed() } } else { 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/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 91492b2959d8..f65ca3b60818 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -600,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" 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/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/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/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/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/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/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/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/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationsInteractorTest.kt index bad33a402ff7..915edc03952d 100644 --- 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 @@ -32,12 +32,11 @@ 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.core.StatusBarRootModernization 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.StatusBarChipsModernization +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 @@ -50,12 +49,8 @@ import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) -@EnableFlags( - PromotedNotificationUi.FLAG_NAME, - StatusBarNotifChips.FLAG_NAME, - StatusBarChipsModernization.FLAG_NAME, - StatusBarRootModernization.FLAG_NAME, -) +@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) +@EnableChipsModernization class AODPromotedNotificationsInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() @@ -111,10 +106,10 @@ class AODPromotedNotificationsInteractorTest : SysuiTestCase() { renderNotificationListInteractor.setRenderedList(listOf(ronEntry)) - // THEN aod content is sensitive + // THEN aod content is redacted val content by collectLastValue(underTest.content) assertThat(content).isNotNull() - assertThat(content?.title).isNull() // SOON: .isEqualTo("REDACTED") + assertThat(content!!.title).isEqualTo("REDACTED") } @Test @@ -128,10 +123,10 @@ class AODPromotedNotificationsInteractorTest : SysuiTestCase() { renderNotificationListInteractor.setRenderedList(listOf(ronEntry)) - // THEN aod content is sensitive + // THEN aod content is redacted val content by collectLastValue(underTest.content) assertThat(content).isNotNull() - assertThat(content?.title).isNull() // SOON: .isEqualTo("REDACTED") + assertThat(content!!.title).isEqualTo("REDACTED") } private fun Kosmos.setKeyguardLocked(locked: Boolean) { 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/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/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/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/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..2fdca6bc68d9 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 @@ -193,7 +193,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 @@ -584,7 +583,6 @@ object KeyguardRootViewBinder { private val aodNotificationIconContainerId = R.id.aod_notification_icon_container 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 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/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/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt index f717431f6a40..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 @@ -39,7 +39,7 @@ import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDi 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 @@ -102,7 +102,7 @@ constructor( 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 efdc5abf1f67..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 @@ -31,7 +31,7 @@ import com.android.systemui.shade.ShadeDisplayAware 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 @@ -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 } @@ -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/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/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/KeyguardSmartspaceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt index 5cc34e749b46..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,6 +17,8 @@ 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 @@ -94,6 +96,43 @@ constructor( 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/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java index bf1f971c0f8c..4f86257e3870 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java @@ -609,8 +609,7 @@ public class MediaSwitchingController devices, getSelectedMediaDevice(), connectedMediaDevice, - needToHandleMutingExpectedDevice, - getConnectNewDeviceItem()); + needToHandleMutingExpectedDevice); } else { List<MediaItem> updatedMediaItems = buildMediaItems( @@ -701,7 +700,6 @@ public class MediaSwitchingController } } dividerItems.forEach(finalMediaItems::add); - attachConnectNewDeviceItemIfNeeded(finalMediaItems); return finalMediaItems; } } @@ -765,7 +763,6 @@ public class MediaSwitchingController finalMediaItems.add(MediaItem.createDeviceMediaItem(device)); } } - attachConnectNewDeviceItemIfNeeded(finalMediaItems); return finalMediaItems; } @@ -879,6 +876,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 +892,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/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/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt index 465c78e91e53..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 @@ -23,17 +23,14 @@ import com.android.systemui.lifecycle.Hydrator import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractor -import com.android.systemui.shade.domain.interactor.ShadeModeInteractor 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 @@ -51,31 +48,12 @@ constructor( val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory, val sceneInteractor: SceneInteractor, private val shadeInteractor: ShadeInteractor, - shadeModeInteractor: ShadeModeInteractor, disableFlagsInteractor: DisableFlagsInteractor, mediaCarouselInteractor: MediaCarouselInteractor, - activeNotificationsInteractor: ActiveNotificationsInteractor, ) : ExclusiveActivatable() { private val hydrator = Hydrator("NotificationsShadeOverlayContentViewModel.hydrator") - val showClock: Boolean by - hydrator.hydratedStateOf( - traceName = "showClock", - initialValue = - shouldShowClock( - isShadeLayoutWide = shadeModeInteractor.isShadeLayoutWide.value, - areAnyNotificationsPresent = - activeNotificationsInteractor.areAnyNotificationsPresentValue, - ), - source = - combine( - shadeModeInteractor.isShadeLayoutWide, - activeNotificationsInteractor.areAnyNotificationsPresent, - this::shouldShowClock, - ), - ) - val showMedia: Boolean by hydrator.hydratedStateOf( traceName = "showMedia", @@ -114,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/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt index 1a0af514cf87..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 @@ -290,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, @@ -322,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/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/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 c800ab3d0bf2..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 @@ -54,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 @@ -101,6 +101,7 @@ 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 { @@ -646,8 +647,8 @@ constructor( // result in broken states. return true } + var handled = hubShowing try { - var handled = false if (!touchTakenByKeyguardGesture) { communalContainerWrapper?.dispatchTouchEvent(ev) { if (it) { @@ -655,18 +656,10 @@ constructor( } } } - return handled || hubShowing + return handled } finally { - if (Flags.bouncerUiRevamp()) { - userActivityNotifier.notifyUserActivity( - event = PowerManager.USER_ACTIVITY_EVENT_TOUCH - ) - } else { - powerManager.userActivity( - SystemClock.uptimeMillis(), - PowerManager.USER_ACTIVITY_EVENT_TOUCH, - 0, - ) + if (handled) { + userTouchActivityNotifier.notifyActivity(ev) } } } 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 9282e166f605..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 @@ -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,7 +206,6 @@ private class FrameLayoutWithMaxHeight(maxHeight: Int, context: Context) : Frame } else { childMeasuredHeight } - setMeasuredDimension(ownMeasuredWidth, ownMeasuredHeight) } } @@ -205,18 +215,22 @@ private val PromotedNotificationContentModel.layoutResource: Int? 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 } @@ -333,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 -> {} } @@ -346,11 +362,15 @@ private class AODPromotedNotificationViewUpdater(root: View) { private fun updateBase( content: PromotedNotificationContentModel, + collapsed: Boolean, textView: ImageFloatingTextView? = text, ) { - updateHeader(content) + val headerTitleView = if (collapsed) title else null + updateHeader(content, titleView = headerTitleView, collapsed = collapsed) - updateTitle(title, content) + if (headerTitleView == null) { + updateTitle(title, content) + } updateText(textView, content) updateSmallIcon(icon, content) updateImageView(rightIcon, content.skeletonLargeIcon) @@ -358,21 +378,21 @@ private class AODPromotedNotificationViewUpdater(root: View) { } 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) + updateBase(content, collapsed = false) updateNewProgressBar(content) } @@ -409,24 +429,35 @@ private class AODPromotedNotificationViewUpdater(root: View) { } } - private fun updateHeader(content: PromotedNotificationContentModel) { - updateAppName(content) + private fun updateHeader( + content: PromotedNotificationContentModel, + collapsed: Boolean, + titleView: TextView?, + ) { + 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) - // Not calling updateTitle(headerText, content) because the title is always a separate - // element in the expanded layout used for AOD RONs. + updateTitle(titleView, content) updateTimeAndChronometer(content) - updateHeaderDividers(content) + updateHeaderDividers(content, hideTitle = !hasTitle, hideAppName = hideAppName) updateTopLine(content) } - private fun updateHeaderDividers(content: PromotedNotificationContentModel) { - val hasAppName = content.appName != null + private fun updateHeaderDividers( + content: PromotedNotificationContentModel, + hideAppName: Boolean, + hideTitle: Boolean, + ) { + val hasAppName = content.appName != null && !hideAppName val hasSubText = content.subText != null - // Not setting hasHeader = content.title because the title is always a separate element in - // the expanded layout used for AOD RONs. - val hasHeader = false + val hasHeader = content.title != null && !hideTitle val hasTimeOrChronometer = content.time != null val hasTextBeforeSubText = hasAppName @@ -442,13 +473,17 @@ private class AODPromotedNotificationViewUpdater(root: View) { timeDivider?.isVisible = showDividerBeforeTime } - private fun updateConversationHeader(content: PromotedNotificationContentModel) { - updateAppName(content) + private fun updateConversationHeader( + content: PromotedNotificationContentModel, + collapsed: Boolean, + ) { + updateAppName(content, forceHide = collapsed) updateTimeAndChronometer(content) + updateImageView(verificationIcon, content.verificationIcon) updateTextView(verificationText, content.verificationText) - updateConversationHeaderDividers(content) + updateConversationHeaderDividers(content, hideTitle = true, hideAppName = collapsed) updateTopLine(content) @@ -456,11 +491,13 @@ private class AODPromotedNotificationViewUpdater(root: View) { updateTitle(conversationText, content) } - private fun updateConversationHeaderDividers(content: PromotedNotificationContentModel) { - // Not setting hasTitle = content.title because the title is always a separate element in - // the expanded layout used for AOD RONs. - val hasTitle = false - val hasAppName = content.appName != null + private fun updateConversationHeaderDividers( + content: PromotedNotificationContentModel, + hideTitle: Boolean, + hideAppName: Boolean, + ) { + val hasTitle = content.title != null && !hideTitle + val hasAppName = content.appName != null && !hideAppName val hasTimeOrChronometer = content.time != null val hasVerification = !content.verificationIcon.isNullOrEmpty() || content.verificationText != null @@ -478,8 +515,8 @@ 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) { 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 d9bdfbc81145..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() @@ -191,12 +232,16 @@ constructor( 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() } @@ -206,9 +251,9 @@ constructor( 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() } @@ -293,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 } @@ -316,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, 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 5c0991059dec..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiForceExpanded.kt +++ /dev/null @@ -1,66 +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 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 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 - - /** A token used for dependency declaration */ - val token: FlagToken - get() = FlagToken(FLAG_NAME, isEnabled) - - /** Is the refactor enabled */ - @JvmStatic - inline val isEnabled - get() = Flags.uiRichOngoing() - - /** - * 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/shared/model/PromotedNotificationContentModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt index 339a5bb29a34..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 @@ -174,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, } 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 740391d7010e..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 @@ -128,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; @@ -882,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; @@ -1381,7 +1380,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (mIsSummaryWithChildren) { return mChildrenContainer.getIntrinsicHeight(); } - if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) { + if (isPromotedOngoing()) { return getMaxExpandHeight(); } if (mExpandedWhenPinned) { @@ -3030,7 +3029,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (mIsSummaryWithChildren && !shouldShowPublic()) { return !mChildrenExpanded; } - if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) { + if (isPromotedOngoing()) { return false; } return mEnableNonGroupedNotificationExpand && mExpandable; @@ -3141,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); @@ -3411,7 +3410,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public boolean isExpanded(boolean allowOnKeyguard) { - if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) { + if (isPromotedOngoing()) { return isPromotedNotificationExpanded(allowOnKeyguard); } 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 d5e2e7eb3a9c..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 @@ -50,7 +50,6 @@ import com.android.systemui.statusbar.TransformableView; import com.android.systemui.statusbar.ViewTransformationHelper; import com.android.systemui.statusbar.notification.ImageTransformState; import com.android.systemui.statusbar.notification.TransformState; -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.HybridNotificationView; import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; @@ -196,8 +195,7 @@ public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapp } private void adjustTitleAndRightIconForPromotedOngoing() { - if (PromotedNotificationUiForceExpanded.isEnabled() && - 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/promoted/PromotedNotificationUiAod.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/AvalancheReplaceHunWhenCritical.kt index 69e27dcc2e6c..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,19 +14,17 @@ * limitations under the License. */ -package com.android.systemui.statusbar.notification.promoted +package com.android.systemui.statusbar.notification.shared -import android.app.Flags +import com.android.systemui.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 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_UI_RICH_ONGOING + const val FLAG_NAME = Flags.FLAG_AVALANCHE_REPLACE_HUN_WHEN_CRITICAL /** A token used for dependency declaration */ val token: FlagToken @@ -35,7 +33,7 @@ object PromotedNotificationUiAod { /** Is the refactor enabled */ @JvmStatic inline val isEnabled - get() = Flags.uiRichOngoing() + get() = Flags.avalancheReplaceHunWhenCritical() /** * Called to ensure code is only run when the flag is enabled. This protects users from the @@ -48,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/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/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/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt index 7728f684f0f2..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 @@ -64,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 @@ -137,6 +139,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { notificationStackScrollLayoutController, keyguardMediaController, lockscreenSmartspaceController, + userTouchActivityNotifier, logcatLogBuffer("GlanceableHubContainerControllerTest"), kosmos.userActivityNotifier, ) @@ -178,6 +181,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { notificationStackScrollLayoutController, keyguardMediaController, lockscreenSmartspaceController, + userTouchActivityNotifier, logcatLogBuffer("GlanceableHubContainerControllerTest"), kosmos.userActivityNotifier, ) @@ -208,6 +212,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { notificationStackScrollLayoutController, keyguardMediaController, lockscreenSmartspaceController, + userTouchActivityNotifier, logcatLogBuffer("GlanceableHubContainerControllerTest"), kosmos.userActivityNotifier, ) @@ -234,6 +239,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { notificationStackScrollLayoutController, keyguardMediaController, lockscreenSmartspaceController, + userTouchActivityNotifier, logcatLogBuffer("GlanceableHubContainerControllerTest"), kosmos.userActivityNotifier, ) @@ -539,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/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/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/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt index 23251d27cff9..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 @@ -22,9 +22,7 @@ import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarou import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor -import com.android.systemui.shade.domain.interactor.shadeModeInteractor 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: @@ -34,9 +32,7 @@ val Kosmos.notificationsShadeOverlayContentViewModel: notificationsPlaceholderViewModelFactory = notificationsPlaceholderViewModelFactory, sceneInteractor = sceneInteractor, shadeInteractor = shadeInteractor, - shadeModeInteractor = shadeModeInteractor, disableFlagsInteractor = disableFlagsInteractor, mediaCarouselInteractor = mediaCarouselInteractor, - activeNotificationsInteractor = activeNotificationsInteractor, ) } 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/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/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/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/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index f2ededaa2a9c..07530e1c6f7b 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; @@ -536,6 +539,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 +670,7 @@ public class InputManagerService extends IInputManager.Stub mKeyboardLayoutManager.systemRunning(); mBatteryController.systemRunning(); + mSysfsNodeMonitor.systemRunning(); mKeyboardBacklightController.systemRunning(); mKeyboardLedController.systemRunning(); mKeyRemapper.systemRunning(); diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index ccf1a2c90876..de54cd81aa43 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -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. @@ -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/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 87259d80554f..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; } @@ -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); } @@ -6709,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/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java index 0202b554b8aa..2152f76395a9 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityService.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java @@ -40,6 +40,7 @@ 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.AmbientBacklightEvent; import android.media.quality.AmbientBacklightMetadata; @@ -129,6 +130,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; @@ -230,21 +234,24 @@ public class MediaQualityService extends SystemService { @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()); + } + }); } private boolean hasPermissionToUpdatePictureProfile(Long dbId, PictureProfile toUpdate) { @@ -258,35 +265,37 @@ 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, - Binder.getCallingUid(), Binder.getCallingPid()); - } else { - mMqManagerNotifier.notifyOnPictureProfileRemoved( - mPictureProfileTempIdMap.getValue(dbId), toDelete, + PictureProfile.ERROR_NO_PERMISSION, 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); + } } } - } + }); } private boolean hasPermissionToRemovePictureProfile(PictureProfile toDelete) { @@ -368,13 +377,18 @@ 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 + params.putLong(BaseParameters.PARAMETER_ID, longId); PictureParameter[] pictureParameters = MediaQualityUtils .convertPersistableBundleToPictureParameterList(params); @@ -429,6 +443,7 @@ public class MediaQualityService extends SystemService { return toReturn; } + @GuardedBy("mSoundProfileLock") @Override public List<SoundProfileHandle> getSoundProfileHandle(String[] ids, int userId) { @@ -448,56 +463,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())) - && !hasGlobalSoundQualityServicePermission()) { - 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) { @@ -511,34 +530,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, - Binder.getCallingUid(), Binder.getCallingPid()); - } else { - mMqManagerNotifier.notifyOnSoundProfileRemoved( - mSoundProfileTempIdMap.getValue(dbId), toDelete, + SoundProfile.ERROR_NO_PERMISSION, 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) { @@ -619,12 +640,18 @@ 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) { + // put ID in params for profile update in HAL + params.putLong(BaseParameters.PARAMETER_ID, longId); SoundParameter[] soundParameters = MediaQualityUtils.convertPersistableBundleToSoundParameterList(params); @@ -738,6 +765,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( @@ -849,14 +896,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") @@ -877,13 +926,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 @@ -894,22 +946,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") @@ -932,22 +986,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") @@ -970,22 +1026,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") @@ -1127,15 +1186,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; @@ -1154,11 +1215,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( @@ -1172,12 +1231,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; @@ -1196,11 +1253,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( @@ -1416,8 +1471,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); @@ -1546,9 +1612,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 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..08a0b595033c 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java @@ -1273,14 +1273,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/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 0ea9af4b9c38..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; } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 8cf0481b1dc3..e8843ac214ec 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()) { @@ -4764,11 +4619,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { return true; } - if (overridePowerKeyBehaviorInFocusedWindow() && keyCode == KEYCODE_POWER) { - handleUnhandledSystemKey(event); - return true; - } - if (useKeyGestureEventHandler()) { return false; } @@ -5595,12 +5445,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 +5708,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 +5719,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 +7576,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/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/tv/TvInputHal.java b/services/core/java/com/android/server/tv/TvInputHal.java index 87ebdbfbf4e8..2a744e6a64ae 100644 --- a/services/core/java/com/android/server/tv/TvInputHal.java +++ b/services/core/java/com/android/server/tv/TvInputHal.java @@ -67,6 +67,8 @@ final class TvInputHal implements Handler.Callback { private static native void nativeClose(long ptr); 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; diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index 5eceb6490256..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; @@ -1189,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; } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 00a437cc31f9..c23dabcd2a48 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; @@ -9241,25 +9239,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 a03b765cae6a..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); } @@ -5507,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/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 e29511564cea..ee7c9368f897 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -2926,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(); @@ -3388,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/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/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt index eeac70afcffb..f3ab0e33d026 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 @@ -1755,13 +1755,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 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/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/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index 5427dc22e700..795273d47230 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; @@ -1154,53 +1152,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; 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/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java b/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java index a08b650b4c2f..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(); @@ -247,18 +238,8 @@ public class IntegrationTests { int mismatchedAppUID = 25; editText.reportAppJankStats(JankUtils.getAppJankStats(mismatchedAppUID)); - // 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 should be available. + JankUtils.waitForResults(jankTracker, WAIT_FOR_PENDING_JANKSTATS_MS); pendingStats = jankTracker.getPendingJankStats(); diff --git a/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java b/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java index 7067b873d4b7..302cad11bbb9 100644 --- a/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java +++ b/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java @@ -17,6 +17,7 @@ package android.app.jank.tests; import android.app.jank.AppJankStats; +import android.app.jank.JankTracker; import android.app.jank.RelativeFrameTimeHistogram; import android.os.Process; @@ -56,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/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt index 4737d19acde1..1858b1da916b 100644 --- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt +++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt @@ -714,18 +714,16 @@ class InputManagerServiceTests { ) } - val info = - KeyInterceptionInfo( - /* type = */ 0, - if (hasPrivateFlag) { - WindowManager.LayoutParams.PRIVATE_FLAG_ALLOW_ACTION_KEY_EVENTS - } else { - 0 - }, - "title", - /* uid = */ 0, - /* inputFeatureFlags = */ 0, - ) + val info = KeyInterceptionInfo( + /* type = */0, + if (hasPrivateFlag) { + WindowManager.LayoutParams.PRIVATE_FLAG_ALLOW_ACTION_KEY_EVENTS + } else { + 0 + }, + "title", + /* uid = */0 + ) whenever(windowManagerInternal.getKeyInterceptionInfoFromToken(any())).thenReturn(info) } } |